Salty.IO - K42'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: 53/178

Findings: 2

Award: $255.83

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Awards

216.4912 USDC - $216.49

Labels

bug
G (Gas Optimization)
grade-a
sponsor acknowledged
G-16

External Links

Gas Optimization Report for Salty.IO by K42

  • Note: I made sure these optimizations are unique in relation to the Bot Report and 4Analy3er Report.

Possible Optimizations in ArbitrageSearch.sol

Possible Optimization 1 =

  • Use immutable variables 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
}
  • Estimated Gas Saved = Directly referencing immutable variables can save gas by avoiding additional memory or storage operations.

Possible Optimization 2 =

Code Snippet:

function _rightMoreProfitable(...) internal pure returns (bool) {
    // ... [existing calculations]
    return int256(amountOutRight) - int256(midpoint + MIDPOINT_PRECISION) > profitMidpoint;
}
  • Estimated Gas Saved = This approach reduces the need for an extra variable and directly returns the comparison result, saving gas on variable assignment.

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;
}
  • Estimated Gas Saved = A clearer loop condition and direct return statement can save gas by reducing the number of operations.

Possible Optimization 4 =

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);
}
  • Estimated Gas Saved = This optimization reduces redundant calculations, saving gas each time the function is called.

Possible Optimizations in DAO.sol

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);
    }
}
  • Estimated Gas Saved = This change can save gas by avoiding unnecessary event emissions when the website URL remains the same. The gas savings depend on how often this function is called with the same URL.

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);
    }
}
  • Estimated Gas Saved = This optimization can significantly reduce the gas cost of updating and checking country exclusions, especially as the number of countries grows.

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);
}
  • Estimated Gas Saved = This change can reduce the gas cost during contract deployment by minimizing the number of separate approve calls.

Possible Optimization 4 =

  • Consolidate similar logic in ballot finalization functions to reduce redundancy.

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);
    }
}
  • Estimated gas saved = This optimization can save gas by reducing the complexity of the finalizeBallot() function and avoiding redundant checks. The savings depend on the frequency of ballot finalizations.

Possible Optimizations in DAOConfig.sol

Possible Optimization 1 =

  • Consolidate the range checking logic in the parameter change functions to reduce redundancy and improve code maintainability.

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
  • Estimated gas saved = This optimization reduces the bytecode size and simplifies the logic, potentially saving gas on deployment and execution. The exact savings depend on the frequency of these function calls.

Possible Optimization 2 =

  • Define a 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
  • Estimated gas saved = This approach centralizes parameter range management, reducing the complexity of update functions and saving gas on repeated logic execution.

Possible Optimization 3 =

  • Batch multiple parameter changes into a single transaction and emit a consolidated event to reduce gas costs associated with multiple transactions and event emissions.

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);
}
  • Estimated gas saved = Batching updates in a single transaction can significantly reduce the gas cost compared to multiple separate transactions, especially when multiple parameters are frequently updated together.

Possible Optimization 4 =

  • Remove redundant range checks in setter functions, relying on the logic in _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);
}
  • Estimated gas saved = This optimization reduces the gas cost by eliminating redundant conditional checks in each setter function.

Possible Optimizations in Proposals.sol

Possible Optimization 1 =

  • In the proposeTokenWhitelisting() function, multiple conditions are checked sequentially. By using short-circuit evaluation, we can avoid unnecessary condition checks once one fails, saving gas.

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"
    );
    ...
}
  • Estimated gas saved = This change can save gas by reducing the number of condition checks. The exact savings depend on how often the conditions fail early.

Possible Optimization 2 =

  • The 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);
    ...
}
  • Estimated gas saved = This optimization can save gas by reducing the number of external calls. The exact amount of gas saved depends on the frequency of function calls.

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;
}
  • Estimated gas saved = This change can save gas by reducing the number of conditional checks. The exact savings depend on the frequency of calls to this function.

Possible Optimizations in Parameters.sol

