Salty.IO - sivanesh_808'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: 122/178

Findings: 2

Award: $32.48

🌟 Selected for report: 0

🚀 Solo Findings: 0

S.NoTitleContract Name
L-01Misaligned Emissions Rate CalculationEmissions.sol
L-02Potential Precision Loss in _zapSwapAmount CalculationPoolMath.sol
L-03Division Before Validation in Arbitrage Profit DistributionPoolStats.sol
L-04Fund Loss Due to Inadequate Handling of Dust in _placeInternalSwapPoolUtils.sol
L-05Precision Loss in Price AggregationPriceAggregator.sol
L-06Risk of Fund Loss in _sendInitialLiquidityRewardsSaltRewards.sol
L-07Risk of Fund Loss in _bisectionSearch FunctionArbitrageSearch.sol

[L-01] Misaligned Emissions Rate Calculation

Contract Name: Emissions.sol

Description:

The performUpkeep function in the Emissions contract calculates the amount of SALT to distribute based on time elapsed and the emissionsWeeklyPercentTimes1000 configuration. However, the calculation may suffer from a logic error due to how Solidity handles integer division, potentially leading to incorrect (usually lower) emissions than intended.

Code Snippet:

function performUpkeep(uint256 timeSinceLastUpkeep) external {
    ...
    uint256 saltToSend = (saltBalance * timeSinceLastUpkeep * rewardsConfig.emissionsWeeklyPercentTimes1000()) / (100 * 1000 weeks);
    ...
}

Expected Behavior:

The function should accurately calculate the amount of SALT to send based on the percentage specified by emissionsWeeklyPercentTimes1000. The expected result is a proportional distribution of tokens according to the elapsed time and the set weekly emission rate.

Actual Behavior:

Due to the use of integer division, the multiplication of saltBalance, timeSinceLastUpkeep, and rewardsConfig.emissionsWeeklyPercentTimes1000() might result in a value that, when divided by (100 * 1000 weeks), could produce a truncated result. This truncation occurs because Solidity does not handle fractional numbers, and any remainder after division is discarded. This could result in consistently lower emissions than expected, especially when the saltBalance is low or the timeSinceLastUpkeep is short.

Scenario Setup:

  1. Initial Conditions:

    • saltBalance (total SALT tokens in the contract): 100,000 ether (using ether as a unit for simplicity).
    • timeSinceLastUpkeep: 3 days.
    • emissionsWeeklyPercentTimes1000: 50 (representing a weekly emission rate of 0.05%).
  2. Calculation Process: The amount of SALT to send (saltToSend) is calculated in the contract as follows: [ saltToSend = (saltBalance * timeSinceLastUpkeep * emissionsWeeklyPercentTimes1000) / (100 * 1000 * 1 week) ]

Expected Calculation:

  1. Convert Time Units: Convert 3 days into a fraction of a week: 3 days / 7 days = 0.42857 weeks.
  2. Calculate saltToSend (Expected):
    • Using decimal arithmetic for precision: [ saltToSend = (100,000 * 0.42857 * 50) / (100 * 1000) ] [ saltToSend ≈ 214.285 ] (SALT tokens)

Actual Calculation (With Solidity Integer Arithmetic):

  1. Integer Arithmetic in Solidity: Solidity truncates decimals in integer division.
  2. Calculate saltToSend (Actual in Solidity):
    • Without decimal places: [ saltToSend = (100,000 * 3 days * 50) / (100 * 1000 * 7 days) ] [ saltToSend = (15,000,000) / 700,000 ] [ saltToSend = 21 ] (SALT tokens, as Solidity truncates the decimal part)

Analysis:

  • Expected Behavior: 214.285 SALT tokens should be emitted.
  • Actual Behavior in Solidity: Only 21 SALT tokens are emitted due to integer division truncation.
  • Discrepancy: This results in a significant underestimation of the emitted tokens. The contract emits far less than intended due to the truncation inherent in Solidity's integer arithmetic.

Github : Emissions.sol


[L-02] Potential Precision Loss in _zapSwapAmount Calculation

Contract Name: PoolMath.sol

Description:

In the _zapSwapAmount function of the PoolMath library, there's a complex calculation involving square roots and division to determine the swap amount for liquidity addition. The use of integer division and square roots can lead to precision loss, especially when dealing with large numbers or numbers that do not divide evenly.

Code Snippet:

// ... within _zapSwapAmount function
uint256 sqrtDiscriminant = Math.sqrt(discriminant);
swapAmount = (sqrtDiscriminant - B) / (2 * A);

Expected Behavior:

The function is expected to accurately compute the swap amount needed to balance the liquidity addition based on the reserves in the pool and the amounts to be zapped. This calculation should be precise to ensure the correct amount of tokens are swapped, maintaining the intended liquidity ratios.

Actual Behavior:

Due to Solidity's handling of integer division and square roots, the result of the calculation may be imprecise. The division (sqrtDiscriminant - B) / (2 * A) could result in a truncated outcome, and the square root operation might not yield an exact result for non-perfect squares. This imprecision could lead to slightly off-balanced liquidity addition, potentially resulting in inefficiencies or minor losses in the intended liquidity ratios.

Scenario Setup:

  1. Initial Conditions:

    • reserve0 (reserve of token0): 10,000 units.
    • reserve1 (reserve of token1): 20,000 units.
    • zapAmount0 (amount of token0 to be zapped): 1,000 units.
    • zapAmount1 (amount of token1 to be zapped): 500 units.
    • A, B, C are constants derived from the quadratic equation in the contract comments.
  2. Calculation Process:

The swap amount of token0 (swapAmount) is calculated as follows: swapAmount = (-B + sqrt(B^2 - 4AC)) / 2A where: A = 1 B = 2 * reserve0 C = reserve0 * ((reserve0 * zapAmount1 - reserve1 * zapAmount0) / (reserve1 + zapAmount1))

Expected Calculation:

  1. Calculate Constants:

    • A = 1
    • B = 2 * 10,000 = 20,000
    • C = 10,000 * ((10,000 * 500 - 20,000 * 1,000) / (20,000 + 500))
    • C ≈ -47,619,048
  2. Calculate swapAmount (Expected Using Precise Arithmetic):

    • discriminant = B^2 - 4AC ≈ 400,000,000,000 + 190,476,192,000 ≈ 590,476,192,000
    • sqrtDiscriminant ≈ 768,734.89
    • swapAmount ≈ (768,734.89 - 20,000) / 2 ≈ 374,367.45 units of token0

Actual Calculation (With Solidity Integer Arithmetic):

  1. Integer Arithmetic in Solidity: Solidity rounds down to the nearest integer.
  2. Calculate swapAmount (Actual in Solidity):
    • sqrtDiscriminant = sqrt(590,476,192,000) ≈ 768,734 (rounded down)
    • swapAmount = (768,734 - 20,000) / 2 ≈ 374,367 units of token0

Analysis:

  • Expected Behavior: Approximately 374,367.45 units of token0 should be swapped.
  • Actual Behavior in Solidity: 374,367 units of token0 are swapped due to rounding down.
  • Discrepancy: This results in a slight underestimation of the swap amount by 0.45 units of token0 due to the rounding inherent in Solidity's integer arithmetic.

Github : PoolMath.sol


[L-03] Division Before Validation in Arbitrage Profit Distribution

Contract Name: PoolStats.sol

Description:

In the PoolStats contract, the function _calculateArbitrageProfits divides the arbitrageProfit by 3 before validating whether the pool indices (index1, index2, index3) are valid. This approach could lead to incorrect profit calculations, especially if one or more indices are invalid (i.e., equal to INVALID_POOL_ID).

