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: 66/178
Findings: 5
Award: $126.72
🌟 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/staking/StakingRewards.sol#L67
The main impact of this vulnerability is that liquidators will not be able to liquidate the positions. And as time passes on, this bad debt will ultimately make the collaterals insolvent. This will affect the protocol, as it will lose funds.
In CollateralAndLiquidity
contract, when a user deposits collateral through depositCollateralAndIncreaseShare
, he can borrow USDS. Let's assume that the user has borrowed X amt of USDS which is maximum amount he can borrow against his deposited collateral.
One thing to note here is that when depositCollateralAndIncreaseShare
is called, it will trigger a cooldown period (currently 1 hour). This is a security mechanism to prevent reward hunting. Now the user can't increase or decrease his shares for 1 hour.
Now say the market has crashed, and the price of WETH and WBTC has gone down. Due to this, the user's position has also become liquidateble. So, a liquidator calls liquidateUser
to liquidate the user's position. This will remove the liquidity and decrease the user's share.
But the user can prevent his position from getting liquidated, by frontrunning the liquidateUser
function call and depositing 101 wei of WETH and WBTC using the depositCollateralAndIncreaseShare
function first. Now as the depositCollateralAndIncreaseShare
function will get executed first because of front-running, so this will trigger the cooldown period of 1 hour. Now after this transaction, the liquidateUser
function will get executed, but it will revert because of the cooldown period. That is because, the liquidateUser
function calls _decreaseUserShare
function which will check if block.timestamp >= user.cooldownExpiration
, and as this will be false, so the transaction will revert.
Now the user can continue preventing his position from liquidation, by front-running liquidators, or he can submit 101 wei after every stakingConfig.modificationCooldown()
time, so that the cooldown period never expires.
In the meantime, the price of WETH and WBTC may go further down, which may make the position insolvent. This means that the protocol will surely lose funds.
A test case is presented below, representing how the functions would be executed when front-run. It is a representation , i.e I have not showcased how to do the front-run by analysing mempool , and have simply shown the order of execution of the functions when front-runned.
function testcounter() external { // charlie is a random user depositing collateral vm.startPrank( charlie ); collateralAndLiquidity.depositCollateralAndIncreaseShare(wbtc.balanceOf(charlie), weth.balanceOf(charlie), 0, block.timestamp, false ); vm.stopPrank(); // alice is the attacker account vm.startPrank( alice ); collateralAndLiquidity.depositCollateralAndIncreaseShare(wbtc.balanceOf(alice)/3, weth.balanceOf(alice)/3, 0, block.timestamp, false ); uint256 maxUSDS = collateralAndLiquidity.maxBorrowableUSDS(alice); collateralAndLiquidity.borrowUSDS( maxUSDS); vm.stopPrank(); // Market crashes _crashCollateralPrice(); vm.warp( block.timestamp + 1 days ); // Alice frontruns and deposits 101 wei before the liquidator tries to liquidate his position vm.startPrank(alice); bool x = collateralAndLiquidity.canUserBeLiquidated(alice); console.log(x); collateralAndLiquidity.depositCollateralAndIncreaseShare(101, 101, 0, block.timestamp, false ); vm.stopPrank(); // this will revert because of the cooldown revert vm.startPrank(charlie); collateralAndLiquidity.liquidateUser(alice); vm.stopPrank(); }
Manula review
Cooldown period should not affect liquidation process. Make exception.
Timing
#0 - c4-judge
2024-02-02T11:51:49Z
Picodes marked the issue as duplicate of #312
#1 - c4-judge
2024-02-17T18:50:12Z
Picodes marked the issue as satisfactory
16.3165 USDC - $16.32
https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L128
In CollateralAndLiquidity
contract, the function repayUSDS
and liquidateUser
can be DOSed by a motivated attacker, by repeatedly calling borrowUSDS
and then repayUSDS
functions.
Consider a motivated attacker has deposited WBTC/WETH liquidity, and this allowed him to borrow X amount of USDS. So he call borrowUSDS
with X amt which gives him X USDS tokens. He then repays the X amt of USDS by calling repayUSDS
.
This increases the usdsThatShouldBeBurned
variable (in liquidizer contract) by X amt, due to #L128 in repayUSDS
.
A motivated attacker can now call borrowUSDS
and repayUSDS
, until the value of usdsThatShouldBeBurned
is about to cross type(uint256).max
.
So now when a legit user will try to call repayUSDS
, it will try to increment usdsThatShouldBeBurned
variable , which will revert because of overflow. Hence the repay functionality would be DOSed.
Similarly liquidateUser
function will also be DOSed due to #L181
Given below a test case where it is shown how to call borrowUSDS and repayUSDS. The below test case only calls these functions 1000 times, but to cause the overflow , a motivated attacker needs to call the below test case multiple times in batches.
function testcounter() external { vm.startPrank( alice ); collateralAndLiquidity.depositCollateralAndIncreaseShare(wbtc.balanceOf(alice), weth.balanceOf(alice), 0, block.timestamp, true ); uint count1 = liquidizer.usdsThatShouldBeBurned(); console.log(count1); for(uint i = 0;i<1000;i++) { uint256 maxUSDS = collateralAndLiquidity.maxBorrowableUSDS(alice); collateralAndLiquidity.borrowUSDS( maxUSDS ); collateralAndLiquidity.repayUSDS(maxUSDS); } uint count = liquidizer.usdsThatShouldBeBurned(); console.log(count); vm.stopPrank(); }
Manual review
In line : https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L125
send the usds tokens to address(liquidizer)
instead of address(usds)
DoS
#0 - c4-judge
2024-02-02T16:44:52Z
Picodes marked the issue as duplicate of #618
#1 - c4-judge
2024-02-17T18:39:27Z
Picodes marked the issue as satisfactory
53.4926 USDC - $53.49
https://github.com/code-423n4/2024-01-salty/blob/main/src/pools/Pools.sol#L187
A user's position will not be liquidated for the 1st depositor, because of the revert statement in #L187 of Pools
contract.
Consider that the protocol has been deployed recently.
UserA being its 1st user to deposit collateral to pool collateralPoolID
.
Now userA borrows USDS.
Now consider that price of WETH/WBTC crashes, and user's position is liquitable.
A liquidator when tries to liquidate the position, he will not be able to do so, because of the revert statement in #L187 of Pools
contract.
As the reserves for token A and token B for pool with ID collateralPoolID
is equal to the collateral deposited by the depositor, hence removing the collaterals would trigger the revert statement.
This will prevent liquidator to liquidate the position of 1st depositor, untill no other collaterals are deposited.
function testliquidation() external { vm.startPrank( alice ); collateralAndLiquidity.depositCollateralAndIncreaseShare(wbtc.balanceOf(alice), weth.balanceOf(alice), 0, block.timestamp, true ); uint256 maxUSDS = collateralAndLiquidity.maxBorrowableUSDS(alice); collateralAndLiquidity.borrowUSDS( maxUSDS ); _crashCollateralPrice(); vm.warp( block.timestamp + 1 days ); vm.prank(bob); collateralAndLiquidity.liquidateUser(alice); vm.stopPrank(); }
Manual review
Handle the edge case for the 1st collateral depositor
Error
#0 - c4-judge
2024-02-01T23:12:09Z
Picodes marked the issue as duplicate of #459
#1 - c4-judge
2024-02-21T08:13:43Z
Picodes marked the issue as satisfactory
44.44 USDC - $44.44
https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L151
An attacker can grief a liquidator by front-running liquidateUser
function. This exposes the liquidator to unnecessary slippage.
In function liquidateUser
, in #L151, while removing liquidity of the liquidated user, the minimum reclaimable tokenA and tokenB are set to 0. This ultimately means that the liquidator is open to 100% slippage tolerance.
This allows an attacker to front-run liquidateUser
function , resulting in lesser reclaimed token A and B , which in-turn gives less rewards to the liquidator.
Thus the missing slippage results in loss for liquidator(receives less reward) and also for protocol(reclaims less tokenA and tokenB)
Consider the situation where :
liquidateUser
to liquidate a user's positionreclaimedA
or reclaimedB
would be lowest.Manual review
Take the minimum reclaimable tokenA and tokenB parameters as arguments, and ask the liquidator to fill it.
MEV
#0 - c4-judge
2024-02-01T23:03:20Z
Picodes marked the issue as duplicate of #384
#1 - c4-judge
2024-02-21T15:37:23Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2024-02-21T15:40:21Z
Picodes marked the issue as duplicate of #224
🌟 Selected for report: juancito
Also found by: 0x3b, 0xBinChook, 0xCiphky, 0xHelium, 0xMango, 0xOmer, 0xRobocop, 0xSmartContractSamurai, 0xWaitress, 0xbepresent, 0xpiken, 7ashraf, Arz, Audinarey, Banditx0x, Bauchibred, Draiakoo, IceBear, J4X, Jorgect, Kaysoft, KingNFT, Rhaydden, Rolezn, The-Seraphs, Tigerfrake, Topmark, Tripathi, Udsen, ZanyBonzy, a3yip6, b0g0, chaduke, codeslide, csanuragjain, dharma09, djxploit, eta, ether_sky, grearlake, inzinko, jasonxiale, jesjupyter, josephdara, lanrebayode77, linmiaomiao, lsaudit, niroh, oakcobalt, peanuts, pina, sivanesh_808, slvDev, t0x1c, vnavascues, wangxx2026
11.6897 USDC - $11.69
https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L172
WBTC has pause functions, which allows it to be paused. This will prevent transferring of WBTC, which will affect the liquidation process and liquidity withdrawal process.
Furthermore, as salt.io doesn't have any pause functionality, so even if WBTC is paused , the protocol would be live.
This would affect user's who would want to withdraw WBTC collateral and for protocol, this would prevent the protocol from liquidating any position because of the transfer statement in #L172
Consider the below situation where the liquidator will not be able to liquidate a position: User deposited WETH/WBTC collateral User borrowed X USDS against this collateral Market crashed, and user's position got below liquidation ratio Liquidator sees this and tried to liquidate the position, but got reverted because of the wbtc transfer statement in #L172, that will fail (as wbtc is paused)
Hence liquidator will not be able to liquidate positions
Manual review
Add a functionality to pause the protocol by the DAO
DoS
#0 - c4-judge
2024-02-03T15:00:41Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2024-02-08T08:14:46Z
othernet-global (sponsor) acknowledged
#2 - Picodes
2024-02-20T15:50:49Z
In the absence of credible mitigation and past example of this happening, I'll downgrade this to low
#3 - c4-judge
2024-02-20T15:50:54Z
Picodes changed the severity to QA (Quality Assurance)
#4 - c4-judge
2024-02-21T17:11:08Z
Picodes marked the issue as grade-c
#5 - c4-judge
2024-02-21T17:11:12Z
Picodes marked the issue as grade-b