Possible Optimization 1 =

  • Instead of using a long series of if-else statements, use a mapping to associate ParameterTypes with their corresponding functions. This can reduce the bytecode size and improve the efficiency of the function calls.

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);
    }
}
  • Estimated gas saved = This change can reduce the gas cost associated with the execution of _executeParameterChange by minimizing the number of conditional checks and simplifying the control flow.

Possible Optimization 2 =

  • For parameters that are often updated together, provide a function to update them in a batch. This can save gas by reducing the number of separate transactions required for each update.

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], ...);
    }
}
  • Estimated gas saved = This optimization can significantly reduce the gas cost when multiple parameters need to be updated together, as it consolidates multiple transactions into a single call.

Possible Optimization 3 =

  • If the enum ParameterTypes is only used for indexing and not for logic, consider replacing it with a simpler data structure like an array or a list, especially if the order of the parameters is not crucial.

Here is the optimized code snippet:

// Replace the enum with a constant array if applicable
string[] private parameterNames = ["maximumWhitelistedPools", "maximumInternalSwapPercentTimes1000", ...];
  • Estimated gas saved = This change might save a small amount of gas by simplifying the contract's structure, but the actual savings depend on how the enum is used throughout the contract.

Possible Optimizations in Airdrop.sol

Possible Optimization 1 =

  • Batch update for 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;
}
  • Estimated gas saved = This optimization might save a moderate amount of gas by reducing the number of SSTORE operations.

Possible Optimization 2 =

  • Use a counter for authorized users instead of 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;
}
  • Estimated gas saved = This change can significantly reduce the gas cost of adding and counting authorized users, especially as the number of users grows.

Possible Optimization 3 =

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 ...
}
  • Estimated gas saved = This change can save a small amount of gas for each claimAirdrop() call.

Possible Optimizations in BootstrapBallot.sol

Possible Optimization 1 =

  • Inline signature verification in vote().

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 ...
}
  • Estimated gas saved = This change can save a small amount of gas for each vote() call.

Possible Optimization 2 =

  • Use a bit field for 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 ...
}
  • Estimated gas saved = This optimization can significantly reduce the gas cost of recording votes, especially as the number of voters grows.

Possible Optimizations in InitialDistribution.sol

Possible Optimization 1 =

  • Instead of multiple 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]);
    }
}
  • Estimated gas saved = This optimization can save a significant amount of gas by reducing the number of external calls and loop iterations. The exact amount of gas saved depends on the number of transfers.

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 ...
}
  • Estimated gas saved = This optimization may save a small amount of gas by avoiding an external call to retrieve whitelisted pools. The savings are relatively minor but can add up in contracts with high transaction volume.

Possible Optimization 3 =

  • Add an early check for zero balance to prevent unnecessary operations if there's no 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 ...
}
  • Estimated gas saved = This check can prevent unnecessary gas usage in case of a call with no SALT to distribute. The gas savings depend on the frequency of such calls.

Possible Optimizations in PoolsConfig.sol

Possible Optimization 1 =

  • Emit a single event after batch operations instead of emitting an event for each operation.

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);
}
  • Estimated gas saved = This optimization can significantly reduce gas costs when whitelisting multiple pools in a single transaction by reducing the number of emitted events.

Possible Optimization 2 =

  • Use a more gas-efficient data structure for storing whitelisted pools.

Here is the optimized code:

mapping(bytes32 => bool) private _whitelistedPools;

function isWhitelisted(bytes32 poolID) public view returns (bool) {
    return _whitelistedPools[poolID];
}
  • Estimated gas saved = This change can reduce the gas cost for read operations like 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));
}
  • Estimated gas saved = This optimization can save gas by reducing the number of operations in the tokenHasBeenWhitelisted() function. The savings are more significant when this function is called frequently.

Possible Optimizations in PoolStats.sol

Possible Optimization 1 =

  • Instead of resetting _arbitrageProfits for each 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]];
    }
}
  • Estimated gas saved = This change can reduce the gas cost for the clearProfitsForPools() function, especially when the number of whitelisted pools is large.