Code Snippet:

function _calculateArbitrageProfits( bytes32[] memory poolIDs, uint256[] memory _calculatedProfits ) internal view {
    for( uint256 i = 0; i < poolIDs.length; i++ ) {
        ...
        uint256 arbitrageProfit = _arbitrageProfits[poolID] / 3;
        if ( arbitrageProfit > 0 ) {
            ArbitrageIndicies memory indicies = _arbitrageIndicies[poolID];

            if ( indicies.index1 != INVALID_POOL_ID )
                _calculatedProfits[indicies.index1] += arbitrageProfit;
            ...
        }
    }
}

Expected Behavior:

The function should first validate the pool indices (index1, index2, index3) to ensure they are valid before dividing the arbitrageProfit. This ensures that the division only occurs when the profits can be correctly attributed to valid pools.

Actual Behavior:

The arbitrageProfit is divided by 3 unconditionally before checking if the pool indices are valid. If any of these indices are invalid, the divided profits do not get correctly attributed, leading to potential discrepancies in profit distribution. This could result in some pools receiving less profit than they should, or in extreme cases, profits getting lost if all indices are invalid.

Scenario Setup:

  1. Initial Conditions:

    • Assume there are 3 pools involved in an arbitrage transaction.
    • Total arbitrageProfit generated by a specific arbitrage path: 300 units.
    • Pool indices for the arbitrage path (as stored in _arbitrageIndicies):
      • index1: 0 (valid index)
      • index2: INVALID_POOL_ID (indicating an invalid pool)
      • index3: 2 (valid index)
  2. Calculation of Profits:

    • The _calculateArbitrageProfits function will divide the arbitrageProfit by 3 and attempt to distribute it to the pools based on their indices.

Expected Calculation (Correct Logic):

  1. With Valid Pool Indices:

    • Ideally, the function should first check if each index is valid.
    • Only distribute profits to valid indices.
    • For our scenario, this means only index1 and index3 should receive profits.
  2. Expected Profit Distribution:

    • index1 profit: 150 units (half of 300, as only two indices are valid).
    • index3 profit: 150 units.

Actual Calculation (Current Logic in the Provided Code):

  1. Unconditional Division:

    • The function divides arbitrageProfit by 3 unconditionally.
    • Each index is supposed to receive 100 units (300 / 3).
  2. Actual Profit Distribution:

    • index1 (valid): Receives 100 units.
    • index2 (invalid): No distribution, but 100 units are still notionally allocated and thus "lost".
    • index3 (valid): Receives 100 units.

Analysis:

  • Expected Behavior: Only valid indices receive a share of the profits, and the total distributed profit should equal the total arbitrageProfit. In our scenario, this would mean 150 units each to index1 and index3.

  • Actual Behavior in Solidity: Due to the unconditional division, the total distributed profit is 200 units (100 each to index1 and index3), leading to a loss of 100 units due to the invalid index2. This results in a discrepancy in profit distribution and a loss of funds.

Github : PoolStats.sol


[L-04] Fund Loss Due to Inadequate Handling of Dust in _placeInternalSwap

Contract Name: PoolUtils.sol

Description:

In the PoolUtils library, the _placeInternalSwap function is designed to execute token swaps within the protocol. However, there's a potential issue with how the function handles very small amounts of tokens, termed as 'Dust'. The function does not check if the amountIn is below the defined DUST threshold, which could lead to situations where the amount is treated as negligible and not effectively swapped, potentially causing a loss of funds.

Code Snippet:

function _placeInternalSwap( ... ) internal returns (uint256 swapAmountIn, uint256 swapAmountOut) {
    if (amountIn == 0)
        return (0, 0);

    (uint256 reservesIn,) = pools.getPoolReserves(tokenIn, tokenOut);
    uint256 maxAmountIn = reservesIn * maximumInternalSwapPercentTimes1000 / (100 * 1000);
    if (amountIn > maxAmountIn)
        amountIn = maxAmountIn;

    swapAmountIn = amountIn;
    swapAmountOut = pools.depositSwapWithdraw(tokenIn, tokenOut, amountIn, 0, block.timestamp);
}

Expected Behavior:

The function should include a check for amountIn against the DUST threshold. If the amount is below this threshold, the function should either round up the amount to the minimum viable swap amount or handle it in a manner that prevents the loss of these tokens.

Actual Behavior:

Currently, if amountIn is a very small amount (but not zero), the function proceeds with the swap. Given that the amount is below the DUST threshold, it might be disregarded in the swap process due to its negligible size, leading to a situation where these tokens are effectively lost or stuck without contributing to any meaningful swap outcome.

Scenario Setup:

  • Initial Conditions:
    • reservesIn: 100,000 units
    • maximumInternalSwapPercentTimes1000: 5,000 (5%)
    • amountIn: 50 units
    • DUST threshold: 100 units

Expected Calculation (With Dust Check):

  1. Calculate maxAmountIn:

    • maxAmountIn = reservesIn * maximumInternalSwapPercentTimes1000 / 100,000
    • maxAmountIn = 100,000 * 5,000 / 100,000
    • maxAmountIn = 500,000,000 / 100,000
    • maxAmountIn = 5,000 units
  2. Check Against Dust:

    • amountIn (50 units) is compared to DUST (100 units).
    • Since amountIn < DUST, the function should handle this case specially.
  3. Expected Swap Execution:

    • The swap might not execute, or amountIn could be adjusted to DUST.
    • If adjusted to DUST, amountIn would be 100 units for the swap.

Actual Calculation (Without Dust Check):

  1. Calculate maxAmountIn:

    • This calculation remains the same as above.
    • maxAmountIn = 5,000 units
  2. No Dust Check:

    • amountIn is 50 units, which is below the DUST threshold but is used as-is because there's no dust check.
  3. Actual Swap Execution:

    • The swap proceeds with amountIn of 50 units.
    • There's no adjustment for being below DUST, so the small amount is used directly.

Result Comparison:

  • Expected Behavior (With Dust Check):

    • The swap either does not proceed or proceeds with a minimum of 100 units (DUST threshold) instead of the original 50 units.
    • This prevents the loss of funds due to very small swaps being ineffective.
  • Actual Behavior (Without Dust Check):

    • The swap proceeds with 50 units, despite being below the DUST threshold.
    • This could result in ineffective swaps where the small amount doesn't significantly impact the pool, potentially leading to a gradual loss of these tokens over multiple transactions.

Github : PoolUtils.sol


[L-05] Precision Loss in Price Aggregation

Contract Name: PriceAggregator.sol

Description:

The PriceAggregator contract, particularly in the _aggregatePrices function, calculates the average price from three different price feeds for BTC and ETH. However, the calculation method used to determine if the price sources are too far apart may suffer from precision loss, which could lead to incorrect rejection of valid prices.

Code Snippet:

function _aggregatePrices(uint256 price1, uint256 price2, uint256 price3) internal view returns (uint256) {
    ...
    uint256 averagePrice = (priceA + priceB) / 2;

    if ((_absoluteDifference(priceA, priceB) * 100000) / averagePrice > maximumPriceFeedPercentDifferenceTimes1000)
        return 0;

    return averagePrice;
}

Expected Behavior:

The function should accurately determine whether the closest two price feeds are within an acceptable range, defined by maximumPriceFeedPercentDifferenceTimes1000. This should be done with high precision to ensure that valid price pairs are not incorrectly discarded.

Actual Behavior:

