Platform: Code4rena
Start Date: 21/02/2024
Pot Size: $200,000 USDC
Total HM: 22
Participants: 36
Period: 19 days
Judge: Trust
Total Solo HM: 12
Id: 330
League: ETH
Rank: 17/36
Findings: 2
Award: $1,128.18
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: WoolCentaur
Also found by: AM, SBSecurity
1128.1754 USDC - $1,128.18
https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseCore.sol#L493-L499 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseLending.sol#L229-L232
It is stated in the documentation of the contest from the c4 site that liquidations are rounded against the liquidator when it will evaluate the amount of tokens they need to payback related to the borrowshares they payoff. This becomes a problem when the amount that will be liquidated will be bigger then the amount of tokens that are available in the pool, when this happens the liquidation process logic will get in the function _withdrawOrAllocateSharesLiquidation which it's role is to cashout all the possible tokens from the pool and if that amount is not enough it will also give some shares to the liquidator. Now, the problem comes from the fact that totalPoolInShares will be rounded down because variable _maxSharePrice is selected as false, while totalPoolToken is not rounded down and represents the actual(real) amount of tokens that are in the pool. After that when the function _coreWithdrawBare is called and those 2 values will be used to update pseudoTotalPool and totalDepositShares ( using functions _decreasePseudoTotalPool and _decreaseTotalDepositShares ) the variables pseudoTotalPool and totalDepositShares will not be synced anymored as one is updated with a rounded down value and the other one with a real value. The variable totalDepositShares will be bigger then pseudoTotalPool and this becomes a problem in _syncPoolAfterCodeExecution because it will determine that lendingSharePriceAfter is smaller then lendingSharePriceBefore breaking the invariant and reverting the liquidation transaction.
Any liquidation over a pool that does not have enough tokens in it's balance to to pay the liquidator will be blocked
Run the test from below using command
forge test -vvvv --match-test "test_liq__create_shares_for_liquidator_correct_price_feeds_price_drop_20_perc" --fork-url mainnet
function test_liq__create_shares_for_liquidator_correct_price_feeds_price_drop_20_perc() public { _useBlock(NEW_BLOCK); _deployNewWiseLending({_mainnetFork: true}); //This is the bug, for this tests to work properly the _maxSharePrice of calculateLendingShares in _withdrawOrAllocateSharesLiquidation needs to be on false //Carol initialization address carol = vm.addr(0x17896); //Bob initialization uint256 bobPositionId; uint256 bobDepositSharesAfterFirstDeposit; address bob = vm.addr(0x45678); //Alice initialization uint256 alicePositionId; uint256 alicesDepositSharesAfterFirstDeposit; uint256 aliceBorrowSharesAfterFirstBorrow; address alice = vm.addr(0x12345); vm.deal(alice, 200 ether); vm.deal(bob, 200 ether); vm.deal(carol, 200 ether); deal(USDC_ADDRESS, alice, 20000e6); vm.warp(block.timestamp+1); ( , int256 goodAnswer, , , ) = IPriceFeed(USDC_ETH_PRICE_FEED).latestRoundData(); ( uint80 roundId, , , , ) = IPriceFeed(USDC_USD_PRICE_FEED).latestRoundData(); vm.mockCall( USDC_USD_PRICE_FEED, abi.encodeWithSelector(IPriceFeed(USDC_USD_PRICE_FEED).latestRoundData.selector), abi.encode(roundId,goodAnswer,0,0,0) ); vm.mockCall( USDC_USD_PRICE_FEED, abi.encodeWithSelector(IPriceFeed(USDC_USD_PRICE_FEED).decimals.selector), abi.encode(18) ); //Bob operations vm.startPrank(bob); bobPositionId = POSITION_NFTS_INSTANCE.mintPosition(); bobDepositSharesAfterFirstDeposit = LENDING_INSTANCE.depositExactAmountETH{value: 10 ether}(bobPositionId); vm.stopPrank(); console.log("Bob deposit shares: ", bobDepositSharesAfterFirstDeposit ); //Alice operations vm.startPrank(alice); IERC20(USDC_ADDRESS).approve(address(LENDING_INSTANCE), 20000e6); alicePositionId = POSITION_NFTS_INSTANCE.mintPosition(); alicesDepositSharesAfterFirstDeposit = LENDING_INSTANCE.depositExactAmount(alicePositionId, USDC_ADDRESS, 20000e6); aliceBorrowSharesAfterFirstBorrow = LENDING_INSTANCE.borrowExactAmountETH(alicePositionId, 4.1 ether); vm.stopPrank(); //Carol operations vm.startPrank(carol); uint256 carolPositionId = POSITION_NFTS_INSTANCE.mintPosition(); LENDING_INSTANCE.depositExactAmountETH{value: 10 ether}(carolPositionId); LENDING_INSTANCE.borrowExactAmount(carolPositionId, USDC_ADDRESS, 4000e6); vm.stopPrank(); vm.mockCall( USDC_USD_PRICE_FEED, abi.encodeWithSelector(IPriceFeed(USDC_USD_PRICE_FEED).latestRoundData.selector), abi.encode(roundId,goodAnswer/10000*8100,0,0,0) ); deal(WETH_ADDRESS, bob, 200e18); vm.startPrank(bob); IERC20(WETH_ADDRESS).approve(address(LENDING_INSTANCE), type(uint256).max); LENDING_INSTANCE.liquidatePartiallyFromTokens( alicePositionId, bobPositionId, WETH_ADDRESS, USDC_ADDRESS, aliceBorrowSharesAfterFirstBorrow ); vm.stopPrank(); uint256 numbersOfBobPositions = LENDING_INSTANCE.getPositionLendingTokenLength(bobPositionId); console.log("Alice deposit shares: ", alicesDepositSharesAfterFirstDeposit ); console.log("Alice borrow shares: ", aliceBorrowSharesAfterFirstBorrow ); console.log("Alice ether balance: ", address(alice).balance); console.log("Bob USDC balance: ", IERC20(USDC_ADDRESS).balanceOf(bob)); console.log("Bob lending positions: ", numbersOfBobPositions); }
The output of the test
├─ [0] console::log("lendSharePriceBefore: ", 1000000000049999997 [1e18]) [staticcall] │ │ └─ ← () │ ├─ [0] console::log("lendSharePriceAfter: ", 999999999750000062 [9.999e17]) [staticcall] │ │ └─ ← () │ └─ ← InvalidAction() └─ ← InvalidAction() Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.85s (1.77s CPU time) Ran 1 test suite in 4.85s (2.85s CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in contracts/FindingsTest.t.sol:FindingsTest [FAIL. Reason: InvalidAction()] test_liq__create_shares_for_liquidator_correct_price_feeds_price_drop_20_perc() (gas: 34530857)
We can clearly observe that lendSharePriceAfter is smaller then lendSharePriceBefore and this is why the transaction is reverted with the error InvalidAction.
Manual Review
When you are calculating the value of totalPoolInShares round up the value turning _maxSharePrice to true.
uint256 totalPoolInShares = calculateLendingShares( { _poolToken: _poolToken, _amount: totalPoolToken, _maxSharePrice: true } );
DoS
#0 - vm06007
2024-03-17T08:16:41Z
Any liquidation over a pool that does not have enough tokens in it's balance to to pay the liquidator will be blocked
that seems to be like a desired functionality by design and expected behavior. @vonMangoldt can confirm
#1 - c4-pre-sort
2024-03-17T14:27:21Z
GalloDaSballo marked the issue as primary issue
#2 - vonMangoldt
2024-03-18T13:53:55Z
Any liquidation over a pool that does not have enough tokens in it's balance to to pay the liquidator will be blocked
that seems to be like a desired functionality by design and expected behavior. @vonMangoldt can confirm
no i think hes right from my first looking into it. Just curious why it didnt dos in our javascript tests. Probably the percentage roundings etc need to be aligned for that behaviour
#3 - c4-pre-sort
2024-03-18T16:25:52Z
GalloDaSballo marked the issue as sufficient quality report
#4 - vonMangoldt
2024-03-20T12:52:36Z
I would suggest downgrading to medium since as a liquidator you can always find a rounding where it still works which is annoying but it means the general funcitonality still works and no user funds at risk etc. Still a good find 👍 (For an example take a look at javascript test extra functionality2 : it("Liquidator get right amount if shares when pool has not enough token") @GalloDaSballo
#5 - vm06007
2024-03-20T14:47:18Z
@GalloDaSballo please mark this as disagree with severity
(add label)
#6 - c4-judge
2024-03-26T16:32:03Z
trust1995 marked issue #116 as primary and marked this issue as a duplicate of 116
#7 - c4-judge
2024-03-26T19:00:09Z
trust1995 marked the issue as satisfactory
#8 - c4-judge
2024-03-26T19:10:33Z
trust1995 changed the severity to 2 (Med Risk)