Possible Optimization 2 =

  • Optimize the _calculateArbitrageProfits() function by reducing redundant calculations and memory allocations. Specifically, calculate the arbitrage profit once per pool and directly update the _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;
        }
    }
}
  • Estimated gas saved = This change can save gas by reducing the number of times the arbitrage profit is calculated and accessed from storage.

Possible Optimizations in Pools.sol

Possible Optimization 1 =

  • The current implementation of _addLiquidity() function performs multiple arithmetic operations that can be optimized.

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);
}
  • Estimated gas saved = This optimization can save a significant amount of gas by reducing the number of arithmetic operations, especially in high-frequency liquidity addition scenarios.

Possible Optimization 2 =

  • The _adjustReservesForSwap() function performs multiple conditional checks and arithmetic operations that can be optimized.

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);
    }
}
  • Estimated gas saved = This optimization can significantly reduce gas costs by simplifying the logic and reducing the number of operations, particularly beneficial for high-frequency swap transactions.

Possible Optimizations in CoreUniswapFeed.sol

Possible Optimization 1 =

  • The _getUniswapTwapWei() function performs several arithmetic operations that can be optimized for gas efficiency.

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);
    }
}
  • Estimated gas saved = This optimization can save a significant amount of gas by reducing the number of multiplications and divisions, especially when this function is called frequently.

Possible Optimization 2 =

  • The getTwapWBTC() and getTwapWETH() functions perform similar operations with some redundant calculations. Therefore create a helper function to handle common calculations and reduce code duplication.

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);
}
  • Estimated gas saved = This optimization can significantly reduce gas costs by eliminating redundant calculations and improving code readability, particularly beneficial for frequent price feed updates.

Possible Optimization in PriceAggregator.sol

Possible Optimization =

  • Optimize the _aggregatePrices() function by reducing redundant calculations and early exiting when possible.

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;
}
  • Estimated gas saved = This optimization reduces the computational complexity of the _aggregatePrices() function, leading to gas savings. The exact amount of gas saved depends on the frequency of price aggregation calls and the complexity of the original implementation.

Possible Optimization in RewardsConfig.sol

Possible Optimization =

  • Consolidate the four similar change functions into a single generic function to reduce code redundancy.

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);
}
  • Estimated gas saved = This optimization reduces the bytecode size by eliminating redundant code, leading to lower deployment and execution costs. The exact gas savings depend on the frequency of configuration changes.

Possible Optimization in RewardsEmitter.sol

Possible Optimization =

  • Implement batch processing for addSALTRewards() function to reduce gas costs when adding rewards for multiple pools.

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");
}
  • Estimated gas saved = The gas savings primarily come from reducing the number of external 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 in SaltRewards.sol

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);
    }
}
  • Estimated gas saved = This optimization primarily saves gas by reducing the bytecode size and simplifying the execution path. The exact gas savings would depend on the frequency and context of these function calls.

Possible Optimization in StableConfig.sol

Possible Optimization =

  • Implement a function to batch update multiple configuration parameters in a single transaction. This reduces the number of transactions required to update multiple parameters, saving gas when multiple changes are needed.

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);
}
  • Estimated gas saved = Batching multiple updates into a single transaction can significantly reduce the cumulative gas cost compared to executing each update in a separate transaction.

Possible Optimizations in CollateralAndLiquidity.sol

Possible Optimization 1 =

  • Consolidate the repeated logic for calculating the collateral value in 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;
}
  • Estimated gas saved = This optimization primarily improves code maintainability and readability, with minor gas savings due to reduced code duplication.

Possible Optimization 2 =

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;
}
  • Estimated gas saved = Batch processing can significantly reduce the gas cost per liquidation when multiple liquidations are processed in a single transaction. The savings are more pronounced when the number of liquidations in a batch is higher.

Possible Optimizations in Liquidizer.sol