Due to the division by averagePrice before multiplying by 100000, there is a risk of precision loss, especially when averagePrice is large. This could result in the condition incorrectly evaluating to true, causing the function to return 0 even when the price feeds are within the acceptable range.

Scenario Setup:

  1. Initial Conditions:

    • Three price feeds (price1, price2, price3) for BTC or ETH.
    • Example prices from the feeds: price1 = 50,000, price2 = 51,000, price3 = 55,000 (unit: USD).
    • maximumPriceFeedPercentDifferenceTimes1000: 2,000 (representing a maximum allowable difference of 2%).
  2. Calculation of Average and Difference Check:

    • The _aggregatePrices function calculates the average of the two closest prices and checks if their difference is within the allowable range.

Expected Calculation (With High Precision Arithmetic):

  1. Identify Closest Prices:

    • Closest prices are price1 and price2 (50,000 and 51,000).
  2. Calculate Average:

    • averagePrice = (50,000 + 51,000) / 2 = 101,000 / 2 = 50,500.
  3. Difference Check:

    • difference = |50,000 - 51,000| = 1,000.
    • Percentage difference: difference / averagePrice = 1,000 / 50,500 ≈ 0.0198 (approximately 1.98%).
    • Since 1.98% < 2%, the prices are considered valid.

Actual Calculation (With Solidity Integer Arithmetic):

  1. Same Closest Prices:

    • price1 and price2 are still the closest.
  2. Calculate Average:

    • averagePrice calculation remains the same: 50,500.
  3. Difference Check in Solidity:

    • difference calculation remains the same: 1,000.
    • The check for percentage difference in Solidity: (_absoluteDifference(priceA, priceB) * 100000) / averagePrice.
    • Calculating: (1,000 * 100,000) / 50,500 = 100,000,000 / 50,500 ≈ 1,980 (Solidity truncates the decimal).
    • Since this calculation is meant to represent a percentage multiplied by 1,000, the actual percentage difference is approximately 1.98%, which is below the 2% threshold.

Analysis:

  • Expected Behavior: The two closest price feeds are within the acceptable range (less than 2% difference), so the average price is valid and returned.

  • Actual Behavior in Solidity: The calculation also correctly identifies that the price difference is within the allowable range, and the average price is valid.

Github : PriceAggregator.sol


[L-06] Risk of Fund Loss in _sendInitialLiquidityRewards

Contract Name: SaltRewards.sol

Description:

In the SaltRewards contract, the function _sendInitialLiquidityRewards divides the liquidityBootstrapAmount evenly across all initial pools. However, if this division results in a fractional number of tokens that cannot be represented in Solidity's integer-based arithmetic, it can lead to a loss of tokens due to rounding down.

Code Snippet:

function _sendInitialLiquidityRewards(uint256 liquidityBootstrapAmount, bytes32[] memory poolIDs) internal {
    uint256 amountPerPool = liquidityBootstrapAmount / poolIDs.length; // Possible loss of tokens due to rounding

    AddedReward[] memory addedRewards = new AddedReward[](poolIDs.length);
    for (uint256 i = 0; i < addedRewards.length; i++) {
        addedRewards[i] = AddedReward(poolIDs[i], amountPerPool);
    }

    liquidityRewardsEmitter.addSALTRewards(addedRewards);
}

Expected Behavior:

The function should distribute liquidityBootstrapAmount in a way that ensures no tokens are lost. Ideally, any remaining tokens after the division should be allocated appropriately, perhaps to one of the pools or handled in another defined manner.

Actual Behavior:

Due to integer division, the amountPerPool calculation may truncate any fractional part of the token amount. When multiplied back by the number of pools, the total distributed amount can be less than liquidityBootstrapAmount, leading to a loss of tokens. For example, if liquidityBootstrapAmount is 1,000 and there are 3 pools, each pool receives 333 tokens, totaling 999 tokens distributed with 1 token effectively lost.

Scenario Setup:

  • Initial Conditions:
    • liquidityBootstrapAmount (Total SALT tokens to be distributed): 1,000 tokens.
    • poolIDs (Number of pools): 3 pools (e.g., pool1, pool2, pool3).

Expected Calculation (Ideal Handling of Token Distribution):

  1. Calculate Rewards Per Pool (Ideal):

    • Ideally, liquidityBootstrapAmount should be distributed evenly across the pools, with any remaining tokens allocated to prevent loss.
    • amountPerPool (Ideal) = liquidityBootstrapAmount / Number of Pools
    • amountPerPool (Ideal) = 1,000 tokens / 3 pools = 333.33 tokens per pool.
    • Since we can't distribute a fraction of a token, we round down to 333 tokens per pool and handle the remainder separately.
  2. Handle Remainder:

    • Total distributed without remainder: 333 tokens * 3 pools = 999 tokens.
    • Remainder: 1,000 tokens (total) - 999 tokens (distributed) = 1 token.
    • This remaining 1 token could be distributed to one of the pools or handled in another manner.

