Open c4-submissions opened 9 months ago
@toshiSat I think we cansolve this by burning the tokens in requestWithdraw
0xleastwood marked the issue as duplicate of #59
0xleastwood marked the issue as not a duplicate
0xleastwood marked the issue as primary issue
0xleastwood marked the issue as selected for report
elmutt (sponsor) confirmed
Lines of code
https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L133-L141
Vulnerability details
Bug Description
In
AfEth.sol
, theprice()
function returns the current price of afEth:AfEth.sol#L133-L141
As seen from above, the price of afEth is calculated by the TVL of both safEth and vAfEth divided by
totalSupply()
. However, this calculation does not take into account afEth that is transferred to the contract whenrequestWithdraw()
is called:AfEth.sol#L183-L187
When a user calls
requestWithdraw()
to initiate a withdrawal, his afEth is transferred to theAfEth
contract as shown above. Afterwards, an amount of vAfEth proportional to his withdrawal amount is burned, andpendingSafEthWithdraws
is increased.When
price()
is called afterwards,safEthBalanceMinusPending()
andvEthStrategy.balanceOf(address(this))
will be decreased. However, since the user's afEth is only transferred and not burnt,totalSupply()
remains the same. This causes the value returned byprice()
to be lower than what it should be, sincetotalSupply()
is larger than the actual circulating supply of afEth.This is an issue as
deposit()
relies onprice()
to determine how much afEth to mint to a depositor:AfEth.sol#L166-L168
Where:
totalValue
is the ETH value of the caller's deposit.priceBeforeDeposit
is the cached value ofprice()
.If anyone has initiated a withdrawal using
requestWithdraw()
but hasn't calledwithdraw()
to withdraw his funds,price()
will be lower than what it should be. Subsequently, whendeposit()
is called, the depositor will receive more afEth than he should sincepriceBeforeDeposit
is smaller.Furthermore, a first depositor can call
requestWithdraw()
with all his afEth immediately after staking to makeprice()
return 0, thereby permanently DOSing all future deposits asdeposit()
will always revert with a division by zero error.Impact
When there are pending withdrawals,
price()
will return a value smaller than its actual value. This causes depositors to receive more afEth than intended when callingdeposit()
, resulting in a loss of funds for previous depositors.Additionally, a first depositor can abuse this to force
deposit()
to always revert, permanently bricking the protocol forever.Proof of Concept
Assume that the protocol is newly deployed and Alice is the only depositor.
totalSupply()
.Alice calls
requestWithdraw()
with_amount
as all her afEth:_amount == totalSupply()
,withdrawRatio
is1e18
(100%).pendingSafEthWithdraws
is increased to the protocol's safEth balance.Bob calls
deposit()
to deposit some ETH into the protocol:price()
is called:pendingSafEthWithdraws
is equal to the protocol's safEth balance,safEthBalanceMinusPending()
is 0, thereforesafEthValueInEth
is also 0.vEthStrategy.balanceOf(address(this))
(the protocol's vAfEth balance) is 0,vEthValueInEth
is also 0.totalSupply()
is non-zero.price()
returns 0 as:priceBeforeDeposit
is 0, this line reverts with a division by zero error.As demonstrated above,
deposit()
will always revert as long as Alice does not callwithdraw()
to burn her afEth, thereby bricking the protocol's core functionality.Recommended Mitigation
In
price()
, consider subtracting the amount of afEth held in the contract fromtotalSupply()
:AfEth.sol#L133-L141
Assessed type
Other