Possible Optimization 1 =

  • Implement a function to batch process token swaps to 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());
        }
    }
}
  • Estimated gas saved = This optimization reduces the gas cost by minimizing the number of external function calls and looping through an array of tokens, rather than calling the swap function separately for each token.

Possible Optimization 2 =

  • Introduce a mechanism to dynamically adjust the 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;
}
  • Estimated gas saved = While this optimization may not directly save a significant amount of gas, it optimizes the liquidity management, potentially saving costs associated with excessive liquidity withdrawal and subsequent swaps.

Possible Optimization in StakingConfig.sol

Possible Optimization =

  • Combine the similar functions for changing staking parameters into a single function with parameters. This reduces the bytecode size and simplifies the contract.

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;
    }
}
  • Estimated gas saved = This optimization reduces the contract's complexity and gas cost for deployment. It also simplifies future maintenance.

Possible Optimization In Liquidity.sol

Possible Optimization =

  • The contract repeatedly calls approve on the 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);
    }
}
  • Estimated gas saved = This optimization reduces the gas cost for repeated approve calls, especially in scenarios with multiple liquidity operations.

Possible Optimization In StakingRewards.sol

Possible Optimization =

  • Implement a batch processing mechanism for claiming rewards across multiple pools. This can reduce the gas cost per transaction when users interact with multiple pools.

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
}
  • Estimated gas saved = This approach can significantly reduce the gas cost when a user claims rewards from multiple pools in a single transaction. The exact savings depend on the number of pools interacted with.

Possible Optimizations In Staking.sol

Possible Optimization 1 =

  • Here also, allowing users to batch process multiple 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
}
  • Estimated gas saved = This optimization can significantly reduce the gas cost when a user processes multiple unstakes in a single transaction. The savings are more pronounced with a higher number of unstakes.

Possible Optimization 2 =

  • Cache frequently accessed 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);
}
  • Estimated gas saved = This optimization can save a small amount of gas for each external call that accesses these variables. The savings are more significant in functions called frequently or in loops.

Possible Optimizations In Upkeep.sol

Possible Optimization 1 =

  • Consolidate repetitive token approval logic. The contract frequently calls approve on 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);
    }
}
  • Estimated gas saved = This optimization reduces the gas cost for repeated approve calls, especially in scenarios with multiple upkeep operations. The exact savings depend on the frequency of these operations.

Possible Optimization 2 =

  • Batch processing of 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
        }
    }
}
  • Estimated gas saved = This approach can significantly reduce the gas cost when multiple upkeep steps are processed in a single transaction. The savings are more pronounced when the number of steps in a batch is higher.

Possible Optimization In ExchangeConfig.sol

Possible Optimization =

  • Simplify the setContracts() function to reduce its complexity and improve readability.

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;
}
  • Estimated gas saved = This optimization does not directly save gas but improves the contract's maintainability and readability, which is crucial for long-term sustainability. Included in gas optimizations list as will make code cleaner and more straightforward code can prevent errors and make future updates easier.

#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

Awards

39.3353 USDC - $39.34

Labels

analysis-advanced
grade-b
A-17

External Links

Advanced Analysis Report for Salty-IO by K42

Overview

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.

Understanding the Ecosystem:
  • The contracts are intricately linked, creating dependencies where actions in one can significantly impact others.
  • The system is designed to incentivize user participation through mechanisms like staking, liquidity provision, and arbitrage opportunities, backed by reward systems.
Codebase Quality Analysis:
  1. Key Data Structures and Libraries

    • SafeERC20 is used across contracts for secure token handling.
    • ReentrancyGuard is employed in Staking and Upkeep to prevent reentrancy attacks.
  2. Use of Modifiers and Access Control

    • nonReentrant modifier is extensively used in Staking and Liquidity for functions involving token transfers.
    • ExchangeConfig exercises centralized control, which could be a potential point of failure.
  3. Use of State Variables

    • Immutable variables for contract addresses in ExchangeConfig enhance security and gas efficiency.
    • Staking and Liquidity use state variables to track user stakes and liquidity shares, respectively.
  4. Use of Events and Logging

    • Extensive use of events in Staking and SaltRewards ensures transparency in transactions.
  5. Key Functions that need special attention

  6. Upgradability

    • The contracts are non-upgradable, emphasizing the need for comprehensive testing and audits.
