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: 76/178
Findings: 2
Award: $90.94
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: falconhoof
Also found by: BiasedMerc, J4X, Rhaydden, cats, forgebyola, inzinko, jesjupyter, josephdara, zhaojie
79.2483 USDC - $79.25
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L130-L152 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L385-L400 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L162-L178
The protocol implements a system of maxPendingTokensForWhitelisting
which limits the number of tokens that can be pending at once, the default proposed number is 5, but it can go as low as 3 and as 12, this means when the Proposals::proposeTokenWhitelisting
is called it checks if that the open ballots must be less than the maxPendingTokensForWhitelisting
, before a new proposal can be allowed, and a ballot formed for it, the issue is that the way the protocol works is that couple of tokens can be proposed for whitelisting and users will vote on them with their voting power, but after the duration of the ballots, the ballots that did not reach the quorum will not be able to finalize, due to the requirements to finalize the ballots, hence the _openBallotsForTokenWhitelisting
will still contain those ballot that did not finalize, and at a point the check inside the Proposals::proposeTokenWhitelisting
will be unable to pass, causing a DOS of that function
A detailed concept of how the DOS will be achieved will be listed below
DAO::finalizeBallot
is called on the first token that has enough quorum to pass after the ballot end time and it finalizes which removes it from pending whitelisting tokens as shown belowfunction markBallotAsFinalized( uint256 ballotID ) external nonReentrant { require( msg.sender == address(exchangeConfig.dao()), "Only the DAO can mark a ballot as finalized" ); Ballot storage ballot = ballots[ballotID]; // Remove finalized whitelist token ballots from the list of open whitelisting proposals if ( ballot.ballotType == BallotType.WHITELIST_TOKEN ) _openBallotsForTokenWhitelisting.remove( ballotID ); ...more code
DAO::finalizeBallot
is called it also calls Proposals::canFinalizeBallot
to check if truly the requirements to finalize has been met as shown belowfunction canFinalizeBallot( uint256 ballotID ) external view returns (bool) { Ballot memory ballot = ballots[ballotID]; if ( ! ballot.ballotIsLive ) return false; // Check that the minimum duration has passed if (block.timestamp < ballot.ballotMinimumEndTime ) return false; // Check that the required quorum has been reached if ( totalVotesCastForBallot(ballotID) < requiredQuorumForBallotType( ballot.ballotType )) return false; //@audit Here we Can see Ballot That Does not Reach Quorum for their ballot type will get returned false, rendering finalization impossible return true; }
Then to reach the DOS point we continue
function proposeTokenWhitelisting( IERC20 token, string calldata tokenIconURL, string calldata description ) external nonReentrant returns (uint256 _ballotID) { require( address(token) != address(0), "token cannot be address(0)" ); require( token.totalSupply() < type(uint112).max, "Token supply cannot exceed uint112.max" ); // 5 quadrillion max supply with 18 decimals of precision require( _openBallotsForTokenWhitelisting.length() < daoConfig.maxPendingTokensForWhitelisting(), "The maximum number of token whitelisting proposals are already pending" ); // @audit This Check Will not pass as the 5 Tokens is Not less than 5 required space ...more code
The DOS point is now achieved as New Tokens cant be proposed
Manual Review
_openBallotsForTokenWhitelisting
arrayDoS
#0 - c4-judge
2024-02-02T09:29:53Z
Picodes marked the issue as duplicate of #991
#1 - c4-judge
2024-02-14T07:54:31Z
Picodes marked the issue as satisfactory
🌟 Selected for report: falconhoof
Also found by: BiasedMerc, J4X, Rhaydden, cats, forgebyola, inzinko, jesjupyter, josephdara, zhaojie
79.2483 USDC - $79.25
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/PoolsConfig.sol#L44-L60 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/PoolsConfig.sol#L77-L91 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L162-L177 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/PoolsConfig.sol#L113-L116
The Protocol implements a maximum number of pools that can be whitelisted, which means that if the size of the maximum number is changed and reduced there is a check in the PoolsConfig::whitelistPool
that checks that if the current whitelisted pools are actually less than the maximum, but in the function PoolsConfig::changeMaximumWhitelistedPool
it doesn't actually effect that check before reducing the number of maximum of tokens, if both are not tracked effectly before the change is made a DOS is possible for at least 10/11 Days as that what it will take for the Parameter change Ballot.
A simple and straight forward way the DOS is Possible Will be presented below
function changeMaximumWhitelistedPools(bool increase) external onlyOwner { if (increase) { if (maximumWhitelistedPools < 100) maximumWhitelistedPools += 10; } else { if (maximumWhitelistedPools > 20) maximumWhitelistedPools -= 10; } emit MaximumWhitelistedPoolsChanged(maximumWhitelistedPools); }
PoolsConfig::whitelistPool
the next time it is called as shown belowfunction whitelistPool( IPools pools, IERC20 tokenA, IERC20 tokenB ) external onlyOwner { require( _whitelist.length() < maximumWhitelistedPools, "Maximum number of whitelisted pools already reached" ); //@audit This will not pass the check resulting in a DOS ...more code }
Proposals::proposeTokenWhitelisting
as shown belowfunction proposeTokenWhitelisting( IERC20 token, string calldata tokenIconURL, string calldata description ) external nonReentrant returns (uint256 _ballotID) { require( address(token) != address(0), "token cannot be address(0)" ); require( token.totalSupply() < type(uint112).max, "Token supply cannot exceed uint112.max" ); // 5 quadrillion max supply with 18 decimals of precision require( _openBallotsForTokenWhitelisting.length() < daoConfig.maxPendingTokensForWhitelisting(), "The maximum number of token whitelisting proposals are already pending" ); //@audit This will also not pass, as it is the exact same kind of check as the previous check and it will result in a DOS ...more code }
function numberOfWhitelistedPools() external view returns (uint256) { return _whitelist.length(); }
NOTE: Low Probability this happens but High impact as a 10 days DOS happens Stopping any pending Whitelist impossible to complete and also new token proposals ballot wont go through.
Manual Review
changeMaximumWhitelistedPools
function a check shold be implemented that the reduction that is about to take place will not allow this kind of miscalculationsDoS
#0 - c4-judge
2024-02-02T09:29:44Z
Picodes marked the issue as duplicate of #991
#1 - c4-judge
2024-02-14T07:54:26Z
Picodes marked the issue as satisfactory
🌟 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
If the Ballot end time has been completed, users are not supposed to be able to cast vote again, but the cast vote does not check that the ballot end time has reached
function castVote( uint256 ballotID, Vote vote ) external nonReentrant { Ballot memory ballot = ballots[ballotID]; // Require that the ballot is actually live require( ballot.ballotIsLive, "The specified ballot is not open for voting" ); //@audit This check here that asks if the ballot is live or not does not prove that the ballot end time has been completed, because that value is only updated if the ballot has been finalized ...more code }
Mitigation Implement a check to see if The ballot duration has ended
In the Docs of the project, it says that 45% of SALT rewards must be burnt, but this value is different in the code
the docs as shown below
It generates yield for the DAO in the form of SALT. By default 45% of this generated SALT is burned.
https://docs.salty.io/decentralization/protocol-owned-liquidity
But in the code of the DAOConfig
The Value is 50%
as shown below
// For rewards distributed to the DAO, the percentage of SALT that is burned with the remaining staying in the DAO for later use. // Range: 25% to 75% with an adjustment of 5% uint256 public percentPolRewardsBurned = 50;
In the Pools::deposit
users can deposit unwhitelisted tokens because the function does not check if the tokens that are about to be sent are whitelisted tokens, it just allow them to deposit them as shown below
function deposit( IERC20 token, uint256 amount ) external nonReentrant { require( amount > PoolUtils.DUST, "Deposit amount too small"); _userDeposits[msg.sender][token] += amount; // Transfer the tokens from the sender - only tokens without fees should be whitelsited on the DEX token.safeTransferFrom(msg.sender, address(this), amount ); //@audit No check for Non-Whitelisted token emit TokenDeposit(msg.sender, token, amount); }
This Goes against the rules of the protocol, as only whitelisted tokens should be present in the Pool and any other contract
#0 - c4-judge
2024-02-03T13:15:38Z
Picodes marked the issue as grade-b