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: 53/178
Findings: 2
Award: $255.83
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xVolcano
Also found by: 0x11singh99, 0xAnah, Beepidibop, JCK, JcFichtner, K42, Kaysoft, Pechenite, Raihan, Rolezn, dharma09, hunter_w3b, lsaudit, n0kto, naman1778, niroh, sivanesh_808, slvDev, unique
216.4912 USDC - $216.49
Possible Optimization 1 =
wbtc
, weth
, and salt
directly in the _arbitragePath() function.Code Snippet:
function _arbitragePath(IERC20 swapTokenIn, IERC20 swapTokenOut) internal view returns (IERC20 arbToken2, IERC20 arbToken3) { if (address(swapTokenIn) == address(wbtc) && address(swapTokenOut) == address(weth)) { return (wbtc, salt); } // Other conditions using direct references to wbtc, weth, and salt }
Possible Optimization 2 =
return
statement in _rightMoreProfitable() by directly returning the comparison result.Code Snippet:
function _rightMoreProfitable(...) internal pure returns (bool) { // ... [existing calculations] return int256(amountOutRight) - int256(midpoint + MIDPOINT_PRECISION) > profitMidpoint; }
Possible Optimization 3 =
Code Snippet:
function _bisectionSearch(...) internal pure returns (uint256 bestArbAmountIn) { // ... [initial setup] while (rightPoint - leftPoint > someThreshold) { uint256 midpoint = (leftPoint + rightPoint) / 2; if (_rightMoreProfitable(midpoint, ...)) { leftPoint = midpoint; } else { rightPoint = midpoint; } } return (leftPoint + rightPoint) / 2; }
Possible Optimization 4 =
amountOut
value in _rightMoreProfitable().Code Snippet:
function _rightMoreProfitable(...) internal pure returns (bool) { uint256 amountOutMid = calculateAmountOut(midpoint, ...); uint256 amountOutRight = calculateAmountOut(midpoint + MIDPOINT_PRECISION, ...); return int256(amountOutRight) - int256(midpoint + MIDPOINT_PRECISION) > int256(amountOutMid) - int256(midpoint); }
Possible Optimization 1 =
Minimize the emission of events when the state does not change.
Code Snippet:
function _executeSetWebsiteURL(Ballot memory ballot) internal { if (keccak256(bytes(websiteURL)) != keccak256(bytes(ballot.string1))) { websiteURL = ballot.string1; emit SetWebsiteURL(ballot.string1); } }
Possible Optimization 2 =
Implement a more efficient check for country exclusions using bitwise operations.
Code Snippet:
uint256 private countryExclusionFlags; function isCountryExcluded(string memory country) public view returns (bool) { uint256 countryIndex = getCountryIndex(country); // Implement a mapping of country codes to indices return (countryExclusionFlags & (1 << countryIndex)) != 0; } function setCountryExclusion(string memory country, bool excluded) internal { uint256 countryIndex = getCountryIndex(country); if (excluded) { countryExclusionFlags |= (1 << countryIndex); } else { countryExclusionFlags &= ~(1 << countryIndex); } }
Possible Optimization 3 =
Batch token approvals in the constructor to reduce the number of transactions.
Code Snippet:
constructor(/* ... */) { // ... batchApproveTokens(); } function batchApproveTokens() private { ISalt _salt = salt; IUSDS _usds = usds; IERC20 _dai = dai; ICollateralAndLiquidity _collateralAndLiquidity = collateralAndLiquidity; uint256 maxUint = type(uint256).max; _salt.approve(address(_collateralAndLiquidity), maxUint); _usds.approve(address(_collateralAndLiquidity), maxUint); _dai.approve(address(_collateralAndLiquidity), maxUint); }
Possible Optimization 4 =
Here is the optimized code snippet:
function finalizeBallot(uint256 ballotID) external nonReentrant { require(proposals.canFinalizeBallot(ballotID), "Ballot not finalizable"); Ballot memory ballot = proposals.ballotForID(ballotID); if (ballot.ballotType == BallotType.PARAMETER) { _finalizeParameterBallot(ballotID); } else if (ballot.ballotType == BallotType.WHITELIST_TOKEN) { _finalizeTokenWhitelisting(ballotID); } else { _finalizeApprovalBallot(ballotID); } }
Possible Optimization 1 =
Here is the optimized code snippet:
function _updateParameter(uint256 current, uint256 min, uint256 max, uint256 step, bool increase) private pure returns (uint256) { if (increase) { return current < max ? current + step : current; } else { return current > min ? current - step : current; } } function changeBootstrappingRewards(bool increase) external onlyOwner { bootstrappingRewards = _updateParameter(bootstrappingRewards, 50000 ether, 500000 ether, 50000 ether, increase); emit BootstrappingRewardsChanged(bootstrappingRewards); } // Similar changes for other parameter update functions
Possible Optimization 2 =
struct
for parameter ranges and use a mapping to manage them, simplifying the update logic.Here is the optimized code:
struct ParameterRange { uint256 min; uint256 max; uint256 step; } mapping(string => ParameterRange) private parameterRanges; constructor() { parameterRanges["bootstrappingRewards"] = ParameterRange(50000 ether, 500000 ether, 50000 ether); // Initialize other parameters similarly } function _updateParameter(string memory paramName, uint256 current, bool increase) private view returns (uint256) { ParameterRange memory range = parameterRanges[paramName]; return increase ? (current < range.max ? current + range.step : current) : (current > range.min ? current - range.step : current); } // Update functions use _updateParameter
Possible Optimization 3 =
Here is the optimized code snippet:
event ParametersUpdated(string[] parameterNames, uint256[] newValues); function batchUpdateParameters(string[] calldata names, bool[] calldata increases) external onlyOwner { require(names.length == increases.length, "Length mismatch"); uint256[] memory newValues = new uint256[](names.length); for (uint i = 0; i < names.length; i++) { newValues[i] = _updateParameter(names[i], /* current value */, increases[i]); // Update the actual parameter value } emit ParametersUpdated(names, newValues); }
Possible Optimization 4 =
_updateParameter
to enforce limits.Here is the optimized code snippet:
// Assuming _updateParameter handles range enforcement function changeBootstrappingRewards(bool increase) external onlyOwner { bootstrappingRewards = _updateParameter("bootstrappingRewards", bootstrappingRewards, increase); emit BootstrappingRewardsChanged(bootstrappingRewards); }
Possible Optimization 1 =
Here is the optimized code snippet:
function proposeTokenWhitelisting(...) external nonReentrant returns (uint256 _ballotID) { require( address(token) != address(0) && token.totalSupply() < type(uint112).max && _openBallotsForTokenWhitelisting.length() < daoConfig.maxPendingTokensForWhitelisting() && poolsConfig.numberOfWhitelistedPools() < poolsConfig.maximumWhitelistedPools() && !poolsConfig.tokenHasBeenWhitelisted(token, exchangeConfig.wbtc(), exchangeConfig.weth()), "Token whitelisting conditions not met" ); ... }
Possible Optimization 2 =
totalStaked
value is recalculated each time requiredQuorumForBallotType() is called. Caching this value can save gas by reducing repetitive calls to staking.totalShares(PoolUtils.STAKED_SALT)
.Here is the optimized code:
function requiredQuorumForBallotType(BallotType ballotType) public view returns (uint256 requiredQuorum) { uint256 totalStaked = staking.totalShares(PoolUtils.STAKED_SALT); ... }
Possible Optimization 3 =
Here is the optimized code snippet:
function winningParameterVote(uint256 ballotID) external view returns (Vote) { mapping(Vote => uint256) storage votes = _votesCastForBallot[ballotID]; if (votes[Vote.INCREASE] > votes[Vote.DECREASE] && votes[Vote.INCREASE] > votes[Vote.NO_CHANGE]) { return Vote.INCREASE; } else if (votes[Vote.DECREASE] > votes[Vote.NO_CHANGE]) { return Vote.DECREASE; } return Vote.NO_CHANGE; }
Possible Optimization 1 =
Here is the optimized code snippet:
mapping(ParameterTypes => function(bool) internal) private parameterFunctions; constructor() { parameterFunctions[ParameterTypes.maximumWhitelistedPools] = poolsConfig.changeMaximumWhitelistedPools; // ... other mappings } function _executeParameterChange(ParameterTypes parameterType, bool increase, ...) internal { function(bool) internal f = parameterFunctions[parameterType]; if (f != nil) { f(increase); } }
Possible Optimization 2 =
Here is the optimized code:
function batchUpdateParameters( ParameterTypes[] calldata types, bool[] calldata increases ) external onlyOwner { require(types.length == increases.length, "Array lengths must match"); for (uint i = 0; i < types.length; i++) { _executeParameterChange(types[i], increases[i], ...); } }
Possible Optimization 3 =
Here is the optimized code snippet:
// Replace the enum with a constant array if applicable string[] private parameterNames = ["maximumWhitelistedPools", "maximumInternalSwapPercentTimes1000", ...];
enum
is used throughout the contract.Possible Optimization 1 =
claimingAllowed
and saltAmountForEachUser
in allowClaiming().Here is the optimized code snippet:
function allowClaiming() external { require(msg.sender == address(exchangeConfig.initialDistribution()), "Only InitialDistribution can call"); require(!claimingAllowed, "Claiming already allowed"); require(numberAuthorized() > 0, "No authorized addresses"); uint256 saltBalance = salt.balanceOf(address(this)); uint256 numAuthorized = numberAuthorized(); // Cache numberAuthorized call // Batch update saltAmountForEachUser = saltBalance / numAuthorized; salt.approve(address(staking), saltBalance); claimingAllowed = true; }
SSTORE
operations.Possible Optimization 2 =
EnumerableSet
.Here is the optimized code:
uint256 private _authorizedUserCount; function authorizeWallet(address wallet) external { // ... existing checks ... if (_authorizedUsers.add(wallet)) { _authorizedUserCount++; } } function numberAuthorized() public view returns (uint256) { return _authorizedUserCount; }
Possible Optimization 3 =
isAuthorized
check in claimAirdrop().Here is the optimized code snippet:
function claimAirdrop() external nonReentrant { require(claimingAllowed, "Claiming not allowed"); require(_authorizedUsers.contains(msg.sender), "Not authorized"); require(!claimed[msg.sender], "Already claimed"); // ... rest of the function ... }
Possible Optimization 1 =
Here is the optimized code snippet:
function vote(bool voteStartExchangeYes, bytes calldata signature) external nonReentrant { // ... existing checks ... bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, msg.sender)); address signer = messageHash.toEthSignedMessageHash().recover(signature); require(signer == expectedSigner, "Invalid signature"); // ... rest of the function ... }
Possible Optimization 2 =
hasVoted
mapping.Here is the optimized code:
uint256 private votedBitField; function vote(bool voteStartExchangeYes, bytes calldata signature) external nonReentrant { // ... existing checks ... uint256 voterIndex = getVoterIndex(msg.sender); // Implement getVoterIndex require((votedBitField & (1 << voterIndex)) == 0, "User already voted"); votedBitField |= (1 << voterIndex); // ... rest of the function ... }
Possible Optimization 1 =
safeTransfer
calls for different recipients, use a single function to batch these transfers. This reduces the overhead associated with multiple external calls.Here is the optimized code snippet:
function _batchTokenTransfer(address[] memory recipients, uint256[] memory amounts) internal { require(recipients.length == amounts.length, "Mismatched array lengths"); for (uint256 i = 0; i < recipients.length; i++) { salt.safeTransfer(recipients[i], amounts[i]); } }
Possible Optimization 2 =
Here is the optimized code:
function distributionApproved() external { // ... existing checks ... // Inline whitelisted pools retrieval bytes32[] memory poolIDs = poolsConfig.whitelistedPools(); // ... rest of the function ... }
Possible Optimization 3 =
SALT
to distribute.Here is the optimized code snippet:
function distributionApproved() external { require(salt.balanceOf(address(this)) > 0, "No SALT to distribute"); // ... rest of the function ... }
SALT
to distribute. The gas savings depend on the frequency of such calls.Possible Optimization 1 =
Here is the optimized code snippet:
event PoolsUpdated(bytes32[] updatedPools, bool whitelisted); function batchWhitelistPools(IPools pools, IERC20[] memory tokensA, IERC20[] memory tokensB) external onlyOwner { require(tokensA.length == tokensB.length, "Array lengths mismatch"); bytes32[] memory updatedPools = new bytes32[](tokensA.length); for (uint256 i = 0; i < tokensA.length; i++) { bytes32 poolID = PoolUtils._poolID(tokensA[i], tokensB[i]); // Whitelisting logic... updatedPools[i] = poolID; } emit PoolsUpdated(updatedPools, true); }
Possible Optimization 2 =
Here is the optimized code:
mapping(bytes32 => bool) private _whitelistedPools; function isWhitelisted(bytes32 poolID) public view returns (bool) { return _whitelistedPools[poolID]; }
isWhitelisted
by using a direct mapping instead of EnumerableSet
. The exact savings depend on the frequency of these read operations.Possible Optimization 3 =
Here is the optimized code snippet:
function tokenHasBeenWhitelisted(IERC20 token, IERC20 wbtc, IERC20 weth) external view returns (bool) { return isWhitelisted(PoolUtils._poolID(token, wbtc)) || isWhitelisted(PoolUtils._poolID(token, weth)); }
Possible Optimization 1 =
poolID
in a loop, we can reset them in a batch to optimize gas usage.Here is the optimized code snippet:
function clearProfitsForPools() external { require(msg.sender == address(exchangeConfig.upkeep()), "Only Upkeep contract can call"); bytes32[] memory poolIDs = poolsConfig.whitelistedPools(); for (uint256 i = 0; i < poolIDs.length; i++) { delete _arbitrageProfits[poolIDs[i]]; } }
Possible Optimization 2 =
_calculatedProfits
array.Here is the optimized code:
function _calculateArbitrageProfits(bytes32[] memory poolIDs, uint256[] memory _calculatedProfits) internal view { for (uint256 i = 0; i < poolIDs.length; i++) { bytes32 poolID = poolIDs[i]; uint256 arbitrageProfit = _poolData[poolID].arbitrageProfits / 3; if (arbitrageProfit > 0) { ArbitrageIndicies memory indicies = _poolData[poolID].arbitrageIndicies; if (indicies.index1 != INVALID_POOL_ID) _calculatedProfits[indicies.index1] += arbitrageProfit; if (indicies.index2 != INVALID_POOL_ID) _calculatedProfits[indicies.index2] += arbitrageProfit; if (indicies.index3 != INVALID_POOL_ID) _calculatedProfits[indicies.index3] += arbitrageProfit; } } }
Possible Optimization 1 =
Here is the optimized code snippet:
function _addLiquidity(bytes32 poolID, uint256 maxAmount0, uint256 maxAmount1, uint256 totalLiquidity) internal returns(uint256 addedAmount0, uint256 addedAmount1, uint256 addedLiquidity) { PoolReserves storage reserves = _poolReserves[poolID]; uint256 reserve0 = reserves.reserve0; uint256 reserve1 = reserves.reserve1; if (reserve0 == 0 || reserve1 == 0) { reserves.reserve0 += uint128(maxAmount0); reserves.reserve1 += uint128(maxAmount1); return (maxAmount0, maxAmount1, maxAmount0 + maxAmount1); } uint256 proportionalB = (maxAmount0 * reserve1) / reserve0; if (proportionalB > maxAmount1) { addedAmount0 = (maxAmount1 * reserve0) / reserve1; addedAmount1 = maxAmount1; } else { addedAmount0 = maxAmount0; addedAmount1 = proportionalB; } reserves.reserve0 += uint128(addedAmount0); reserves.reserve1 += uint128(addedAmount1); addedLiquidity = (totalLiquidity * (addedAmount0 > addedAmount1 ? addedAmount0 : addedAmount1)) / (addedAmount0 > addedAmount1 ? reserve0 : reserve1); }
Possible Optimization 2 =
Here is the optimized code:
function _adjustReservesForSwap(IERC20 tokenIn, IERC20 tokenOut, uint256 amountIn) internal returns (uint256 amountOut) { (bytes32 poolID, bool flipped) = PoolUtils._poolIDAndFlipped(tokenIn, tokenOut); PoolReserves storage reserves = _poolReserves[poolID]; uint256 reserveIn = flipped ? reserves.reserve1 : reserves.reserve0; uint256 reserveOut = flipped ? reserves.reserve0 : reserves.reserve1; require(reserveIn >= PoolUtils.DUST && reserveOut >= PoolUtils.DUST, "Insufficient reserves"); reserveIn += amountIn; amountOut = reserveOut * amountIn / reserveIn; reserveOut -= amountOut; require(reserveIn >= PoolUtils.DUST && reserveOut >= PoolUtils.DUST, "Insufficient reserves after swap"); if (flipped) { reserves.reserve0 = uint128(reserveOut); reserves.reserve1 = uint128(reserveIn); } else { reserves.reserve0 = uint128(reserveIn); reserves.reserve1 = uint128(reserveOut); } }
Possible Optimization 1 =
Here is the optimized code snippet:
function _getUniswapTwapWei(IUniswapV3Pool pool, uint256 twapInterval) public view returns (uint256) { uint32[] memory secondsAgo = new uint32[](2); secondsAgo[0] = uint32(twapInterval); // from (before) secondsAgo[1] = 0; // to (now) (int56[] memory tickCumulatives, ) = pool.observe(secondsAgo); int24 tick = int24((tickCumulatives[1] - tickCumulatives[0]) / int56(uint56(twapInterval))); uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick); uint256 price = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96); uint8 decimals0 = ERC20(pool.token0()).decimals(); uint8 decimals1 = ERC20(pool.token1()).decimals(); uint256 decimalFactor = 10 ** 18; if (decimals1 > decimals0) { return FullMath.mulDiv(decimalFactor, 10 ** (decimals1 - decimals0), price); } else if (decimals0 > decimals1) { return FullMath.mulDiv(decimalFactor, FixedPoint96.Q96, price * (10 ** (decimals0 - decimals1))); } else { return FullMath.mulDiv(decimalFactor, FixedPoint96.Q96, price); } }
Possible Optimization 2 =
Here is the optimized code:
function _calculateTwap(IUniswapV3Pool pool1, IUniswapV3Pool pool2, bool flipped1, bool flipped2, uint256 twapInterval) internal view returns (uint256) { uint256 twap1 = getUniswapTwapWei(pool1, twapInterval); uint256 twap2 = getUniswapTwapWei(pool2, twapInterval); if ((twap1 == 0) || (twap2 == 0)) return 0; if (flipped1) twap1 = 10**36 / twap1; if (!flipped2) twap2 = 10**36 / twap2; return (twap2 * 10**18) / twap1; } function getTwapWBTC(uint256 twapInterval) public virtual view returns (uint256) { return _calculateTwap(UNISWAP_V3_WBTC_WETH, UNISWAP_V3_WETH_USDC, wbtc_wethFlipped, weth_usdcFlipped, twapInterval); } function getTwapWETH(uint256 twapInterval) public virtual view returns (uint256) { uint256 uniswapWETH_USDC = getUniswapTwapWei(UNISWAP_V3_WETH_USDC, twapInterval); if (uniswapWETH_USDC == 0) return 0; return weth_usdcFlipped ? uniswapWETH_USDC : (10**36 / uniswapWETH_USDC); }
Possible Optimization =
Here is the optimized code snippet:
function _aggregatePrices(uint256 price1, uint256 price2, uint256 price3) internal view returns (uint256) { uint256[] memory prices = new uint256[](3); prices[0] = price1; prices[1] = price2; prices[2] = price3; uint256 numNonZero; for (uint i = 0; i < 3; i++) { if (prices[i] > 0) numNonZero++; } if (numNonZero < 2) return 0; // Early exit uint256 minDiff = type(uint256).max; uint256 priceA; uint256 priceB; for (uint i = 0; i < 2; i++) { for (uint j = i + 1; j < 3; j++) { uint256 diff = _absoluteDifference(prices[i], prices[j]); if (diff < minDiff) { minDiff = diff; (priceA, priceB) = (prices[i], prices[j]); } } } uint256 averagePrice = (priceA + priceB) / 2; if ((_absoluteDifference(priceA, priceB) * 100000) / averagePrice > maximumPriceFeedPercentDifferenceTimes1000) { return 0; } return averagePrice; }
Possible Optimization =
Here is the optimized code snippet:
enum ConfigType { RewardsEmitterDailyPercent, EmissionsWeeklyPercent, StakingRewardsPercent, PercentRewardsSaltUSDS } function changeConfig(ConfigType configType, bool increase) external onlyOwner { uint256 changeAmount; uint256 minValue; uint256 maxValue; if (configType == ConfigType.RewardsEmitterDailyPercent) { changeAmount = 250; minValue = 250; maxValue = 2500; } else if (configType == ConfigType.EmissionsWeeklyPercent) { changeAmount = 250; minValue = 250; maxValue = 1000; } else if (configType == ConfigType.StakingRewardsPercent) { changeAmount = 5; minValue = 25; maxValue = 75; } else if (configType == ConfigType.PercentRewardsSaltUSDS) { changeAmount = 5; minValue = 5; maxValue = 25; } uint256 currentValue = _getConfigValue(configType); uint256 newValue = increase ? currentValue + changeAmount : currentValue - changeAmount; require(newValue >= minValue && newValue <= maxValue, "Value out of range"); _setConfigValue(configType, newValue); _emitConfigChangeEvent(configType, newValue); } function _getConfigValue(ConfigType configType) internal view returns (uint256) { if (configType == ConfigType.RewardsEmitterDailyPercent) return rewardsEmitterDailyPercentTimes1000; if (configType == ConfigType.EmissionsWeeklyPercent) return emissionsWeeklyPercentTimes1000; if (configType == ConfigType.StakingRewardsPercent) return stakingRewardsPercent; if (configType == ConfigType.PercentRewardsSaltUSDS) return percentRewardsSaltUSDS; revert("Invalid config type"); } function _setConfigValue(ConfigType configType, uint256 newValue) internal { if (configType == ConfigType.RewardsEmitterDailyPercent) rewardsEmitterDailyPercentTimes1000 = newValue; else if (configType == ConfigType.EmissionsWeeklyPercent) emissionsWeeklyPercentTimes1000 = newValue; else if (configType == ConfigType.StakingRewardsPercent) stakingRewardsPercent = newValue; else if (configType == ConfigType.PercentRewardsSaltUSDS) percentRewardsSaltUSDS = newValue; } function _emitConfigChangeEvent(ConfigType configType, uint256 newValue) internal { if (configType == ConfigType.RewardsEmitterDailyPercent) emit RewardsEmitterDailyPercentChanged(newValue); else if (configType == ConfigType.EmissionsWeeklyPercent) emit EmissionsWeeklyPercentChanged(newValue); else if (configType == ConfigType.StakingRewardsPercent) emit StakingRewardsPercentChanged(newValue); else if (configType == ConfigType.PercentRewardsSaltUSDS) emit PercentRewardsSaltUSDSChanged(newValue); }
Possible Optimization =
Here is the optimized code snippet:
function addSALTRewardsBatch(AddedReward[] calldata addedRewardsBatch) external nonReentrant { uint256 totalSum = 0; for (uint256 batch = 0; batch < addedRewardsBatch.length; batch++) { uint256 sum = 0; AddedReward[] calldata addedRewards = addedRewardsBatch[batch]; for (uint256 i = 0; i < addedRewards.length; i++) { AddedReward memory addedReward = addedRewards[i]; require(poolsConfig.isWhitelisted(addedReward.poolID), "Invalid pool"); uint256 amountToAdd = addedReward.amountToAdd; if (amountToAdd != 0) { pendingRewards[addedReward.poolID] += amountToAdd; sum += amountToAdd; } } if (sum > 0) { salt.safeTransferFrom(msg.sender, address(this), sum); totalSum += sum; } } require(totalSum > 0, "No rewards to add"); }
safeTransferFrom
calls when adding rewards for multiple pools in batches. The exact gas savings depend on the number of pools and the frequency of reward additions.Possible Optimization =
Here is the optimized code snippet:
function _distributeRewards(bytes32[] memory poolIDs, uint256[] memory amounts, bool isStaking) internal { AddedReward[] memory addedRewards = new AddedReward[](poolIDs.length); for (uint256 i = 0; i < poolIDs.length; i++) { addedRewards[i] = AddedReward(poolIDs[i], amounts[i]); } if (isStaking) { stakingRewardsEmitter.addSALTRewards(addedRewards); } else { liquidityRewardsEmitter.addSALTRewards(addedRewards); } }
Possible Optimization =
Here is the optimized code snippet:
function batchUpdateConfigurations( uint256 newRewardPercentForCallingLiquidation, uint256 newMaxRewardValueForCallingLiquidation, uint256 newMinimumCollateralValueForBorrowing, uint256 newInitialCollateralRatioPercent, uint256 newMinimumCollateralRatioPercent, uint256 newPercentArbitrageProfits ) external onlyOwner { rewardPercentForCallingLiquidation = newRewardPercentForCallingLiquidation; maxRewardValueForCallingLiquidation = newMaxRewardValueForCallingLiquidation; minimumCollateralValueForBorrowing = newMinimumCollateralValueForBorrowing; initialCollateralRatioPercent = newInitialCollateralRatioPercent; minimumCollateralRatioPercent = newMinimumCollateralRatioPercent; percentArbitrageProfitsForStablePOL = newPercentArbitrageProfits; emit RewardPercentForCallingLiquidationChanged(newRewardPercentForCallingLiquidation); emit MaxRewardValueForCallingLiquidationChanged(newMaxRewardValueForCallingLiquidation); emit MinimumCollateralValueForBorrowingChanged(newMinimumCollateralValueForBorrowing); emit InitialCollateralRatioPercentChanged(newInitialCollateralRatioPercent); emit MinimumCollateralRatioPercentChanged(newMinimumCollateralRatioPercent); emit PercentArbitrageProfitsForStablePOLChanged(newPercentArbitrageProfits); }
Possible Optimization 1 =
USD
into a single internal
function. This will reduce the bytecode size and improve readability.Here is the optimized code snippet:
function _calculateCollateralValueInUSD(uint256 amountBTC, uint256 amountETH) internal view returns (uint256) { uint256 btcPrice = priceAggregator.getPriceBTC(); uint256 ethPrice = priceAggregator.getPriceETH(); uint256 btcValue = (amountBTC * btcPrice) / wbtcTenToTheDecimals; uint256 ethValue = (amountETH * ethPrice) / wethTenToTheDecimals; return btcValue + ethValue; }
Possible Optimization 2 =
liquidatable
users to reduce gas costs in findLiquidatableUsers().Here is the optimized code:
function findLiquidatableUsersBatch(uint256 batchSize) external view returns (address[] memory) { uint256 userCount = numberOfUsersWithBorrowedUSDS(); uint256 batchCount = (userCount + batchSize - 1) / batchSize; address[] memory liquidatableUsers = new address[](userCount); uint256 currentIndex = 0; for (uint256 batch = 0; batch < batchCount; ++batch) { uint256 start = batch * batchSize; uint256 end = Math.min(start + batchSize, userCount); address[] memory batchUsers = findLiquidatableUsers(start, end); for (uint256 i = 0; i < batchUsers.length; ++i) { liquidatableUsers[currentIndex++] = batchUsers[i]; } } // Resize the array to fit the actual number of liquidatable users address[] memory resizedLiquidatableUsers = new address[](currentIndex); for (uint256 i = 0; i < currentIndex; ++i) { resizedLiquidatableUsers[i] = liquidatableUsers[i]; } return resizedLiquidatableUsers; }
Possible Optimization 1 =
USDS
. This reduces the number of transactions required when multiple tokens are available for swap, thereby saving gas.Here is the optimized code snippet:
function _batchSwapToUSDS() internal { address[] memory tokens = new address[](3); tokens[0] = address(wbtc); tokens[1] = address(weth); tokens[2] = address(dai); for (uint i = 0; i < tokens.length; i++) { uint256 tokenBalance = IERC20(tokens[i]).balanceOf(address(this)); if (tokenBalance > 0) { PoolUtils._placeInternalSwap(pools, IERC20(tokens[i]), usds, tokenBalance, poolsConfig.maximumInternalSwapPercentTimes1000()); } } }
Possible Optimization 2 =
PERCENT_POL_TO_WITHDRAW
based on the shortfall amount. This can prevent withdrawing more liquidity than necessary, thus saving on potential swap fees and slippage.Here is the optimized code:
function adjustPOLWithdrawal(uint256 shortfall) internal returns (uint256 adjustedPercent) { uint256 totalPOLValue = // Calculate total Protocol Owned Liquidity value adjustedPercent = (shortfall * 100) / totalPOLValue; adjustedPercent = Math.min(adjustedPercent, MAX_PERCENT_POL_TO_WITHDRAW); // Ensure it doesn't exceed a max limit return adjustedPercent; }
Possible Optimization =
Here is the optimized code snippet:
function changeStakingParameter(string memory parameter, bool increase) external onlyOwner { if (keccak256(bytes(parameter)) == keccak256(bytes("minUnstakeWeeks"))) { minUnstakeWeeks = _adjustValue(minUnstakeWeeks, increase, 1, 12); emit MinUnstakeWeeksChanged(minUnstakeWeeks); } else if (keccak256(bytes(parameter)) == keccak256(bytes("maxUnstakeWeeks"))) { maxUnstakeWeeks = _adjustValue(maxUnstakeWeeks, increase, 20, 108, 8); emit MaxUnstakeWeeksChanged(maxUnstakeWeeks); } else if (keccak256(bytes(parameter)) == keccak256(bytes("minUnstakePercent"))) { minUnstakePercent = _adjustValue(minUnstakePercent, increase, 10, 50, 5); emit MinUnstakePercentChanged(minUnstakePercent); } else if (keccak256(bytes(parameter)) == keccak256(bytes("modificationCooldown"))) { modificationCooldown = _adjustValue(modificationCooldown, increase, 15 minutes, 6 hours, 15 minutes); emit ModificationCooldownChanged(modificationCooldown); } } function _adjustValue(uint256 currentValue, bool increase, uint256 minValue, uint256 maxValue, uint256 stepSize) internal pure returns (uint256) { if (increase) { return (currentValue + stepSize <= maxValue) ? currentValue + stepSize : currentValue; } else { return (currentValue - stepSize >= minValue) ? currentValue - stepSize : currentValue; } }
Possible Optimization =
tokenA
and tokenB
within the _dualZapInLiquidity() and _depositLiquidityAndIncreaseShare() functions. This can be optimized since the contract can set a maximum allowance once and only update it if necessary. Set a maximum allowance for tokenA
and tokenB
for the pools contract initially and only update it when the allowance is insufficient.Here is the optimized code snippet:
function _ensureMaxAllowance(IERC20 token, address spender) internal { if (token.allowance(address(this), spender) < type(uint256).max / 2) { token.approve(spender, type(uint256).max); } }
approve
calls, especially in scenarios with multiple liquidity operations.Possible Optimization =
Here is the optimized code snippet:
function claimRewardsForMultiplePools(bytes32[] calldata poolIDs) external nonReentrant { uint256 totalClaimableRewards = 0; for (uint256 i = 0; i < poolIDs.length; i++) { totalClaimableRewards += _claimRewards(msg.sender, poolIDs[i]); } if (totalClaimableRewards > 0) { salt.safeTransfer(msg.sender, totalClaimableRewards); } } function _claimRewards(address wallet, bytes32 poolID) internal returns (uint256) { // existing logic to calculate and update claimable rewards // return the calculated claimable rewards }
Possible Optimization 1 =
unstaking
operations in a single transaction can save gas compared to executing multiple separate transactions.Here is the optimized code snippet:
function batchProcessUnstakes(uint256[] calldata unstakeIDs) external nonReentrant { for (uint256 i = 0; i < unstakeIDs.length; i++) { _processUnstake(unstakeIDs[i]); } } function _processUnstake(uint256 unstakeID) internal { Unstake storage u = _unstakesByID[unstakeID]; // Existing logic for processing an unstake }
unstakes
in a single transaction. The savings are more pronounced with a higher number of unstakes
.Possible Optimization 2 =
immutable
variables in memory
within functions to reduce gas costs associated with reading from storage.Here is the optimized code:
function stakeSALT(uint256 amountToStake) external nonReentrant { ISalt _salt = salt; // Cache immutable variable IExchangeConfig _exchangeConfig = exchangeConfig; // Cache immutable variable require(_exchangeConfig.walletHasAccess(msg.sender), "Sender does not have exchange access"); _increaseUserShare(msg.sender, PoolUtils.STAKED_SALT, amountToStake, false); _salt.safeTransferFrom(msg.sender, address(this), amountToStake); emit SALTStaked(msg.sender, amountToStake); }
Possible Optimization 1 =
weth
, salt
, usds
, and dai
tokens. This can be optimized by setting a maximum allowance initially and only updating it when necessary, reducing the number of approve calls.Here is the optimized code snippet:
function _ensureMaxAllowance(IERC20 token, address spender) internal { if (token.allowance(address(this), spender) < type(uint256).max / 2) { token.approve(spender, type(uint256).max); } }
Possible Optimization 2 =
upkeep
steps. Instead of executing each step individually, group related steps into batch functions to reduce the overhead of external calls and state checks.Here is the optimized code:
function performBatchUpkeep(uint256[] calldata steps) public nonReentrant { for (uint256 i = 0; i < steps.length; i++) { if (steps[i] == 1) { try this.step1() {} catch (bytes memory error) { emit UpkeepError("Step 1", error); } } else if (steps[i] == 2) { try this.step2(msg.sender) {} catch (bytes memory error) { emit UpkeepError("Step 2", error); } // ... other steps } } }
Possible Optimization =
Here is the optimized code:
function setContracts(IDAO _dao, IUpkeep _upkeep, IInitialDistribution _initialDistribution, IAirdrop _airdrop, VestingWallet _teamVestingWallet, VestingWallet _daoVestingWallet) external onlyOwner { require(address(dao) == address(0), "setContracts can only be called once"); dao = _dao; upkeep = _upkeep; initialDistribution = _initialDistribution; airdrop = _airdrop; teamVestingWallet = _teamVestingWallet; daoVestingWallet = _daoVestingWallet; }
#0 - c4-judge
2024-02-03T14:31:31Z
Picodes marked the issue as grade-a
#1 - c4-sponsor
2024-02-08T08:50:45Z
othernet-global (sponsor) acknowledged
🌟 Selected for report: peanuts
Also found by: 0xAsen, 0xHelium, 0xSmartContract, 0xepley, DedOhWale, K42, LinKenji, Sathish9098, ZanyBonzy, catellatech, fouzantanveer, foxb868, hassanshakeel13, hunter_w3b, jauvany, kaveyjoe, kinda_very_good, klau5, niroh, rspadi, yongskiws
39.3353 USDC - $39.34
Salty-IO's suite, including Staking, Liquidity, StakingRewards, SaltRewards, Upkeep, ExchangeConfig, and ArbitrageSearch, forms a complex ecosystem. Each contract is integral, managing specific aspects like staking, liquidity, rewards distribution, system maintenance, and arbitrage opportunities.
Key Data Structures and Libraries
Use of Modifiers and Access Control
nonReentrant
modifier is extensively used in Staking and Liquidity for functions involving token transfers.Use of State Variables
Use of Events and Logging
Key Functions that need special attention
Upgradability
stakeSALT
: Inaccurate reward calculations could lead to incorrect reward distributions. Implement rigorous testing and validation for reward calculation logic.unstake
: Potential manipulation of unstaking terms by users. Validate unstaking conditions and implement checks against manipulation.recoverSALT
: Unauthorized recovery of SALT tokens. Strengthen access controls and validate user permissions._depositLiquidityAndIncreaseShare
: Potential imbalances in liquidity provision due to zapping logic. Review and optimize the zapping logic for balanced liquidity provision._withdrawLiquidityAndClaim
: Inaccurate calculation of withdrawals leading to loss of funds. Implement thorough validation checks for withdrawal calculations.performUpkeep
: Inefficient execution of maintenance tasks leading to increased gas costs and potential errors. Optimize upkeep functions for efficiency and error handling.setContracts
: Unauthorized modification of critical contract addresses. Implement multi-signature control for critical function calls.walletHasAccess
: Potential bypass of access control mechanisms. Regularly update and audit the AccessManager contract.performUpkeep
: Inefficiencies in reward distribution mechanism. Optimize the logic for distributing rewards to ensure fairness and efficiency.addSALTRewards
: Potential manipulation in the addition of rewards. Implement strict validation checks for reward additions._increaseUserShare
: Inaccurate allocation of rewards leading to unfair distribution. Ensure precision and fairness in the reward distribution mechanism._decreaseUserShare
: Loss of rewards due to miscalculations. Implement robust validation checks for share decrements.createProposal
: Vulnerability to spam or malicious proposals. Implement robust proposal validation and anti-spam mechanisms.vote
: Potential for voting manipulation or sybil attacks. Strengthen the voting mechanism with additional security measures like identity verification.claimAirdrop
: Exploitation of the airdrop mechanism. Implement stringent eligibility checks and anti-fraud measures.ExchangeConfig
.StakingRewards
and Liquidity
, to ensure robustness against exploits.Upkeep
to prevent failures during maintenance operations.Liquidity
to prevent imbalances and inefficiencies.Salty-IO's contract suite, while structurally sound, presents challenges due to its interconnectedness and complexity. Emphasis on decentralized governance, rigorous testing, and continuous monitoring is essential for maintaining the integrity and efficiency of the ecosystem.
30 hours
#0 - c4-judge
2024-02-03T14:55:23Z
Picodes marked the issue as grade-b