Actual Calculation (With Solidity's Integer Arithmetic):

  1. Calculate Rewards Per Pool (Solidity):

    • In Solidity, division truncates the decimal part, effectively rounding down.
    • amountPerPool (Solidity) = liquidityBootstrapAmount / Number of Pools
    • amountPerPool (Solidity) = 1,000 tokens / 3 pools = 333 tokens per pool (decimal part truncated).
  2. Total Distribution in Solidity:

    • Total distributed: 333 tokens * 3 pools = 999 tokens.
    • There's no mechanism to handle the remainder, so 1 token is effectively lost.

Analysis:

  • Expected Behavior (Ideal Handling): Evenly distribute tokens to each pool, and handle the remainder to ensure no tokens are lost. In this case, distribute 333 tokens to each pool and allocate the remaining 1 token appropriately.

  • Actual Behavior in Solidity: Distribute 333 tokens to each pool, resulting in a total of 999 tokens distributed. The remaining 1 token is not allocated due to Solidity's integer division, leading to its loss.

Github : SaltRewards.sol


[L-07] Risk of Fund Loss in _bisectionSearch Function

Contract Name: ArbitrageSearch.sol

Description:

The _bisectionSearch function in the ArbitrageSearch contract is designed to find the optimal amount for an arbitrage opportunity. However, there's a potential issue in the way this function handles the search range and the midpoint calculation, which might lead to an inaccurate determination of the optimal arbitrage amount, potentially resulting in a less profitable or even unprofitable arbitrage transaction.

Code Snippet:

function _bisectionSearch( ... ) internal pure returns (uint256 bestArbAmountIn) {
    ...
    uint256 leftPoint = swapAmountInValueInETH >> 7;
    uint256 rightPoint = swapAmountInValueInETH + (swapAmountInValueInETH >> 2);
    ...
    for( uint256 i = 0; i < 8; i++ ) {
        uint256 midpoint = (leftPoint + rightPoint) >> 1;
        ...
    }
    ...
    bestArbAmountIn = (leftPoint + rightPoint) >> 1;
    ...
}

Expected Behavior:

The function should accurately determine the optimal arbitrage amount that maximizes profit. The bisection search algorithm used should efficiently converge to the most profitable point within the defined search range.

Actual Behavior:

The implementation of the bisection search might not accurately find the optimal arbitrage amount due to how the range and midpoint are calculated. The function calculates the initial search range based on a fraction and a percentage of swapAmountInValueInETH, and then repeatedly bisects this range. However, if the actual optimal amount for arbitrage lies outside of this initial range or near its boundaries, the function may miss it, leading to suboptimal arbitrage decisions. This could result in either lower profits or, in some cases, unprofitable transactions if transaction costs are considered.

Scenario Setup:

  1. Initial Conditions:

    • swapAmountInValueInETH: Let's assume it's 1,000 ETH.
    • Reserves for pools A, B, and C are assumed to be large enough not to influence the calculations significantly in this example.
  2. Bisection Search Range:

    • Initial leftPoint = swapAmountInValueInETH / 128 = 1,000 / 128 ≈ 7.81 ETH.
    • Initial rightPoint = swapAmountInValueInETH + (swapAmountInValueInETH / 4) = 1,000 + 250 = 1,250 ETH.
  3. Assumption for Optimal Arbitrage Amount:

    • Let's assume the actual optimal arbitrage amount is 500 ETH, which lies within the initial range.

Expected Calculation (Ideal Arbitrage Determination):

  1. Ideal Arbitrage Search:

    • An ideal search algorithm would accurately converge to the optimal point, 500 ETH, efficiently.
    • It would iteratively adjust the search range to zero in on 500 ETH.
  2. Expected Outcome:

    • The algorithm identifies 500 ETH as the optimal amount for arbitrage.

Actual Calculation (Bisection Search Algorithm):

  1. First Iteration:

    • Midpoint = (7.81 + 1,250) / 2 ≈ 628.91 ETH.
    • If the right of this midpoint is more profitable, the next left point would be 628.91 ETH.
  2. Subsequent Iterations:

    • The algorithm continues to bisect the range, but the accuracy of finding the exact optimal point (500 ETH) depends on the range adjustments and the number of iterations.
  3. Final Outcome:

    • The algorithm converges to a point close to 500 ETH, but the exactness depends on the number of iterations and how the profit comparison is made at each step.

Analysis:

  • Expected Behavior: The optimal arbitrage amount is identified accurately, maximizing the profit.

  • Actual Behavior in Solidity: The bisection search algorithm may approximate the optimal amount but may not precisely pinpoint 500 ETH due to the initial range and the method of determining the more profitable side of the midpoint.

Github : ArbitrageSearch.sol


#0 - c4-judge

2024-02-03T13:17:56Z

Picodes marked the issue as grade-b

Findings Information

Awards

20.7932 USDC - $20.79

Labels

bug
G (Gas Optimization)
grade-b
G-07

External Links

Contest : salty[GAS]

S.NoTitleContract Name
G-01Optimization Report for getPriceBTC and getPriceETH Functions in CoreSaltyFeed ContractCoreSaltyFeed.sol
G-02Optimization of latestChainlinkPrice Function in CoreChainlinkFeed ContractCoreChainlinkFeed.sol
G-03Optimization Analysis of _aggregatePrices Function in the Price Aggregator ContractPriceAggregatorGasTest.sol
G-04Optimization of the _placeInternalSwap Function in the PoolUtils LibraryPoolUtils.sol
G-05Optimization of totalVotesCastForBallot in Proposals ContractProposals.sol
G-06Optimization of Unnecessary if Statements for Zero ChecksRewardsEmitter.sol
G-07Optimization of PoolsConfig ContractPoolsConfig.sol
G-08Enhanced Gas Efficiency in changeMaximumWhitelistedPools FunctionPoolsConfig.sol
G-09Enhanced Gas Efficiency in changeMaximumInternalSwapPercentTimes1000 FunctionPoolsConfig.sol
G-10Optimization of walletHasAccess Function in ExchangeConfig ContractExchangeConfig.sol
G-11Optimization of changeBootstrappingRewards in DAOConfig ContractDAOConfig.sol
G-12Arithmetic Operations Optimization in Solidity Smart ContractCollateralAndLiquidity.sol.sol
G-13Optimization of State Variable Access in Solidity Smart ContractCollateralAndLiquidity.sol
G-14Combine Increment and Decrement Operations in StakingConfig ContractStakingConfig.sol
G-15Gas Efficiency Improvement in getTwapWETH FunctionCoreUniswapFeed.sol

[G-01] Optimization Report for getPriceBTC and getPriceETH Functions in CoreSaltyFeed Contract

Contract Name:

CoreSaltyFeed.sol

Description:

The CoreSaltyFeed contract is designed for price retrieval of BTC and ETH in a decentralized finance (DeFi) context. This report addresses the gas optimization of both getPriceBTC and getPriceETH functions. These functions calculate the respective prices based on liquidity pool reserves. The aim is to enhance gas efficiency without compromising the core logic and output.

Original Functions:

getPriceBTC Function:

function getPriceBTC() external view returns (uint256) {
    (uint256 reservesWBTC, uint256 reservesUSDS) = pools.getPoolReserves(wbtc, usds);
    if ((reservesWBTC < PoolUtils.DUST) || (reservesUSDS < PoolUtils.DUST)) {
        return 0;
    }
    return (reservesUSDS * 10**8) / reservesWBTC;
}

getPriceETH Function:

function getPriceETH() external view returns (uint256) {
    (uint256 reservesWETH, uint256 reservesUSDS) = pools.getPoolReserves(weth, usds);
    if ((reservesWETH < PoolUtils.DUST) || (reservesUSDS < PoolUtils.DUST)) {
        return 0;
    }
    return (reservesUSDS * 10**18) / reservesWETH;
}

Optimized Functions:

Common Optimizations for Both Functions:

  1. Caching State Variables in Memory:

    • State variables (wbtc, weth, usds) are cached in memory to reduce gas costs associated with state variable accesses.
  2. Unchecked Division:

    • The division operation is placed inside an unchecked block to avoid unnecessary overflow/underflow checks, thereby saving gas.
  3. Precomputed Constants:

    • Constants (10**8 for BTC, 10**18 for ETH) are precomputed and stored to minimize repetitive calculations.

Optimized getPriceBTC:

uint256 private constant DECIMAL_FACTOR_BTC = 10**8;

function getPriceBTC() external view returns (uint256) {
    IERC20 memory _wbtc = wbtc;
    IERC20 memory _usds = usds;
    (uint256 reservesWBTC, uint256 reservesUSDS) = pools.getPoolReserves(_wbtc, _usds);
    if ((reservesWBTC < PoolUtils.DUST) || (reservesUSDS < PoolUtils.DUST)) {
        return 0;
    }
    unchecked {
        return (reservesUSDS * DECIMAL_FACTOR_BTC) / reservesWBTC;
    }
}

Optimized getPriceETH:

uint256 private constant DECIMAL_FACTOR_ETH = 10**18;

function getPriceETH() external view returns (uint256) {
    IERC20 memory _weth = weth;
    IERC20 memory _usds = usds;
    (uint256 reservesWETH, uint256 reservesUSDS) = pools.getPoolReserves(_weth, _usds);
    if ((reservesWETH < PoolUtils.DUST) || (reservesUSDS < PoolUtils.DUST)) {
        return 0;
    }
    unchecked {
        return (reservesUSDS * DECIMAL_FACTOR_ETH) / reservesWETH;
    }
}

Github : CoreSaltyFeed.sol


[G-02] Optimization of latestChainlinkPrice Function in CoreChainlinkFeed Contract

Contract Name:

CoreChainlinkFeed.sol

Description:

The latestChainlinkPrice function in the CoreChainlinkFeed contract retrieves the latest price data from a Chainlink oracle. The original implementation included multiple checks and assignments, resulting in higher gas costs. The optimized version streamlines these processes to reduce gas usage while maintaining the functionality of fetching and validating the latest Chainlink oracle data.

Original Code:

function latestChainlinkPrice(AggregatorV3Interface chainlinkFeed) public view returns (uint256) {
    int256 price = 0;

    try chainlinkFeed.latestRoundData()
    returns (
        uint80, // _roundID
        int256 _price,
        uint256, // _startedAt
        uint256 _answerTimestamp,
        uint80 // _answeredInRound
    )
        {
        uint256 answerDelay = block.timestamp - _answerTimestamp;

        if ( answerDelay <= MAX_ANSWER_DELAY )
            price = _price;
        else
            price = 0;
        }
    catch (bytes memory) {
        // In case of failure, price will remain 0
    }

    if ( price < 0 )
        return 0;

    return uint256(price) * 10**10;
}

Optimized Code:

function latestChainlinkPrice(AggregatorV3Interface chainlinkFeed) public view returns (uint256) {
    try chainlinkFeed.latestRoundData()
    returns (
        uint80, // _roundID
        int256 _price,
        uint256, // _startedAt
        uint256 _answerTimestamp,
        uint80 // _answeredInRound
    )
        {
        if (block.timestamp - _answerTimestamp > MAX_ANSWER_DELAY || _price < 0)
            return 0;

        return uint256(_price) * 10**10;
        }
    catch (bytes memory) {
        return 0;
    }
}

Explanation of Optimization:

  • Consolidated Conditionals: The optimized code combines the checks for answer delay and positive price into one conditional statement. This reduces the number of operations, as it eliminates the need for a separate variable assignment and conditional check.

  • Direct Return Strategy: Instead of storing the price in a temporary variable and then performing checks, the optimized code directly returns the result. This streamlines the function and reduces the number of assignments and storage operations, leading to lower gas costs.

  • Simplified Error Handling: The catch block remains unchanged but benefits indirectly from the streamlined logic, as it now deals with a simplified logic flow.

Github : CoreChainlinkFeed.sol


[G-03] Optimization Analysis of _aggregatePrices Function in the Price Aggregator Contract

Contract Name:

PriceAggregator.sol

Description:

The _aggregatePrices function within the PriceAggregatorGasTest contract is designed to aggregate prices from three distinct sources, ensuring reliability by eliminating outliers. This function is crucial for providing accurate price data in scenarios where one or more sources might be unreliable. The original implementation uses conditional logic and arithmetic operations that, while functional, offer room for gas consumption optimization. The optimized version of the function aims to reduce gas costs by simplifying the logic and reducing the complexity of calculations.

Original Code:

function _aggregatePrices(uint256 price1, uint256 price2, uint256 price3) internal view returns (uint256) {
    uint256 numNonZero;
    if (price1 > 0) numNonZero++;
    if (price2 > 0) numNonZero++;
    if (price3 > 0) numNonZero++;

    if (numNonZero < 2) return 0;

    uint256 diff12 = _absoluteDifference(price1, price2);
    uint256 diff13 = _absoluteDifference(price1, price3);
    uint256 diff23 = _absoluteDifference(price2, price3);

    uint256 priceA;
    uint256 priceB;
    if ((diff12 <= diff13) && (diff12 <= diff23)) (priceA, priceB) = (price1, price2);
    else if ((diff13 <= diff12) && (diff13 <= diff23)) (priceA, priceB) = (price1, price3);
    else (priceA, priceB) = (price2, price3);

    uint256 averagePrice = (priceA + priceB) / 2;

    if ((_absoluteDifference(priceA, priceB) * 100000) / averagePrice > maximumPriceFeedPercentDifferenceTimes1000) return 0;

    return averagePrice;
}

Optimized Code:

function _aggregatePricesOptimized(uint256 price1, uint256 price2, uint256 price3) internal view returns (uint256) {
    uint256[3] memory prices = [price1, price2, price3];
    uint256 numNonZero = (price1 != 0 ? 1 : 0) + (price2 != 0 ? 1 : 0) + (price3 != 0 ? 1 : 0);

    if (numNonZero < 2) return 0;

    // Sort prices in ascending order
    if (prices[0] > prices[1]) (prices[0], prices[1]) = (prices[1], prices[0]);
    if (prices[1] > prices[2]) (prices[1], prices[2]) = (prices[2], prices[1]);
    if (prices[0] > prices[1]) (prices[0], prices[1]) = (prices[1], prices[0]);

    uint256 averagePrice = (prices[0] + prices[1]) / 2;

    if ((prices[1] - prices[0]) * 100000 / averagePrice > maximumPriceFeedPercentDifferenceTimes1000) return 0;

    return averagePrice;
}

Optimization Explanation:

  • Simplification of Non-Zero Counting: The optimized code employs a more compact approach for counting non-zero prices, reducing the number of conditional checks.

  • Array and Sorting: By storing prices in an array and sorting them, the optimized version efficiently identifies the two closest prices without multiple conditional checks. This method streamlines the process of discarding the outlier and calculating the average of the two closest prices.

  • Reduced Conditional Checks: The sorting algorithm reduces the need for multiple conditional checks to find the two closest prices, which simplifies the logic and potentially saves gas.

  • Direct Average Calculation: After sorting, the optimized code directly averages the first two elements of the sorted array (the two closest prices), eliminating the need for additional logic to select these values.

Github : PriceAggregator.sol


[G-04] Optimization of the _placeInternalSwap Function in the PoolUtils Library

Contract Name:

PoolUtils.sol

Description:

The _placeInternalSwap function is designed to execute token swaps within a protocol, with the swap amount limited to a specific percentage of the token reserves. This function is part of a broader strategy to mitigate the impact of sandwich attacks by limiting the size of swaps. The original code performs a series of checks and calculations to determine the maximum allowed swap amount. The optimized version aims to simplify these calculations and improve the overall gas efficiency of the function.

Original Code:

function _placeInternalSwap(IPools pools, IERC20 tokenIn, IERC20 tokenOut, uint256 amountIn, uint256 maximumInternalSwapPercentTimes1000) internal returns (uint256 swapAmountIn, uint256 swapAmountOut) {
    if (amountIn == 0)
        return (0, 0);

    (uint256 reservesIn,) = pools.getPoolReserves(tokenIn, tokenOut);

    uint256 maxAmountIn = reservesIn * maximumInternalSwapPercentTimes1000 / (100 * 1000);
    if (amountIn > maxAmountIn)
        amountIn = maxAmountIn;

    swapAmountIn = amountIn;
    swapAmountOut = pools.depositSwapWithdraw(tokenIn, tokenOut, amountIn, 0, block.timestamp);
}

Optimized Code:

function _placeInternalSwapOptimized(IPools pools, IERC20 tokenIn, IERC20 tokenOut, uint256 amountIn, uint256 maximumInternalSwapPercentTimes1000) internal returns (uint256 swapAmountIn, uint256 swapAmountOut) {
    if (amountIn == 0) {
        return (0, 0);
    }

    (uint256 reservesIn,) = pools.getPoolReserves(tokenIn, tokenOut);

    // Simplify the division calculation
    uint256 maxAmountIn = (reservesIn * maximumInternalSwapPercentTimes1000) / 100000;

    // Use the min function to determine the swap amount
    swapAmountIn = Math.min(amountIn, maxAmountIn);

    // Only perform the swap if there is an amount to swap
    if (swapAmountIn > 0) {
        swapAmountOut = pools.depositSwapWithdraw(tokenIn, tokenOut, swapAmountIn, 0, block.timestamp);
    } else {
        swapAmountOut = 0;
    }

    return (swapAmountIn, swapAmountOut);
}

Optimization Explanation:

  • Simplified Division Calculation: The optimized code simplifies the division to a single operation by dividing directly by 100000 instead of (100 * 1000). This change reduces the complexity of the arithmetic operation and can potentially save gas.

  • Usage of Math.min Function: The optimized version uses Math.min to determine the smaller value between amountIn and maxAmountIn. This approach is more expressive and can potentially be more gas-efficient than conditional statements.

  • Conditional Swap Execution: The swap is only executed if there is a non-zero amount to swap (swapAmountIn > 0). This check avoids unnecessary calls to pools.depositSwapWithdraw when no swap occurs, potentially saving gas.

  • Explicit Zero Assignment for swapAmountOut: In cases where no swap takes place, swapAmountOut is explicitly set to zero, ensuring clarity in the function's behavior.

Github : PoolUtils.sol


[G-05] Optimization of totalVotesCastForBallot in Proposals Contract

Contract Name: Proposals.sol

Description: The totalVotesCastForBallot function calculates the total number of votes cast for a specific ballot. The optimization focuses on reducing storage access by accessing the _votesCastForBallot mapping directly in the return statement and using a ternary operator for concise logic. The goal is to save gas by minimizing storage operations and streamlining the code.

Original Code:

function totalVotesCastForBallot(uint256 ballotID) public view returns (uint256) {
    mapping(Vote => uint256) storage votes = _votesCastForBallot[ballotID];

    Ballot memory ballot = ballots[ballotID];
    if (ballot.ballotType == BallotType.PARAMETER) {
        return votes[Vote.INCREASE] + votes[Vote.DECREASE] + votes[Vote.NO_CHANGE];
    } else {
        return votes[Vote.YES] + votes[Vote.NO];
    }
}

Optimized Code:

function totalVotesCastForBallotOptimized(uint256 ballotID) public view returns (uint256) {
    Ballot storage ballot = ballots[ballotID];
    return ballot.ballotType == BallotType.PARAMETER
           ? _votesCastForBallot[ballotID][Vote.INCREASE] + _votesCastForBallot[ballotID][Vote.DECREASE] + _votesCastForBallot[ballotID][Vote.NO_CHANGE]
           : _votesCastForBallot[ballotID][Vote.YES] + _votesCastForBallot[ballotID][Vote.NO];
}

Optimization Explanation:

  1. Direct Storage Access: The optimized function directly accesses the _votesCastForBallot mapping in the return statement, which can potentially reduce gas costs by avoiding an extra local variable for storage mapping.

  2. Use of Ternary Operator: The ternary operator (?:) condenses the conditional logic, making the code more readable and concise. While this may not directly impact gas savings, it makes the function more straightforward.

  3. Efficiency in Logic Execution: By combining the logic into a single line with direct mapping access, the optimized code reduces the overhead associated with memory and storage operations, which is beneficial for gas efficiency.

Github : Proposals.sol


[G-06] Optimization of Unnecessary if Statements for Zero Checks

Contract Name: RewardsEmitter.sol

Description:

In the smart contract RewardsEmitter, there's an if statement used to check whether the variable sum is greater than zero before executing the safeTransferFrom function. This check is redundant since the ERC-20 token standard's transfer and transferFrom functions inherently revert if the transfer amount is zero, thus making an explicit check for zero transfer amounts unnecessary. Removing this check can save gas by reducing the overall bytecode size and the execution cost of the contract.

Original Code:

// In function addSALTRewards
if (sum > 0)
    salt.safeTransferFrom(msg.sender, address(this), sum);

Optimized Code:

// In function addSALTRewards
salt.safeTransferFrom(msg.sender, address(this), sum);

Optimization Explanation:

The original code includes an if statement to check if sum is greater than zero before calling salt.safeTransferFrom. This check is not necessary for two reasons:

  1. ERC-20 Standard Compliance: The ERC-20 standard's transfer and transferFrom functions must revert in case of a failure, which includes a transfer of zero tokens when not allowed. Therefore, the ERC-20 token contract for SALT should inherently handle the case where the transfer amount is zero.

  2. Gas Efficiency: Removing unnecessary conditionals reduces the contract's bytecode size, leading to lower deployment and execution costs. The gas cost for the conditional check and the additional jump instruction can be saved.

Github : RewardsEmitter.sol


[G-07] Optimization of PoolsConfig Contract

Contract Name: PoolsConfig.sol

Description: This report presents a gas optimization opportunity in the PoolsConfig contract. The focus is on the tokenHasBeenWhitelisted function, which checks if a token has been whitelisted with WBTC and WETH. The current implementation involves separate checks for each pair, leading to potential inefficiencies.

Original Code:

function tokenHasBeenWhitelisted( IERC20 token, IERC20 wbtc, IERC20 weth ) external view returns (bool) {
    bytes32 poolID1 = PoolUtils._poolID( token, wbtc );
    if ( isWhitelisted(poolID1) )
        return true;

    bytes32 poolID2 = PoolUtils._poolID( token, weth );
    if ( isWhitelisted(poolID2) )
        return true;

    return false;
}

Optimized Code:

function tokenHasBeenWhitelisted( IERC20 token, IERC20 wbtc, IERC20 weth ) external view returns (bool) {
    bytes32 poolID1 = PoolUtils._poolID( token, wbtc );
    bytes32 poolID2 = PoolUtils._poolID( token, weth );
    return isWhitelisted(poolID1) || isWhitelisted(poolID2);
}

Optimization Explanation:

  • Consolidated Logic: The optimized version combines the checks for whitelisting with WBTC and WETH into a single return statement using the logical OR operator (||). This reduces the number of explicit if checks and streamlines the function's execution.

  • Reduced Gas Cost: By combining the checks into one line, the function minimizes the operations performed. This could lead to reduced gas consumption, especially if the isWhitelisted check is not overly complex or gas-intensive.

  • Maintained Functionality: The core logic and output of the function remain unchanged, ensuring that the optimization does not impact the intended functionality of the contract.

Github : PoolsConfig.sol


[G-08] Enhanced Gas Efficiency in changeMaximumWhitelistedPools Function

Contract Name: PoolsConfig.sol

Description: This report details the gas optimization achieved in the PoolsConfig contract by refactoring the changeMaximumWhitelistedPools function. The focus was on simplifying the logic to adjust the maximum number of whitelisted pools, aiming to reduce gas consumption while maintaining the function's core purpose and constraints.

Original Code:

function changeMaximumWhitelistedPoolsOriginal(bool increase) external {
    if (increase) {
        if (maximumWhitelistedPools < 100)
            maximumWhitelistedPools += 10;
    } else {
        if (maximumWhitelistedPools > 20)
            maximumWhitelistedPools -= 10;
    }
    emit MaximumWhitelistedPoolsChanged(maximumWhitelistedPools);
}

Optimized Code:

function changeMaximumWhitelistedPoolsRevised(bool increase) external {
    uint256 newMaxPools = maximumWhitelistedPools;

    if (increase) {
        newMaxPools = newMaxPools < 90 ? newMaxPools + 10 : 100;
    } else {
        newMaxPools = newMaxPools > 30 ? newMaxPools - 10 : 20;
    }

    if (newMaxPools != maximumWhitelistedPools) {
        maximumWhitelistedPools = newMaxPools;
        emit MaximumWhitelistedPoolsChanged(newMaxPools);
    }
}

Optimization Explanation:

  • Streamlined Calculations: The revised function optimizes the calculation of newMaxPools using simpler conditional logic. This reduces the operations executed in the Ethereum Virtual Machine (EVM), leading to lower gas consumption.

  • Effective Range Checks: The function now ensures that maximumWhitelistedPools stays within the specified range (20 to 100) more efficiently. The updated range checks are concise, reducing the complexity of the function.

  • Conditional Event Emission: The event MaximumWhitelistedPoolsChanged is emitted only if there's an actual change in the value, preventing unnecessary gas consumption due to event logging when no change occurs.

Github : PoolsConfig.sol


[G-9] Enhanced Gas Efficiency in changeMaximumInternalSwapPercentTimes1000 Function

Contract Name: PoolsConfig.sol

Description: This report outlines the successful gas optimization for the changeMaximumInternalSwapPercentTimes1000 function within the PoolsConfig smart contract. The primary goal was to streamline the function to reduce gas consumption while maintaining its core functionality - adjusting the maximum internal swap percent with a precision of 1000.

Original Code:

function changeMaximumInternalSwapPercentTimes1000(bool increase) external {
    if (increase) {
        if (maximumInternalSwapPercentTimes1000 < 2000)
            maximumInternalSwapPercentTimes1000 += 250;
    } else {
        if (maximumInternalSwapPercentTimes1000 > 250)
            maximumInternalSwapPercentTimes1000 -= 250;
    }
    emit MaximumInternalSwapPercentTimes1000Changed(maximumInternalSwapPercentTimes1000);
}

Optimized Code:

function changeMaximumInternalSwapPercentTimes1000FurtherRevised(bool increase) external {
    uint256 newMaxSwap = maximumInternalSwapPercentTimes1000;

    if (increase && newMaxSwap < 2000) {
        newMaxSwap += 250;
    } else if (!increase && newMaxSwap > 250) {
        newMaxSwap -= 250;
    }

    if (newMaxSwap != maximumInternalSwapPercentTimes1000) {
        maximumInternalSwapPercentTimes1000 = newMaxSwap;
        emit MaximumInternalSwapPercentTimes1000Changed(newMaxSwap);
    }
}

Optimization Explanation:

  • Simplified Conditional Logic: The revised function uses straightforward if-else blocks, reducing the complexity of the logic. This simplification aims to decrease the computational steps required, potentially leading to lower gas usage.
  • Direct Adjustment Application: Adjustments to the maximumInternalSwapPercentTimes1000 variable are made directly within the if-else blocks. This approach minimizes the operations performed by the function.
  • Conditional State Update and Event Emission: The contract's state is updated, and the event is emitted only if there is a change in the value of maximumInternalSwapPercentTimes1000. This conditional check avoids unnecessary gas expenditure associated with state changes and event logging when no actual change occurs.

Github : PoolsConfig.sol


[G-10] Optimization of walletHasAccess Function in ExchangeConfig Contract

Contract Name: ExchangeConfig.sol

Description: This report details the optimization of the walletHasAccess function within the ExchangeConfig smart contract. The function's primary role is to determine whether a given wallet address has access rights within the protocol. This optimization aims to streamline the conditional checks within the function to improve gas efficiency.

Original Code:

function walletHasAccess(address wallet) external view returns (bool) {
    if (wallet == dao) return true;
    if (wallet == airdrop) return true;

    return accessManager.walletHasAccess(wallet);
}

Optimized Code:

function walletHasAccessOptimized(address wallet) external view returns (bool) {
    return wallet == dao || wallet == airdrop || accessManager.walletHasAccess(wallet);
}

Optimization Explanation:

  • Streamlined Conditional Logic: The original version uses separate if statements to check for DAO and airdrop access. The optimized version condenses these checks into a single line using logical OR (||) operators. This reduces the bytecode size and simplifies the execution path, which can potentially save gas.
  • Direct Return of Result: The function now directly returns the result of the logical expression without separate return statements. This makes the code more concise and straightforward.
  • Maintaining Functional Integrity: The refactoring retains the original functionality of checking access rights for DAO, airdrop, and other wallets as per the AccessManager. The optimization solely focuses on improving the efficiency of these checks.

Github : ExchangeConfig.sol


[G-11] Optimization of changeBootstrappingRewards in DAOConfig Contract

Contract Name: DAOConfig.sol

Description: This report presents a gas optimization carried out on the changeBootstrappingRewards function in the DAOConfig smart contract. This contract, governed by a DAO, allows modification of various configuration parameters. The changeBootstrappingRewards function is used to adjust the amount of SALT provided as a bootstrapping reward. The optimization focuses on reducing gas consumption by simplifying the conditional logic.

Original Code:

function changeBootstrappingRewards(bool increase) external onlyOwner {
    if (increase) {
        if (bootstrappingRewards < 500000 ether)
            bootstrappingRewards += 50000 ether;
    } else {
        if (bootstrappingRewards > 50000 ether)
            bootstrappingRewards -= 50000 ether;
    }
    emit BootstrappingRewardsChanged(bootstrappingRewards);
}

Optimized Code:

function changeBootstrappingRewardsOptimized(bool increase) external onlyOwner {
    uint256 adjustment = 50000 ether;
    uint256 maxLimit = 500000 ether;
    uint256 minLimit = 50000 ether;

    uint256 newRewards = increase ? bootstrappingRewards + adjustment : bootstrappingRewards - adjustment;
    bootstrappingRewards = (increase && newRewards > maxLimit) || (!increase && newRewards < minLimit) 
                            ? bootstrappingRewards 
                            : newRewards;

    emit BootstrappingRewardsChanged(bootstrappingRewards);
}

Optimization Explanation:

  • Consolidated Conditional Logic: The original function used separate if statements for increase and decrease scenarios. The optimized version consolidates these checks using a ternary operator, reducing the number of conditional branches and potentially saving gas.
  • Single Arithmetic Operation: The revised function calculates the newRewards in a single line, whether increasing or decreasing, and applies limit checks in the same line. This approach minimizes the number of arithmetic operations.
  • Maintaining Functionality: The optimized function retains the same logic and limits as the original, ensuring that the functionality of setting bootstrapping rewards remains consistent.

Github : DAOConfig.sol


[G-12] Arithmetic Operations Optimization in Solidity Smart Contract

Contract Name: CollateralAndLiquidity.sol

Description:
This report outlines a potential gas optimization in the CollateralAndLiquidity smart contract by optimizing arithmetic operations. The aim is to minimize the number of arithmetic operations like multiplication and division, particularly where these operations are used repeatedly with the same variables. This can reduce the overall gas consumption of the contract's functions.

Original Code:

// Function: underlyingTokenValueInUSD
function underlyingTokenValueInUSD(uint256 amountBTC, uint256 amountETH) public view returns (uint256) {
    uint256 btcPrice = priceAggregator.getPriceBTC();
    uint256 ethPrice = priceAggregator.getPriceETH();

    uint256 btcValue = (amountBTC * btcPrice) / wbtcTenToTheDecimals;
    uint256 ethValue = (amountETH * ethPrice) / wethTenToTheDecimals;

    return btcValue + ethValue;
}

Optimized Code:

// Function: underlyingTokenValueInUSD (Optimized)
function underlyingTokenValueInUSD(uint256 amountBTC, uint256 amountETH) public view returns (uint256) {
    uint256 btcPrice = priceAggregator.getPriceBTC();
    uint256 ethPrice = priceAggregator.getPriceETH();

    // Directly return the sum of calculations to avoid unnecessary storage operations.
    return ((amountBTC * btcPrice) / wbtcTenToTheDecimals) + ((amountETH * ethPrice) / wethTenToTheDecimals);
}

Optimization Explanation:

In the underlyingTokenValueInUSD function, the arithmetic operations are simplified by directly returning the result of the calculations instead of storing them in intermediate variables (btcValue and ethValue). This reduces the need for additional storage or memory operations. The direct return of the calculated value minimizes the operations performed and thus can save gas. This change is minimal but can contribute to gas savings, especially if this function is called frequently.

Github : CollateralAndLiquidity.sol


[G-13] Optimization of State Variable Access in Solidity Smart Contract

Contract Name: CollateralAndLiquidity.sol

Description:
This report identifies a potential gas optimization in the CollateralAndLiquidity smart contract by optimizing state variable access. The focus is on reducing the redundant fetching of state variables, particularly in functions that are called frequently. This can be achieved by caching state variables in memory when they are used multiple times within the same function, reducing the overall gas consumption.

Original Code:

// Function: maxBorrowableUSDS
function maxBorrowableUSDS(address wallet) public view returns (uint256) {
    if (userShareForPool(wallet, collateralPoolID) == 0) {
        return 0;
    }

    uint256 userCollateralValue = userCollateralValueInUSD(wallet);

    if (userCollateralValue < stableConfig.minimumCollateralValueForBorrowing()) {
        return 0;
    }

    uint256 maxBorrowableAmount = (userCollateralValue * 100) / stableConfig.initialCollateralRatioPercent();

    if (usdsBorrowedByUsers[wallet] >= maxBorrowableAmount) {
        return 0;
    }

    return maxBorrowableAmount - usdsBorrowedByUsers[wallet];
}

Optimized Code:

// Function: maxBorrowableUSDS (Optimized)
function maxBorrowableUSDS(address wallet) public view returns (uint256) {
    uint256 userShares = userShareForPool(wallet, collateralPoolID);
    if (userShares == 0) {
        return 0;
    }

    uint256 userCollateralValue = userCollateralValueInUSD(wallet);
    uint256 minimumCollateral = stableConfig.minimumCollateralValueForBorrowing();
    uint256 initialCollateralRatio = stableConfig.initialCollateralRatioPercent();

    if (userCollateralValue < minimumCollateral) {
        return 0;
    }

    uint256 maxBorrowableAmount = (userCollateralValue * 100) / initialCollateralRatio;
    uint256 borrowedAmount = usdsBorrowedByUsers[wallet];

    if (borrowedAmount >= maxBorrowableAmount) {
        return 0;
    }

    return maxBorrowableAmount - borrowedAmount;
}

Optimization Explanation:

In the optimized version of the maxBorrowableUSDS function, state variables minimumCollateralValueForBorrowing, initialCollateralRatioPercent, and usdsBorrowedByUsers[wallet] are cached at the start of the function. This is beneficial because each access to a state variable can be quite gas-intensive, especially when such variables are accessed multiple times. By storing these variables in memory at the beginning of the function, we avoid repeated state variable fetches, thus saving gas. This optimization is subtle but can contribute to gas savings, especially in functions that are called frequently and involve multiple reads from state variables.

Github : CollateralAndLiquidity.sol


[G-14] Combine Increment and Decrement Operations in StakingConfig Contract

Contract Name: StakingConfig.sol

Description:
This report provides a detailed description of a potential gas optimization in the StakingConfig smart contract, specifically within the changeMaxUnstakeWeeks function. The optimization aims to streamline the conditional checks and assignment operations when increasing or decreasing the maxUnstakeWeeks state variable, thus reducing gas consumption and improving the contract's efficiency.

Original Code:

function changeMaxUnstakeWeeks(bool increase) external onlyOwner {
    if (increase) {
        if (maxUnstakeWeeks < 108)
            maxUnstakeWeeks += 8;
    } else {
        if (maxUnstakeWeeks > 20)
            maxUnstakeWeeks -= 8;
    }
    emit MaxUnstakeWeeksChanged(maxUnstakeWeeks);
}

Optimized Code:

function changeMaxUnstakeWeeks(bool increase) external onlyOwner {
    uint256 _maxUnstakeWeeks = maxUnstakeWeeks;
    uint256 delta = 8; // Constant value for increment or decrement
    if (increase && _maxUnstakeWeeks <= 100) { // 108 - 8
        _maxUnstakeWeeks += delta;
    } else if (!increase && _maxUnstakeWeeks >= 28) { // 20 + 8
        _maxUnstakeWeeks -= delta;
    }
    maxUnstakeWeeks = _maxUnstakeWeeks;
    emit MaxUnstakeWeeksChanged(_maxUnstakeWeeks);
}

Optimization Explanation:

  1. Memory Caching: The state variable maxUnstakeWeeks is cached at the beginning of the function into a local variable _maxUnstakeWeeks. This minimizes the cost associated with reading and writing to storage, as storage operations are more expensive than memory operations in terms of gas.

  2. Boundary Check Optimization: The boundary conditions for incrementing and decrementing maxUnstakeWeeks are optimized by pre-calculating the adjusted limits (108 - 8 for the upper limit and 20 + 8 for the lower limit). This ensures that the value of _maxUnstakeWeeks remains within the specified range, and avoids redundant checks and adjustments.

  3. Single Write to Storage: After all calculations are done in memory, the result is written back to the storage variable maxUnstakeWeeks only once, reducing the gas cost associated with multiple storage writes.

  4. Event Emission with Updated Value: The MaxUnstakeWeeksChanged event is emitted with the updated value of _maxUnstakeWeeks, ensuring that event subscribers receive the most recent value.

Github : StakingConfig.sol


[G-15] Gas Efficiency Improvement in getTwapWETH Function(not confirm)

Contract Name:

CoreUniswapFeed.sol

Description:

The purpose of the getTwapWETH function within the TwapWETHGasSimulation contract is to calculate the Time-Weighted Average Price (TWAP) of WETH in relation to USDC using simulated Uniswap V3 pool data. The original implementation provided a straightforward approach but included conditional checks that could be optimized for gas efficiency. The revised version of this function introduces optimizations aimed at reducing gas consumption by simplifying conditional logic and computation.

Original Code:

function getTwapWETHOriginal(uint256 twapInterval) public view returns (uint256) {
        uint256 uniswapWETH_USDC = getUniswapTwapWei( UNISWAP_V3_WETH_USDC, twapInterval );

    if (uniswapWETH_USDC == 0)
        return 0;

    if (!weth_usdcFlipped)
        return 10**36 / uniswapWETH_USDC;
    else
        return uniswapWETH_USDC;
}

Optimized Code:

function getTwapWETHRevised(uint256 twapInterval) public view returns (uint256) {
    uint256 uniswapWETH_USDC = 1e18; // Simulated value for TWAP

    // Early return if the TWAP is invalid
    if (uniswapWETH_USDC == 0) {
        return 0;
    }

    // Calculate the result based on the pool's token order without using ternary operator
    uint256 result = weth_usdcFlipped ? uniswapWETH_USDC : 1e36 / uniswapWETH_USDC;
    return result;
}

Optimization Explanation:

  • Early Return for Invalid TWAP: The revised code introduces an early return pattern for cases where the TWAP value is zero, effectively minimizing the execution path for these edge cases and saving gas by avoiding further computation.

  • Simplified Conditional Logic: The optimized version eliminates the need for a separate conditional check to determine the pool's token order. Instead, it directly computes the desired value based on the weth_usdcFlipped flag, reducing the number of conditional operations and thereby saving gas.

  • Direct Result Assignment: By calculating the result variable directly based on the pool's token order and returning it in the next line, the revised code reduces the bytecode size and optimizes EVM execution, leading to further gas savings.

  • Constant Expression for Division: The use of a constant expression (1e36) for the division operation in the case of an unflipped pool order is more gas-efficient than computing 10**36 at runtime, as constants are resolved at compile-time.

Github : CoreUniswapFeed.sol


#0 - c4-judge

2024-02-03T14:21:18Z

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