When the pool lacks sufficient funds to fully compensate the liquidator, it will reset both totalPool and totalLendingShares to 0, but will increase the lendingShares of the liquidator by the difference.
This enables the liquidator to retrieve it later when the pool liquidity rises once again, either by paybacks or lp deposits. Consequently, when someone deposits into the pool, the liquidator can claim the remaining portion of their liquidation reward, preventing the depositor from withdrawing their full amount. As we can observe this creates a weird scenario when every next liquidity provider tokens are “stolen” from the previous withdrawer.
This is extremely bad for the market because unlike borrowing, where you are sure that these funds will be back into the pool either by repaying or liquidation when the liquidator that has received surplus shares withdraws them it will lead to a domino effect and the last withdrawer for this market will bear all the cost risking up to 100% of his deposits to be stolen.
When pureCollateral of a user isn’t enough for the liquidator to seize, _withdrawOrAllocateSharesLiquidation is executed:
This is not enough for a liquidator to redeem his difference because on withdraw the following mappings should hold enough values:
totalPool - reset to 0 in liquidation
pseudoTotalPool
totalDepositShares - reset to 0 in liquidation
From there we can see in order liquidator to be able to redeem his shares he needs someone else to deposit his funds in the same market, we saw that payback won’t work. Then after totalDepositShares holds enough shares that satisfy the liquidator he can freely withdraw them “stealing” from the original depositor. Then when he wants to withdraw his tokens and exit the protocol it won’t be possible because previous user has redeemed tokens that belonged to him.
Lets consider the example that deposits and withdrawals will be alternating. Then the scenario will continue to happen for each new deposit in the system being stolen from the previous person.
Proof of Concept
Here is a gist containing test that can be placed in test/extraFunctionality2.test.js:
For simplicity all the other tests can be removed, then in the terminal: npm run test-extra2 will execute it.
The main idea is to verify that when such type of liquidation occurs totalPool is zeroed and after Bob deposits in the system liquidator can redeem shares accrued from liquidation using Bob’s assets.
After that when Bob wants to withdraw big part of his deposit WiseCore::_coreWithdrawBare will revert with underflow.
Tools Used
Manual Review
Recommended Mitigation Steps
Finding an appropriate solution is indeed challenging. One approach could be to allow the liquidator to withdraw the remaining funds only when there is a substantial increase in liquidity, such as 5 times or 10 times the amount he has left to withdraw.
Lines of code
https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/WiseCore.sol#L460-L536 https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/MainHelper.sol#L752-L788
Vulnerability details
Impact
When the pool lacks sufficient funds to fully compensate the liquidator, it will reset both
totalPool
andtotalLendingShares
to 0, but will increase thelendingShares
of the liquidator by the difference.This enables the liquidator to retrieve it later when the pool liquidity rises once again, either by
paybacks
orlp deposits
. Consequently, when someone deposits into the pool, the liquidator can claim the remaining portion of their liquidation reward, preventing the depositor from withdrawing their full amount. As we can observe this creates a weird scenario when every next liquidity provider tokens are “stolen” from the previous withdrawer.This is extremely bad for the market because unlike borrowing, where you are sure that these funds will be back into the pool either by repaying or liquidation when the liquidator that has received surplus shares withdraws them it will lead to a domino effect and the last withdrawer for this market will bear all the cost risking up to 100% of his deposits to be stolen.
When
pureCollateral
of a user isn’t enough for the liquidator to seize,_withdrawOrAllocateSharesLiquidation
is executed:WiseCore.sol#L460-L536
When assets of a pool are less than the amount that liquidator receives we can see that both
lendingShares
totalPool
are being reset to 0.
And when borrowers payback, mappings that get increased are:
totalPool
totalBorrowAmount
MainHelper.sol#L752-L788
This is not enough for a liquidator to redeem his difference because on withdraw the following mappings should hold enough values:
totalPool
- reset to 0 in liquidationpseudoTotalPool
totalDepositShares
- reset to 0 in liquidationFrom there we can see in order liquidator to be able to redeem his shares he needs someone else to deposit his funds in the same market, we saw that
payback
won’t work. Then aftertotalDepositShares
holds enough shares that satisfy the liquidator he can freely withdraw them “stealing” from the original depositor. Then when he wants to withdraw his tokens and exit the protocol it won’t be possible because previous user has redeemed tokens that belonged to him. Lets consider the example that deposits and withdrawals will be alternating. Then the scenario will continue to happen for each new deposit in the system being stolen from the previous person.Proof of Concept
Here is a gist containing test that can be placed in
test/extraFunctionality2.test.js
:https://gist.github.com/AydoanB/1c8012f8e078d5b5de8981dee96a929e
For simplicity all the other tests can be removed, then in the terminal:
npm run test-extra2
will execute it.The main idea is to verify that when such type of liquidation occurs
totalPool
is zeroed and after Bob deposits in the system liquidator can redeem shares accrued from liquidation using Bob’s assets.After that when Bob wants to withdraw big part of his deposit
WiseCore::_coreWithdrawBare
will revert with underflow.Tools Used
Manual Review
Recommended Mitigation Steps
Finding an appropriate solution is indeed challenging. One approach could be to allow the liquidator to withdraw the remaining funds only when there is a substantial increase in liquidity, such as 5 times or 10 times the amount he has left to withdraw.
Assessed type
Other