Platform: Code4rena
Start Date: 16/01/2024
Pot Size: $80,000 USDC
Total HM: 37
Participants: 178
Period: 14 days
Judge: Picodes
Total Solo HM: 4
Id: 320
League: ETH
Rank: 122/178
Findings: 2
Award: $32.48
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: juancito
Also found by: 0x3b, 0xBinChook, 0xCiphky, 0xHelium, 0xMango, 0xOmer, 0xRobocop, 0xSmartContractSamurai, 0xWaitress, 0xbepresent, 0xpiken, 7ashraf, Arz, Audinarey, Banditx0x, Bauchibred, Draiakoo, IceBear, J4X, Jorgect, Kaysoft, KingNFT, Rhaydden, Rolezn, The-Seraphs, Tigerfrake, Topmark, Tripathi, Udsen, ZanyBonzy, a3yip6, b0g0, chaduke, codeslide, csanuragjain, dharma09, djxploit, eta, ether_sky, grearlake, inzinko, jasonxiale, jesjupyter, josephdara, lanrebayode77, linmiaomiao, lsaudit, niroh, oakcobalt, peanuts, pina, sivanesh_808, slvDev, t0x1c, vnavascues, wangxx2026
11.6897 USDC - $11.69
S.No | Title | Contract Name |
---|---|---|
L-01 | Misaligned Emissions Rate Calculation | Emissions.sol |
L-02 | Potential Precision Loss in _zapSwapAmount Calculation | PoolMath.sol |
L-03 | Division Before Validation in Arbitrage Profit Distribution | PoolStats.sol |
L-04 | Fund Loss Due to Inadequate Handling of Dust in _placeInternalSwap | PoolUtils.sol |
L-05 | Precision Loss in Price Aggregation | PriceAggregator.sol |
L-06 | Risk of Fund Loss in _sendInitialLiquidityRewards | SaltRewards.sol |
L-07 | Risk of Fund Loss in _bisectionSearch Function | ArbitrageSearch.sol |
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.
function performUpkeep(uint256 timeSinceLastUpkeep) external { ... uint256 saltToSend = (saltBalance * timeSinceLastUpkeep * rewardsConfig.emissionsWeeklyPercentTimes1000()) / (100 * 1000 weeks); ... }
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.
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.
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%).Calculation Process:
The amount of SALT to send (saltToSend
) is calculated in the contract as follows:
[ saltToSend
= (saltBalance
* timeSinceLastUpkeep
* emissionsWeeklyPercentTimes1000
) / (100 * 1000 * 1 week
) ]
3 days
into a fraction of a week: 3 days / 7 days = 0.42857 weeks
.saltToSend
(Expected):
saltToSend
= (100,000 * 0.42857 * 50) / (100 * 1000) ]
[ saltToSend
≈ 214.285 ] (SALT tokens)saltToSend
(Actual in Solidity):
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)_zapSwapAmount
CalculationIn 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.
// ... within _zapSwapAmount function uint256 sqrtDiscriminant = Math.sqrt(discriminant); swapAmount = (sqrtDiscriminant - B) / (2 * A);
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.
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.
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.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))
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
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 token0swapAmount
(Actual in Solidity):
sqrtDiscriminant
= sqrt(590,476,192,000)
≈ 768,734
(rounded down)swapAmount
= (768,734 - 20,000) / 2
≈ 374,367
units of token0374,367.45
units of token0 should be swapped.374,367
units of token0 are swapped due to rounding down.0.45
units of token0 due to the rounding inherent in Solidity's integer arithmetic.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
).
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; ... } } }
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.
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.
Initial Conditions:
arbitrageProfit
generated by a specific arbitrage path: 300
units._arbitrageIndicies
):
index1
: 0
(valid index)index2
: INVALID_POOL_ID
(indicating an invalid pool)index3
: 2
(valid index)Calculation of Profits:
_calculateArbitrageProfits
function will divide the arbitrageProfit
by 3 and attempt to distribute it to the pools based on their indices.With Valid Pool Indices:
index1
and index3
should receive profits.Expected Profit Distribution:
index1
profit: 150
units (half of 300
, as only two indices are valid).index3
profit: 150
units.Unconditional Division:
arbitrageProfit
by 3 unconditionally.100
units (300 / 3
).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.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.
_placeInternalSwap
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.
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); }
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.
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.
reservesIn
: 100,000
unitsmaximumInternalSwapPercentTimes1000
: 5,000
(5%)amountIn
: 50
unitsDUST
threshold: 100
unitsCalculate maxAmountIn
:
maxAmountIn = reservesIn * maximumInternalSwapPercentTimes1000 / 100,000
maxAmountIn = 100,000 * 5,000 / 100,000
maxAmountIn = 500,000,000 / 100,000
maxAmountIn = 5,000
unitsCheck Against Dust:
amountIn
(50 units) is compared to DUST
(100 units).amountIn
< DUST
, the function should handle this case specially.Expected Swap Execution:
amountIn
could be adjusted to DUST
.DUST
, amountIn
would be 100
units for the swap.Calculate maxAmountIn
:
maxAmountIn = 5,000
unitsNo Dust Check:
amountIn
is 50
units, which is below the DUST
threshold but is used as-is because there's no dust check.Actual Swap Execution:
amountIn
of 50
units.DUST
, so the small amount is used directly.Expected Behavior (With Dust Check):
100
units (DUST
threshold) instead of the original 50
units.Actual Behavior (Without Dust Check):
50
units, despite being below the DUST
threshold.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.
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; }
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.
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.
Initial Conditions:
price1
, price2
, price3
) for BTC or ETH.price1 = 50,000
, price2 = 51,000
, price3 = 55,000
(unit: USD).maximumPriceFeedPercentDifferenceTimes1000
: 2,000
(representing a maximum allowable difference of 2%).Calculation of Average and Difference Check:
_aggregatePrices
function calculates the average of the two closest prices and checks if their difference is within the allowable range.Identify Closest Prices:
price1
and price2
(50,000
and 51,000
).Calculate Average:
averagePrice = (50,000 + 51,000) / 2 = 101,000 / 2 = 50,500
.Difference Check:
difference = |50,000 - 51,000| = 1,000
.difference / averagePrice = 1,000 / 50,500 ≈ 0.0198
(approximately 1.98%).Same Closest Prices:
price1
and price2
are still the closest.Calculate Average:
averagePrice
calculation remains the same: 50,500
.Difference Check in Solidity:
difference
calculation remains the same: 1,000
.(_absoluteDifference(priceA, priceB) * 100000) / averagePrice
.(1,000 * 100,000) / 50,500 = 100,000,000 / 50,500 ≈ 1,980
(Solidity truncates the decimal).1,000
, the actual percentage difference is approximately 1.98%
, which is below the 2% threshold.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.
_sendInitialLiquidityRewards
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.
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); }
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.
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.
liquidityBootstrapAmount
(Total SALT tokens to be distributed): 1,000
tokens.poolIDs
(Number of pools): 3
pools (e.g., pool1
, pool2
, pool3
).Calculate Rewards Per Pool (Ideal):
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.333
tokens per pool and handle the remainder separately.Handle Remainder:
333 tokens * 3 pools = 999 tokens
.1,000 tokens (total) - 999 tokens (distributed) = 1 token
.1 token
could be distributed to one of the pools or handled in another manner.Calculate Rewards Per Pool (Solidity):
amountPerPool
(Solidity) = liquidityBootstrapAmount / Number of Pools
amountPerPool
(Solidity) = 1,000 tokens / 3 pools = 333 tokens
per pool (decimal part truncated).Total Distribution in Solidity:
333 tokens * 3 pools = 999 tokens
.1 token
is effectively lost.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.
_bisectionSearch
FunctionThe _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.
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; ... }
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.
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.
Initial Conditions:
swapAmountInValueInETH
: Let's assume it's 1,000
ETH.Bisection Search Range:
leftPoint
= swapAmountInValueInETH / 128
= 1,000 / 128 ≈ 7.81
ETH.rightPoint
= swapAmountInValueInETH + (swapAmountInValueInETH / 4)
= 1,000 + 250 = 1,250
ETH.Assumption for Optimal Arbitrage Amount:
500
ETH, which lies within the initial range.Ideal Arbitrage Search:
500
ETH, efficiently.500
ETH.Expected Outcome:
500
ETH as the optimal amount for arbitrage.First Iteration:
(7.81 + 1,250) / 2 ≈ 628.91
ETH.628.91
ETH.Subsequent Iterations:
Final Outcome:
500
ETH, but the exactness depends on the number of iterations and how the profit comparison is made at each step.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.
#0 - c4-judge
2024-02-03T13:17:56Z
Picodes marked the issue as grade-b
🌟 Selected for report: 0xVolcano
Also found by: 0x11singh99, 0xAnah, Beepidibop, JCK, JcFichtner, K42, Kaysoft, Pechenite, Raihan, Rolezn, dharma09, hunter_w3b, lsaudit, n0kto, naman1778, niroh, sivanesh_808, slvDev, unique
20.7932 USDC - $20.79
Contest : salty[GAS]
S.No | Title | Contract Name |
---|---|---|
G-01 | Optimization Report for getPriceBTC and getPriceETH Functions in CoreSaltyFeed Contract | CoreSaltyFeed.sol |
G-02 | Optimization of latestChainlinkPrice Function in CoreChainlinkFeed Contract | CoreChainlinkFeed.sol |
G-03 | Optimization Analysis of _aggregatePrices Function in the Price Aggregator Contract | PriceAggregatorGasTest.sol |
G-04 | Optimization of the _placeInternalSwap Function in the PoolUtils Library | PoolUtils.sol |
G-05 | Optimization of totalVotesCastForBallot in Proposals Contract | Proposals.sol |
G-06 | Optimization of Unnecessary if Statements for Zero Checks | RewardsEmitter.sol |
G-07 | Optimization of PoolsConfig Contract | PoolsConfig.sol |
G-08 | Enhanced Gas Efficiency in changeMaximumWhitelistedPools Function | PoolsConfig.sol |
G-09 | Enhanced Gas Efficiency in changeMaximumInternalSwapPercentTimes1000 Function | PoolsConfig.sol |
G-10 | Optimization of walletHasAccess Function in ExchangeConfig Contract | ExchangeConfig.sol |
G-11 | Optimization of changeBootstrappingRewards in DAOConfig Contract | DAOConfig.sol |
G-12 | Arithmetic Operations Optimization in Solidity Smart Contract | CollateralAndLiquidity.sol.sol |
G-13 | Optimization of State Variable Access in Solidity Smart Contract | CollateralAndLiquidity.sol |
G-14 | Combine Increment and Decrement Operations in StakingConfig Contract | StakingConfig.sol |
G-15 | Gas Efficiency Improvement in getTwapWETH Function | CoreUniswapFeed.sol |
getPriceBTC
and getPriceETH
Functions in CoreSaltyFeed ContractCoreSaltyFeed.sol
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.
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; }
Common Optimizations for Both Functions:
Caching State Variables in Memory:
wbtc
, weth
, usds
) are cached in memory to reduce gas costs associated with state variable accesses.Unchecked Division:
unchecked
block to avoid unnecessary overflow/underflow checks, thereby saving gas.Precomputed 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; } }
latestChainlinkPrice
Function in CoreChainlinkFeed ContractCoreChainlinkFeed.sol
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.
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; }
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; } }
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.
_aggregatePrices
Function in the Price Aggregator ContractPriceAggregator.sol
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.
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; }
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; }
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.
_placeInternalSwap
Function in the PoolUtils
LibraryPoolUtils.sol
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.
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); }
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); }
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.
totalVotesCastForBallot
in Proposals
ContractContract 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:
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.
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.
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.
if
Statements for Zero ChecksContract Name: RewardsEmitter.sol
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.
// In function addSALTRewards if (sum > 0) salt.safeTransferFrom(msg.sender, address(this), sum);
// In function addSALTRewards salt.safeTransferFrom(msg.sender, address(this), sum);
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:
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.
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.
PoolsConfig
ContractContract 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.
changeMaximumWhitelistedPools
FunctionContract 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.
changeMaximumInternalSwapPercentTimes1000
FunctionContract 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:
if-else
blocks, reducing the complexity of the logic. This simplification aims to decrease the computational steps required, potentially leading to lower gas usage.maximumInternalSwapPercentTimes1000
variable are made directly within the if-else
blocks. This approach minimizes the operations performed by the function.maximumInternalSwapPercentTimes1000
. This conditional check avoids unnecessary gas expenditure associated with state changes and event logging when no actual change occurs.walletHasAccess
Function in ExchangeConfig
ContractContract 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:
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.AccessManager
. The optimization solely focuses on improving the efficiency of these checks.changeBootstrappingRewards
in DAOConfig ContractContract 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:
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.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.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.
// 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; }
// 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); }
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.
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.
// 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]; }
// 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; }
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.
StakingConfig
ContractContract 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.
function changeMaxUnstakeWeeks(bool increase) external onlyOwner { if (increase) { if (maxUnstakeWeeks < 108) maxUnstakeWeeks += 8; } else { if (maxUnstakeWeeks > 20) maxUnstakeWeeks -= 8; } emit MaxUnstakeWeeksChanged(maxUnstakeWeeks); }
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); }
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.
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.
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.
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.
getTwapWETH
Function(not confirm)CoreUniswapFeed.sol
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.
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; }
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; }
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.
#0 - c4-judge
2024-02-03T14:21:18Z
Picodes marked the issue as grade-b