Closed sherlock-admin2 closed 1 year ago
Sponsor Disputed
Pool::burnRTokens()
is called. At step 5, if Bob calls to BasePositionManager::syncFeeGrowth()
, he still receives 10 rToken and still manages to call BasePositionManager::burnRTokens()
to burn to get fees.Escalate
The part that the rTokens
don't get minted to the PositionManager is correct, they are in fact minted to the Pool because the call to _syncFeeGrowth
passes the to value in _mint
as `address(this):
however the mint itself will still occur in the given scenario because the calculation for rMintQty will be > 0:
The additional 10 rTokens would then be in the Pool contract which as stated the user would try to redeem by first calling BasePositionManager::syncFeeGrowth
which makes a call to Pool::tweakPosZeroLiq
:
this calls _syncFeeGrowth
again, however because the initial call had the updateReinvestLLast
boolean set to false
the tokens are again minted to the Pool, increasing the supply of rTokens
in the Pool once again with rTokens
that have already been accounted for:
for the user to be able to redeem their rTokens
the BasePositionManager::syncFeeGrowth
then tries to update the value of pos.rTokenOwed
in the call to _updateRTokenOwedAndFeeGrowth
:
however, given that the feeGrowth hasn't changed the logic that increments pos.rTokenOwed
is skipped:
and therefore the original issue still holds as the pos.rTokenOwed = 0
from the initial call to BasePositionManager::burnRTokens
which triggers the require statement on L255 to revert:
Escalate
The part that the
rTokens
don't get minted to the PositionManager is correct, they are in fact minted to the Pool because the call to_syncFeeGrowth
passes the to value in_mint
as `address(this):however the mint itself will still occur in the given scenario because the calculation for rMintQty will be > 0:
The additional 10 rTokens would then be in the Pool contract which as stated the user would try to redeem by first calling
BasePositionManager::syncFeeGrowth
which makes a call toPool::tweakPosZeroLiq
:this calls
_syncFeeGrowth
again, however because the initial call had theupdateReinvestLLast
boolean set tofalse
the tokens are again minted to the Pool, increasing the supply ofrTokens
in the Pool once again withrTokens
that have already been accounted for:for the user to be able to redeem their
rTokens
theBasePositionManager::syncFeeGrowth
then tries to update the value ofpos.rTokenOwed
in the call to_updateRTokenOwedAndFeeGrowth
:however, given that the feeGrowth hasn't changed the logic that increments
pos.rTokenOwed
is skipped:and therefore the original issue still holds as the
pos.rTokenOwed = 0
from the initial call toBasePositionManager::burnRTokens
which triggers the require statement on L255 to revert:
The escalation could not be created because you are not exceeding the escalation threshold.
You can view the required number of additional valid issues/judging contest payouts in your Profile page, in the Sherlock webapp.
Escalate
The part that the rTokens
don't get minted to the PositionManager is correct, they are in fact minted to the Pool because the call to _syncFeeGrowth
passes the to value in _mint
as `address(this):
however the mint itself will still occur in the given scenario because the calculation for rMintQty will be > 0:
The additional 10 rTokens would then be in the Pool contract which as stated the user would try to redeem by first calling BasePositionManager::syncFeeGrowth
which makes a call to Pool::tweakPosZeroLiq
:
this calls _syncFeeGrowth
again, however because the initial call had the updateReinvestLLast
boolean set to false
the tokens are again minted to the Pool, increasing the supply of rTokens
in the Pool once again with rTokens
that have already been accounted for:
for the user to be able to redeem their rTokens
the BasePositionManager::syncFeeGrowth
then tries to update the value of pos.rTokenOwed
in the call to _updateRTokenOwedAndFeeGrowth
:
however, given that the feeGrowth hasn't changed the logic that increments pos.rTokenOwed
is skipped:
and therefore the original issue still holds as the pos.rTokenOwed = 0
from the initial call to BasePositionManager::burnRTokens
which triggers the require statement on L255 to revert:
Escalate
The part that the
rTokens
don't get minted to the PositionManager is correct, they are in fact minted to the Pool because the call to_syncFeeGrowth
passes the to value in_mint
as `address(this):however the mint itself will still occur in the given scenario because the calculation for rMintQty will be > 0:
The additional 10 rTokens would then be in the Pool contract which as stated the user would try to redeem by first calling
BasePositionManager::syncFeeGrowth
which makes a call toPool::tweakPosZeroLiq
:this calls
_syncFeeGrowth
again, however because the initial call had theupdateReinvestLLast
boolean set tofalse
the tokens are again minted to the Pool, increasing the supply ofrTokens
in the Pool once again withrTokens
that have already been accounted for:for the user to be able to redeem their
rTokens
theBasePositionManager::syncFeeGrowth
then tries to update the value ofpos.rTokenOwed
in the call to_updateRTokenOwedAndFeeGrowth
:however, given that the feeGrowth hasn't changed the logic that increments
pos.rTokenOwed
is skipped:and therefore the original issue still holds as the
pos.rTokenOwed = 0
from the initial call toBasePositionManager::burnRTokens
which triggers the require statement on L255 to revert:
The escalation could not be created because you are not exceeding the escalation threshold.
You can view the required number of additional valid issues/judging contest payouts in your Profile page, in the Sherlock webapp.
This escalation is invalid.
this calls _syncFeeGrowth again, however because the initial call had the updateReinvestLLast boolean set to false the tokens are again minted to the Pool, increasing the supply of rTokens in the Pool once again with rTokens that have already been accounted for:
=> This is invalid
In the burnRTokens
function in Pool
contract: Even though the boolean is set to false (i.e not update reinvestLLast
in the _syncFeeGrowth
function), it is updated in the burnRTokens
right after calling _syncFeeGrowth
(line 279 -> 282): https://github.com/sherlock-audit/2023-07-kyber-swap/blob/e0cb622e7dacf4e8603507f3ea1c1073f9445dbe/ks-elastic-sc/contracts/Pool.sol#L282
Reason: In the burnRTokens
case, the reinvestL
will be updated as burning, thus, it waits for reinvestL
to be updated before updating reinvestLLast
. In other cases, it can update reinvestLLast = reinvestL
in the _syncFeeGrowth
function as reinvestL
is unchanged after that, so boolean is set to be true
So in general, the reinvestLLast
is always updated to the latest reinvestL
after syncing the growth fee. As the reinvestL
and reinvestLLast
are always updated correctly, the _syncFeeGrowth
works as expected.
This causes other steps in the reporter's comment to be invalid.
nican0r
medium
Loss of rTokens owed if a user is owed more rTokens than the Position Manager's balance
Summary
If a user calls
BasePositionManager::burnRTokens
and they're owed more than the Position Manager's current balance they only receive up to the balance of the Pool and have their remaining balance of rTokenOwed wiped out.Vulnerability Detail
In
BasePositionManager::burnRTokens
when therTokenQty > rTokenBalance
the following call toPool::burnRTokens
burns the entire balance of the BasePositionManager:this results in the LP that called the function only receiving
rTokenBalance - rTokenQty
when the burn is complete.PoC:
rTokenQty = 100
BasePositionManager
only hasrTokenBalance = 90
because the other 10 were earned from fees gained through rewards earned on the reinvestedrTokens
and so are only minted when_syncFeeGrowth
is calledBasePositionManager::burnRTokens
, it setspos.rTokenOwed = 0
and callspool.burnRTokens(90)
Pool::burnRTokens
then makes a call to_syncFeeGrowth
which mints the 10 remainingrTokens
but the function was already called with_qty = 90
, so only those are burned for Bob and the remaining 10 stay in BasePositionManagerBasePositionManager::burnRTokens
again he won't receive any reward because he's hadrTokenOwed
for his position reset to 0Impact
LPs lose
rTokenQty - rTokenBalance
whenever they try to burnrTokenQty > rTokenBalance
. They are unable to recover these lostrTokens
and they remain stuck inBasePositionManager
.Code Snippet
BaseTokenManager::burnRTokens
Pool::burnRTokens
Pool::_syncFeeGrowth
Tool used
Manual Review
Recommendation
Add the following conditional statement to ensure pos.rTokenOwed isn't set to 0 in the case where rTokenQty > rTokenBalance:
this will allow the LP to call
BasePositionManager::burnRTokens
again to burn their remainingrTokens
once the fees for reinvested liquidity growth are synced.