Salty.IO - 7ashraf'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: 90/178

Findings: 1

Award: $72.13

🌟 Selected for report: 0

🚀 Solo Findings: 0

Quality Assurance Report

Summary

The QA report highlights several issues in the codebase. One critical issue ([L-01]) involves the addition of a cooldown period even when the price is not changed, leading to false emissions. Additionally, the report identifies issues such as checking ballot existence before removal ([L-02]), verifying the existence of a country before proposing exclusion ([L-03]), and checking for the existence of a ballotId before counting votes ([L-04]). Other issues include ensuring values are not smaller than a specified threshold ([L-05]), checking for pool whitelisting ([L-06]), and addressing various non-critical concerns ([N-01] to [N-06]). Remedial actions and mitigations are provided for each issue.

Summary Table:

Issue NumberTitleNumber of Instances
L-01Cooldown period added without price change1
L-02Check if ballot exists before removal1
L-03Check if country already exists1
L-04Check if ballotId exists before counting votes3
L-05Check if values are smaller than pool.dust3
L-06Check if pool already whitelisted1
N-01Revert with an error message3
N-02Add else and revert on else1
N-03Require increment amount to be > 01
N-04Emit vote updated and vote casted events1
N-05Check for token decimals under 181
N-06Function requires more comments1
R-01Add msg.sender to proposal created message1
R-02recoverSalt() should be called after unstake1
R-03Emit an event on swap1
R-04Consider adding remaining amount event1

[L-01] Cooldown period will be added even if the price is not changed

On numbers greater than 3, the function will have a false cooldown change and a false emit.

Instances

else if ( priceFeedNum == 3 )
			priceFeed3 = newPriceFeed;

		priceFeedModificationCooldownExpiration = block.timestamp + priceFeedModificationCooldown;
		emit PriceFeedSet(priceFeedNum, newPriceFeed);

Mitigation

Add a final else statement and revert or return.

[L-02] Check if ballot exists first before removing to avoid false removes and false emits

Instancs

	function 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 );

		// Remove from the list of all open ballots
		_allOpenBallots.remove( ballotID );

		ballot.ballotIsLive = false;

		// Indicate that the user who posted the proposal no longer has an active proposal
		address userThatPostedBallot = _usersThatProposedBallots[ballotID];
		_userHasActiveProposal[userThatPostedBallot] = false;

		delete openBallotsByName[ballot.ballotName];

		emit BallotFinalized(ballotID);
		}

[L-03] Check if country already exists

Instances

Instances

	function proposeCountryExclusion( string calldata country, string calldata description ) external nonReentrant returns (uint256 ballotID)
		{
		require( bytes(country).length == 2, "Country must be an ISO 3166 Alpha-2 Code" );

		string memory ballotName = string.concat("exclude:", country );
		return _possiblyCreateProposal( ballotName, BallotType.EXCLUDE_COUNTRY, address(0), 0, country, description );
		}

[L-04] Check if ballotId exists before counting votes

Instances

		mapping(Vote=>uint256) storage votes = _votesCastForBallot[ballotID];

		Ballot memory ballot = ballots[ballotID];
		mapping(Vote=>uint256) storage votes = _votesCastForBallot[ballotID];

		mapping(Vote=>uint256) storage votes = _votesCastForBallot[ballotID];

[L-05] Check if any of these values are smaller than pool.dust

Instances

		(uint256 reservesA0, uint256 reservesA1) = getPoolReserves( weth, arbToken2);

		(uint256 reservesB0, uint256 reservesB1) = getPoolReserves( arbToken2, arbToken3);

		(uint256 reservesC0, uint256 reservesC1) = getPoolReserves( arbToken3, weth);

[L-06] Check if pool is already whitelisted

Instances

		_whitelist.add(poolID);

[N-01] Revert with an error message instead of returning 0 value

Instances

		if ( (reservesWETH < PoolUtils.DUST) || (reservesTokenIn < PoolUtils.DUST) )
			return 0; 
		if ( swapAmountInValueInETH <= PoolUtils.DUST )
			return 0;
		if ( swapAmountInValueInETH == 0 )
			return 0;

[N-02] Add else and revert on else

Should add an else statement and revert with an error message instead of returning the variable un-initialized with zero value

Instances

		if (arbitrageAmountIn > 0)
			arbitrageProfit = _arbitrage(arbToken2, arbToken3, arbitrageAmountIn);

[N-03] Require increment amount to be > 0

Instance

*Liquidizer.sol #

		usdsThatShouldBeBurned += usdsToBurn;

[N-04] Emit vote updated in case of vote update, and vote casted in case of a vote cast

Instances

		if ( lastVote.votingPower > 0 )
			_votesCastForBallot[ballotID][lastVote.vote] -= lastVote.votingPower;

		// Update the votes cast for the ballot with the user's current voting power
		_votesCastForBallot[ballotID][vote] += userVotingPower;

		// Remember how the user voted in case they change their vote later
		_lastUserVoteForBallot[ballotID][msg.sender] = UserVote( vote, userVotingPower );

		emit VoteCast(msg.sender, ballotID, vote, userVotingPower);

[N-05] check for token decimals under 18

Tokens with decimals above 18 may break the contract

Instances

		emit ProposalCreated(ballotID, ballotType, ballotName);
	function proposeTokenWhitelisting( IERC20 token, string calldata tokenIconURL, string calldata description ) external nonReentrant returns (uint256 _ballotID){
+        //require tokens.decimals() <= 18;

        ...
    }

[N-06] Function requires more comments

add comments to describe function behavior, input, and output

Instances

function _possiblyCreateProposal( string memory ballotName, BallotType ballotType, address address1, uint256 number1, string memory string1, string memory string2 ) internal returns (uint256 ballotID)

[R-01] Add msg.sender to proposal created message

Instances

		emit ProposalCreated(ballotID, ballotType, ballotName);

[R-02] recoverSalt() should be called after the unstake() method

Instances

[R-03] emit an event on swap

Instances

[R-04] Consider adding the remaining amount or wallet closed event on paying the full amount

Instances

		if ( usdsBorrowedByUsers[msg.sender] == 0 )
			_walletsWithBorrowedUSDS.remove(msg.sender);

		emit RepaidUSDS(msg.sender, amountRepaid);

#0 - c4-judge

2024-02-03T13:21:59Z

Picodes marked the issue as grade-a

#1 - othernet-global

2024-02-12T21:31:34Z

L5: Already checked in _bisection search: if ( reservesA0 <= PoolUtils.DUST || reservesA1 <= PoolUtils.DUST || reservesB0 <= PoolUtils.DUST || reservesB1 <= PoolUtils.DUST || reservesC0 <= PoolUtils.DUST || reservesC1 <= PoolUtils.DUST ) return 0;

L6: Checked at the time the proposal is made.

#2 - c4-sponsor

2024-02-12T21:31:37Z

othernet-global (sponsor) acknowledged

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