Architecture Recommendations:
  • Implement decentralized governance mechanisms to mitigate centralization risks.
  • Regular security audits and real-time monitoring are recommended for maintaining system integrity.
Centralization Risks:
  • Centralized control in ExchangeConfig and other administrative contracts.
Mechanism Review:
  • The staking and liquidity mechanisms in Staking and Liquidity should be reviewed for efficiency and fairness.
  • The ArbitrageSearch contract's role in identifying profitable arbitrage opportunities within the ecosystem needs careful analysis to ensure it aligns with the overall platform's risk and reward balance.
Systemic Risks:
  • The interdependence of contracts could lead to cascading failures in the event of critical bugs.
Areas of Concern:
  • Error handling in Upkeep during maintenance operations could be improved.
  • Inefficiencies in token swap mechanisms in Liquidity need addressing.
Codebase Analysis:

Staking.sol

  • Functions and Risks
    • 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.

Liquidity.sol

  • Functions and Risks
    • _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.

Upkeep.sol

  • Functions and Risks
    • performUpkeep: Inefficient execution of maintenance tasks leading to increased gas costs and potential errors. Optimize upkeep functions for efficiency and error handling.

ExchangeConfig.sol

  • Functions and Risks
    • 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.

SaltRewards.sol

  • Functions and Risks
    • 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.

StakingRewards.sol

  • Functions and Risks
    • _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.

DAO.sol

  • Functions and Risks
    • 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.

Airdrop.sol

  • Functions and Risks
    • claimAirdrop: Exploitation of the airdrop mechanism. Implement stringent eligibility checks and anti-fraud measures.
Contract Dynamics:
  • Staking.sol and StakingRewards.sol: Staking manages user stakes and interacts with StakingRewards for distributing rewards. Changes in staking directly affect reward calculations.
  • Liquidity.sol and Pools.sol: Liquidity handles user contributions to liquidity pools, interacting with Pools for pool management. Liquidity adjustments impact pool dynamics and reward distributions.
  • DAO.sol and Proposals.sol: DAO facilitates governance, with Proposals managing proposal submissions and voting. Governance decisions can alter system parameters across contracts.
  • ExchangeConfig.sol and AccessManager.sol: ExchangeConfig centralizes configuration settings, with AccessManager controlling access rights. Modifications in ExchangeConfig can cascade through the ecosystem.
  • Upkeep.sol and CollateralAndLiquidity.sol: Upkeep performs routine maintenance, impacting CollateralAndLiquidity's liquidation and collateral management processes.
  • SaltRewards.sol and Emissions.sol: SaltRewards distributes rewards, relying on Emissions for reward generation. Reward policies influence user incentives and platform participation.
  • ArbitrageSearch.sol: ArbitrageSearch plays a crucial role in identifying profitable arbitrage opportunities across the platform's token pairs. Its findings can influence trading strategies and liquidity dynamics within the ecosystem.
Recommendations:
  • Use Tenderly and Defender for continued monitoring to prevent unforeseen risks or actions.
  • Implement decentralized governance to reduce reliance on centralized ExchangeConfig.
  • Continue regular security audits, especially for StakingRewards and Liquidity, to ensure robustness against exploits.
  • Enhance error handling in Upkeep to prevent failures during maintenance operations.
  • Optimize token swap logic in Liquidity to prevent imbalances and inefficiencies.
  • Continuous monitoring using tools like Tenderly and Defender for proactive risk management.
  • Stress testing inter-contract interactions to identify and mitigate systemic risks.
Conclusion:

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.

Time spent:

30 hours

#0 - c4-judge

2024-02-03T14:55:23Z

Picodes marked the issue as grade-b

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