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: 65/178
Findings: 4
Award: $128.64
🌟 Selected for report: 0
🚀 Solo Findings: 0
53.4926 USDC - $53.49
https://github.com/code-423n4/2024-01-salty/blob/main/src/pools/Pools.sol#L187
last liquidity staker fails to remove their liquidity due to DUST checks in removeLiquidity in Pool.
function removeLiquidity( IERC20 tokenA, IERC20 tokenB, uint256 liquidityToRemove, uint256 minReclaimedA, uint256 minReclaimedB, uint256 totalLiquidity ) external nonReentrant returns (uint256 reclaimedA, uint256 reclaimedB) { ... require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve0 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal"); }
NA
Consider to remove this check if the reserve0 and reserve1 are both 0 after deducing the liquidity to unstake.
Context
#0 - c4-judge
2024-02-01T23:13:58Z
Picodes marked the issue as duplicate of #278
#1 - c4-judge
2024-02-21T16:07:16Z
Picodes marked the issue as not a duplicate
#2 - c4-judge
2024-02-21T16:07:20Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2024-02-21T16:07:28Z
Picodes marked the issue as duplicate of #912
🌟 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#L320-L334
quorum of a proposal would change base on the amount of staked salt after the proposal went live. Consider a ballot type has a 10% quorum. Given a totalStake of 1m SALT in the beginning:
Impact: non-deterministic finalizeBallot action, since the quorum becomes a floating variable based on the amount of stake SALT. If oppositely there is a major unstaking happening, it would also lead to an unexpected reduction of quorum to the proposal.
function 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; return true; }
whereas the requiredQuorumForBallotType
function requiredQuorumForBallotType( BallotType ballotType ) public view returns (uint256 requiredQuorum) { // The quorum will be specified as a percentage of the total amount of SALT staked uint256 totalStaked = staking.totalShares( PoolUtils.STAKED_SALT ); require( totalStaked != 0, "SALT staked cannot be zero to determine quorum" ); if ( ballotType == BallotType.PARAMETER ) requiredQuorum = ( 1 * totalStaked * daoConfig.baseBallotQuorumPercentTimes1000()) / ( 100 * 1000 ); else if ( ( ballotType == BallotType.WHITELIST_TOKEN ) || ( ballotType == BallotType.UNWHITELIST_TOKEN ) ) requiredQuorum = ( 2 * totalStaked * daoConfig.baseBallotQuorumPercentTimes1000()) / ( 100 * 1000 ); else // All other ballot types require 3x multiple of the baseQuorum requiredQuorum = ( 3 * totalStaked * daoConfig.baseBallotQuorumPercentTimes1000()) / ( 100 * 1000 ); // Make sure that the requiredQuorum is at least 0.50% of the total SALT supply. // Circulating supply after the first 45 days of emissions will be about 3 million - so this would require about 16% of the circulating // SALT to be staked and voting to pass a proposal (including whitelisting) 45 days after deployment.. uint256 totalSupply = ERC20(address(exchangeConfig.salt())).totalSupply(); uint256 minimumQuorum = totalSupply * 5 / 1000; if ( requiredQuorum < minimumQuorum ) requiredQuorum = minimumQuorum; }
NA
Payable
#0 - c4-judge
2024-02-03T10:11:52Z
Picodes marked the issue as duplicate of #46
#1 - c4-judge
2024-02-21T14:26:42Z
Picodes marked the issue as satisfactory
🌟 Selected for report: juancito
Also found by: 0xCiphky, 0xRobocop, 0xWaitress, DanielArmstrong, J4X, PENGUN, erosjohn, haxatron, klau5, lanrebayode77, pina, twcctop, zhaojie
37.1392 USDC - $37.14
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L102
creating proposal through _possiblyCreateProposal uses a string as the key, which can be frontrun in Ddos.
all proposal creation calls _possiblyCreateProsposal and returns a ballotId when sucessful. Since the proposal is logged by using a string as a key, if the key/string gets created by another proposal, then the ballot creation would fail. Malicious attackers can make use of this and frontrun any honest proposer with a proposal of the same name, but differnt content in terms of parameters or ballot type.
While the attaker is required to have staked Salt to propose, they can repeatedly ddos proposer once they have the required amount of staked tokens.
function _possiblyCreateProposal( string memory ballotName, BallotType ballotType, address address1, uint256 number1, string memory string1, string memory string2 ) internal returns (uint256 ballotID) { require( block.timestamp >= firstPossibleProposalTimestamp, "Cannot propose ballots within the first 45 days of deployment" ); // The DAO can create confirmation proposals which won't have the below requirements if ( msg.sender != address(exchangeConfig.dao() ) ) { // Make sure that the sender has the minimum amount of xSALT required to make the proposal uint256 totalStaked = staking.totalShares(PoolUtils.STAKED_SALT); uint256 requiredXSalt = ( totalStaked * daoConfig.requiredProposalPercentStakeTimes1000() ) / ( 100 * 1000 ); require( requiredXSalt > 0, "requiredXSalt cannot be zero" ); uint256 userXSalt = staking.userShareForPool( msg.sender, PoolUtils.STAKED_SALT ); require( userXSalt >= requiredXSalt, "Sender does not have enough xSALT to make the proposal" ); // Make sure that the user doesn't already have an active proposal require( ! _userHasActiveProposal[msg.sender], "Users can only have one active proposal at a time" ); } // Make sure that a proposal of the same name is not already open for the ballot require( openBallotsByName[ballotName] == 0, "Cannot create a proposal similar to a ballot that is still open" ); require( openBallotsByName[ string.concat(ballotName, "_confirm")] == 0, "Cannot create a proposal for a ballot with a secondary confirmation" ); uint256 ballotMinimumEndTime = block.timestamp + daoConfig.ballotMinimumDuration(); // Add the new Ballot to storage ballotID = nextBallotID++; ballots[ballotID] = Ballot( ballotID, true, ballotType, ballotName, address1, number1, string1, string2, ballotMinimumEndTime ); openBallotsByName[ballotName] = ballotID;
NA
Consider :
DoS
#0 - c4-judge
2024-02-03T16:58:49Z
Picodes marked the issue as duplicate of #621
#1 - c4-judge
2024-02-19T17:07:38Z
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
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L168
maximiumWhitelistedPool can be exceeded in proposeTokenWhitelisting
proposeTokenWhitelisting, there are two caps, the maxPendingTokensForWhitelisting and maximumWhitelistedPools. However when stakers propose a token to be whitelisted, these two parameters are checked individually, but not checked against as one total sum.
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" ); require( poolsConfig.numberOfWhitelistedPools() < poolsConfig.maximumWhitelistedPools(), "Maximum number of whitelisted pools already reached" ); require( ! poolsConfig.tokenHasBeenWhitelisted(token, exchangeConfig.wbtc(), exchangeConfig.weth()), "The token has already been whitelisted" ); string memory ballotName = string.concat("whitelist:", Strings.toHexString(address(token)) ); uint256 ballotID = _possiblyCreateProposal( ballotName, BallotType.WHITELIST_TOKEN, address(token), 0, tokenIconURL, description ); _openBallotsForTokenWhitelisting.add( ballotID ); return ballotID; }
NA
Consider constraining the proposal of token whitelist by the sum of pending and whitelisted.
require( _openBallotsForTokenWhitelisting.length() < daoConfig.maxPendingTokensForWhitelisting(), "The maximum number of token whitelisting proposals are already pending" ); --- require( poolsConfig.numberOfWhitelistedPools() < poolsConfig.maximumWhitelistedPools(), "Maximum number of whitelisted pools already reached" ); +++ require( poolsConfig.numberOfWhitelistedPools() + _openBallotsForTokenWhitelisting.length() < poolsConfig.maximumWhitelistedPools()
Invalid Validation
#0 - c4-sponsor
2024-02-08T06:56:23Z
othernet-global (sponsor) disputed
#1 - othernet-global
2024-02-08T06:57:25Z
The execution of the whitelisting proposal checks maximumWhitelistedPools as well. As such the token will not be whitelisted (the execution of the proposal will fail) - until there is room to do so.
function whitelistPool( IPools pools, IERC20 tokenA, IERC20 tokenB ) external onlyOwner { require( _whitelist.length() < maximumWhitelistedPools, "Maximum number of whitelisted pools already reached" );
#2 - Picodes
2024-02-20T19:42:18Z
Downgrading to Low as the impact remains minimal
#3 - c4-judge
2024-02-20T19:42:23Z
Picodes changed the severity to QA (Quality Assurance)
#4 - c4-judge
2024-02-21T17:10:49Z
Picodes marked the issue as grade-b