Malicious first depositor can steal all funds from all future depositors #811
Labels
3 (High Risk)
Assets can be stolen/lost/compromised directly
bug
Something isn't working
duplicate-42
satisfactory
satisfies C4 submission criteria; eligible for awards
sufficient quality report
This report is of sufficient quality
Lines of code
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L70
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L109
Vulnerability details
Impact
Due to a miscalculation in
LRTOracle#getRSETHPrice()
, users who callLRTDepositPool#depositAsset()
whenrsETH.totalSupply()
is non-zero will receive fewerrsETH
tokens than they should due to a rounding error. This can be exploited by a malicious first depositor, who can deposit 1 wei (frontrunning if necessary), receive 1 share and therefore cause all subsequent deposits to mint 0 shares, despite theasset
tokens being transferred to the contract. Because this results in the first depositor holding 100% of the outstanding shares (1 out of 1) no matter how many more deposits occur, the attacker is entitled to the funds of those future depositors and has essentially stolen their assets.It is worth noting that even if the first depositor is not malicious, and they deposit a reasonable amount of
asset
tokens, they would still be unintentionally stealing value from future depositors as the rounding error would still be present. In other words, a loss of funds for every depositor (except whoever deposits first) is guaranteed.Proof of Concept
Explanation
In
LRTDepositPool#depositAsset()
, theasset
tokens are transferred to the contract, and then_mintRsETH()
is called to mint shares tomsg.sender
, which usesgetRsETHAmountToMint
to determine how many shares to mint. To perform the calculation, the prices ofasset
andrsETH
are used, which are fetched byLRTOracle#getAssetPrice()
andLRTOracle#getRSETHPrice()
.The issue here is that the output of
getRSETHPrice()
depends on the balance ofrsETH
tokens inLRTDepositPool
(as long asrsEthSupply
is non-zero), and this calculation occurs after the usersasset
tokens for the current mint have been transferred. This means that thersETH
price will be overinflated, and since it is used as the denominator in the calculation ofrsethAmountToMint
, this results in fewer tokens minted to the depositor.If the initial depositor deposits a small enough amount, say 1 wei, then the
rsETH
price for their mint will be1 ether
, becausersEthSupply == 0
and so theif
statement is executed and they receive 1 share. WithrsETH.totalSupply()
now equal to 1, any future deposits will causegetRSETHPrice()
to return a massively inflated value, leading togetRsETHAmountToMint()
returning 0 due to a rounding error.Coded PoC
To run the PoC, we must make some alterations to LRTDepositPoolTest.t.sol, as it currently uses
LRTOracleMock
defined at the top of the file, instead ofLRTOracle
. First, manually alter the code forLRTOracleMock
so thatgetRSETHPrice
contains the actual code fromLRTOracle
(simply copy and paste it in). Then, to ensure it works properly, also make sure to importLRTConfig
and define theIRSETH
interface.Now the PoC can be run using the logic of the
LRTOracle
contract. The below test function shows that the first depositor (alice) can deposit 1 wei to ensure that any subsequent depositors receive 0 shares regardless of theirdepositAmount
, and despite theirasset
tokens being transferred to the deposit pool contract.Alternatively, simply place the test file found here into the test folder. The file has three test functions:
Tools Used
Foundry
Recommended Mitigation Steps
Calculate the amount of rsETH to mint before transferring the asset tokens from the user to the contract, so that the price of
rsETH
is calculated correctly and each depositor receives a fair amount of shares. Note that there is no concern for reentrancy due to this change, asnonReentrant
modifiers are used throughout the protocol andrsETH
is a trusted token with no mint/transfer hooks.Assessed type
Token-Transfer
The text was updated successfully, but these errors were encountered: