Platform: Code4rena
Start Date: 21/02/2024
Pot Size: $200,000 USDC
Total HM: 22
Participants: 36
Period: 19 days
Judge: Trust
Total Solo HM: 12
Id: 330
League: ETH
Rank: 23/36
Findings: 1
Award: $608.33
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xAnah
Also found by: 0x11singh99, 0xhacksmithh, K42, albahaca, dharma09
608.3337 USDC - $608.33
Possible Optimization 1 =
modifiers
, to ensure state checks and pool synchronization. These modifiers
, when used frequently and in combination, can introduce redundancy in logic execution. By consolidating these modifiers
into a single, streamlined modifier, we can reduce the redundancy and optimize execution paths, particularly for functions that require both health checks and pool synchronization.Here is the optimized code snippet:
modifier syncAndHealthCheck(uint256 _nftId, address _poolToken) { _checkReentrancy(); if (_poolToken != address(0)) { (uint256 lendSharePrice, uint256 borrowSharePrice) = _syncPoolBeforeCodeExecution(_poolToken); _; _syncPoolAfterCodeExecution(_poolToken, lendSharePrice, borrowSharePrice); } else { _; } _healthStateCheck(_nftId); }
modifiers
can save hundreds of gas units per transaction, depending on the contract's execution paths and the frequency of modifier
usage.Possible Optimization 2 =
events
are emitted with block.timestamp
, which can be found in functions like _emitFundsSolelyDeposited() and _emitFundsSolelyWithdrawn(). Since the timestamp of a block can be inferred from the block itself when the transaction is included, explicitly including block.timestamp
in event emissions is redundant and increases the gas cost for logging those events.Here is the optimized code:
// Optimized event without block.timestamp event FundsSolelyDeposited(address indexed caller, uint256 indexed nftId, address indexed poolToken, uint256 amount); event FundsSolelyWithdrawn(address indexed caller, uint256 indexed nftId, address indexed poolToken, uint256 amount); // Function emitting optimized event function _emitFundsSolelyDepositedOptimized(address _caller, uint256 _nftId, address _poolToken, uint256 _amount) private { emit FundsSolelyDeposited(_caller, _nftId, _poolToken, _amount); }
Possible Optimization 1 =
internal
function that can be called once and whose result can be stored in a memory
variable for subsequent checks within the same transaction. This is particularly applicable to the checksLiquidation(), checksWithdraw(), checksBorrow(), and similar functions that may perform overlapping checks.Here is the optimized code snippet:
// Before optimization: Multiple external view functions might call these checks separately // After optimization: Consolidate checks into a single function call function _consolidatedChecks(uint256 _nftId, address _caller, address _poolToken) private view returns (bool specialCase) { bool isBlacklisted = _checkBlacklisted(_poolToken); bool hasOpenBorrowPosition = overallETHBorrowBare(_nftId) > 0; bool isVerifiedIsolationPool = WISE_LENDING.verifiedIsolationPool(_caller); bool isPositionLocked = WISE_LENDING.positionLocked(_nftId); // Return a struct or multiple values based on what checks are needed return (isBlacklisted || hasOpenBorrowPosition || isVerifiedIsolationPool || isPositionLocked); } // Utilize the optimized checks in relevant functions
Possible Optimization 2 =
UINT256_MAX
for interacting with Curve pools. This pattern can be optimized by checking if an approval reset is necessary, thereby potentially reducing the number of approve calls.Here is the optimized code:
function prepareCurvePoolsOptimized(address _poolToken, CurveSwapStructData calldata _curveSwapStructData, CurveSwapStructToken calldata _curveSwapStructToken) external onlyWiseLending { curveSwapInfoData[_poolToken] = _curveSwapStructData; curveSwapInfoToken[_poolToken] = _curveSwapStructToken; address curvePool = _curveSwapStructData.curvePool; uint256 tokenIndexForApprove = _curveSwapStructToken.curvePoolTokenIndexFrom; IERC20 token = IERC20(ICurve(curvePool).coins(tokenIndexForApprove)); uint256 currentAllowance = token.allowance(address(this), curvePool); if(currentAllowance != UINT256_MAX) { _safeApprove(token, curvePool, UINT256_MAX); } // Repeat for curveMetaPool if applicable }
UINT256_MAX
. The gas savings depend on the frequency of these operations and the current state of allowances but could range from a few thousand to tens of thousands of gas units per approval optimization.Possible Optimization 1 =
private pure
and is used by both calculateLendingShares() and calculateBorrowShares() functions to compute shares. Given its simplicity, inlining this logic will save gas by reducing function call overhead.Here is the optimized code snippet:
function calculateLendingShares(address _poolToken, uint256 _amount, bool _maxSharePrice) public view returns (uint256) { uint256 _product = lendingPoolData[_poolToken].totalDepositShares * _amount; uint256 _pseudo = lendingPoolData[_poolToken].pseudoTotalPool; return _maxSharePrice ? (_product / _pseudo + 1) : (_product / _pseudo - 1); } function calculateBorrowShares(address _poolToken, uint256 _amount, bool _maxSharePrice) public view returns (uint256) { uint256 _product = borrowPoolData[_poolToken].totalBorrowShares * _amount; uint256 _pseudo = borrowPoolData[_poolToken].pseudoTotalBorrowAmount; return _maxSharePrice ? (_product / _pseudo + 1) : (_product / _pseudo - 1); }
JUMP
and JUMPI
operations required for an additional function call.Possible Optimization 2 =
SSTORE
operations.Here is the optimized code:
function _updatePoolDataBatch(address _poolToken) internal { _cleanUp(_poolToken); // Assume this function is adjusted to return values instead of directly setting them uint256 newUtilization = _getValueUtilization(_poolToken); // Other calculations for global pool data update // Batch update global pool data GlobalPoolData storage pool = globalPoolData[_poolToken]; pool.utilization = newUtilization; // Set other updated values here }
SSTORE
operations by consolidating state updates.Possible Optimization 3 =
Here is the optimized code snippet:
// Instead of directly dividing by _pseudo, precalculate its inverse when possible and multiply. function calculateLendingSharesOptimized(address _poolToken, uint256 _amount, bool _maxSharePrice) public view returns (uint256) { uint256 _pseudoInverse = PRECISION_FACTOR_E18 / lendingPoolData[_poolToken].pseudoTotalPool; // Assuming PRECISION_FACTOR_E18 is a predefined constant for precision uint256 _product = lendingPoolData[_poolToken].totalDepositShares * _amount * _pseudoInverse; return _maxSharePrice ? (_product + PRECISION_FACTOR_E18) : (_product - PRECISION_FACTOR_E18); }
(DIV)
to multiplication (MUL)
operations, leveraging precomputed inverses where applicable to reduce computational complexity and gas costs.Possible Optimization 1 =
WISE_LENDING
.Here is the optimized code snippet:
// Optimized function to calculate both collateral and borrow metrics in a single pass function calculateOverallMetrics(uint256 _nftId) internal view returns (uint256 weightedTotal, uint256 unweightedTotal, uint256 totalBorrow) { uint256 amount; uint256 borrowAmount; address tokenAddress; uint256 l = WISE_LENDING.getPositionLendingTokenLength(_nftId); for (uint256 i = 0; i < l; ++i) { tokenAddress = WISE_LENDING.getPositionLendingTokenByIndex(_nftId, i); amount = getFullCollateralETH(_nftId, tokenAddress); weightedTotal += amount * WISE_LENDING.lendingPoolData(tokenAddress).collateralFactor / PRECISION_FACTOR_E18; unweightedTotal += amount; // Assuming similar access pattern for borrow amounts borrowAmount = getETHBorrow(_nftId, tokenAddress); totalBorrow += borrowAmount; } }
Possible Optimization 2 =
ERC20
tokens (e.g., within getFullCollateralETH() and _getTokensInEth()). Solidity's external call mechanism is inherently gas-intensive due to ABI encoding/decoding. A precise optimization in the current context, can be achieved by employing inline assembly for balance fetching, which bypasses the ABI encoding/decoding process, directly accessing the balanceOf
function of ERC20
tokens.Here is the optimized code:
function _getBalanceInAssembly(address token, address account) private view returns (uint256 balance) { assembly { // Construct the signature of the "balanceOf(address)" function let ptr := mload(0x40) mstore(ptr, 0x70a0823100000000000000000000000000000000000000000000000000000000) // balanceOf signature mstore(add(ptr, 4), account) // address argument location // Perform low-level call to token contract let result := staticcall( gas(), // use all available gas token, // token contract address ptr, // input location 0x24, // input size (4 bytes function signature + 32 bytes address) ptr, // output will overwrite input 0x20 // output size (32 bytes) ) // Check call success and return balance switch result case 0 { revert(0, 0) } default { balance := mload(ptr) } } }
ERC20
balanceOf
calls by eliminating ABI
encoding/decoding overhead. Reducing the use of CALL
opcode by directly interacting with the storage of the external token contracts. Eliminating the need for DELEGATECALL
related to ABI
encoding/decoding, further reducing gas consumption. Employs STATICCALL
for safety, ensuring no state modification during the balance fetch. Ensure that all token contracts interacted with adhere strictly to the ERC20 standard without deviations in the balanceOf
function.Possible Optimization 1 =
Here is the optimized code snippet:
function addCompoundRewardsOptimized(uint256 _amount) external syncSupply { if (_amount == 0) { revert ZeroAmount(); } totalLpAssetsToDistribute += _amount; // Only perform actions if caller is not the controller to save gas if (msg.sender != PENDLE_POWER_FARM_CONTROLLER) { _safeTransferFrom( UNDERLYING_PENDLE_MARKET, msg.sender, address(this), _amount ); // Assume UNDERLYING_PENDLE_MARKET is ERC20 compliant and supports safeTransferFrom } } // Consolidate reward updates and calculations in a single loop to minimize external calls and state updates function _calculateAndUpdateRewards() internal { address[] memory rewardTokens = PENDLE_CONTROLLER.pendleChildCompoundInfoRewardTokens(UNDERLYING_PENDLE_MARKET); uint256[] memory rewards = new uint256[](rewardTokens.length); bool updated = false; for (uint256 i = 0; i < rewardTokens.length; ++i) { (uint256 userReward, uint256 lastIndex, uint256 index) = PENDLE_MARKET.rewardInfo(rewardTokens[i], PENDLE_POWER_FARM_CONTROLLER); if (index > lastIndex) { rewards[i] = userReward; updated = true; } } if (updated) { PENDLE_CONTROLLER.increaseReservedForCompound(UNDERLYING_PENDLE_MARKET, rewards); } }
SLOAD
, SSTORE
, and external call (CALL)
opcodes by reducing their usage. The introduction of batch processing for reward tokens decreases the number of state reads and writes, as well as the number of external calls made to update rewards, which are among the most gas-intensive operations in EVM
execution.Possible Optimization 2 =
PENDLE_CONTROLLER
and PENDLE_MARKET
for each token, which is gas-intensive. A batch processing approach can be adopted to reduce the number of external calls by consolidating data retrieval into fewer calls that fetch data for all tokens at once, followed by internal processing.Here is the optimized code:
function _calculateRewardsClaimedOutsideOptimized() internal returns (uint256[] memory) { address[] memory rewardTokens = PENDLE_CONTROLLER.pendleChildCompoundInfoRewardTokens(UNDERLYING_PENDLE_MARKET); uint256 l = rewardTokens.length; uint256[] memory rewardsOutsideArray = new uint256[](l); // Batch fetch the last indexes for all tokens at once uint128[] memory lastIndex = PENDLE_CONTROLLER.pendleChildCompoundInfoLastIndexBatch(UNDERLYING_PENDLE_MARKET, rewardTokens); // Iterate over each token to calculate rewards based on batch-fetched indexes for (uint256 i = 0; i < l; ++i) { address token = rewardTokens[i]; UserReward memory userReward = _getUserReward(token, PENDLE_POWER_FARM_CONTROLLER); uint128 index = userReward.index; if (userReward.accrued > 0) { PENDLE_MARKET.redeemRewards(PENDLE_POWER_FARM_CONTROLLER, token); // Optimized to redeem for specific token userReward = _getUserReward(token, PENDLE_POWER_FARM_CONTROLLER); } if (index > lastIndex[i]) { uint256 activeBalance = _getActiveBalance(); uint256 totalLpAssetsCurrent = totalLpAssets(); uint256 lpBalanceController = _getBalanceLpBalanceController(); bool scaleNecessary = totalLpAssetsCurrent < lpBalanceController; uint256 indexDiff = index - lastIndex[i]; rewardsOutsideArray[i] = scaleNecessary ? indexDiff * activeBalance * totalLpAssetsCurrent / lpBalanceController / PRECISION_FACTOR_E18 : indexDiff * activeBalance / PRECISION_FACTOR_E18; } } return rewardsOutsideArray; }
ABI
encoding/decoding and EVM
call execution.Possible Optimization 1 =
Here is the optimized code snippet:
// Optimized function to set both pool fees and Aave flags in a single transaction function setPoolFeeAndAaveFlagBulk(address[] calldata _poolTokens, uint256[] calldata _newFees, address[] calldata _underlyingTokens) external onlyMaster { require(_poolTokens.length == _newFees.length && _poolTokens.length == _underlyingTokens.length, "Mismatched arrays"); for (uint256 i = 0; i < _poolTokens.length; ++i) { _checkValue(_newFees[i]); // Ensure fee values are valid WISE_LENDING.setPoolFee(_poolTokens[i], _newFees[i]); // Set pool fee _setAaveFlag(_poolTokens[i], _underlyingTokens[i]); // Set Aave flag // Note: Emitting individual events for traceability and transparency emit PoolFeeChanged(_poolTokens[i], _newFees[i], block.timestamp); } }
Possible Optimization 2 =
Here is the optimized code snippet:
// Optimized function to remove a pool token efficiently function removePoolTokenManual(address _poolToken) external onlyMaster { uint256 len = poolTokenAddresses.length; for (uint256 i = 0; i < len; i++) { if (poolTokenAddresses[i] == _poolToken) { poolTokenAddresses[i] = poolTokenAddresses[len - 1]; // Move the last element to the deleted spot poolTokenAddresses.pop(); // Remove the last element poolTokenAdded[_poolToken] = false; // Update the mapping emit PoolTokenRemoved(_poolToken, block.timestamp); // Emit event for removal return; } } revert PoolNotPresent(); // Revert if the token is not found }
Possible Optimization 1 =
Here is the optimized code snippet:
function _executeBalancerFlashLoan(uint256 _nftId, uint256 _flashAmount, uint256 _initialAmount, uint256 _lendingShares, uint256 _borrowShares, uint256 _allowedSpread, bool _ethBack, bool _isAave) internal { address flashAsset = WETH_ADDRESS; IERC20[] memory tokens = new IERC20[](1); uint256[] memory amounts = new uint256[](1); tokens[0] = IERC20(flashAsset); amounts[0] = _flashAmount; allowEnter = true; // Directly passing the array elements to reduce memory allocation BALANCER_VAULT.flashLoan(this, [IERC20(flashAsset)], [_flashAmount], abi.encode(_nftId, _initialAmount, _lendingShares, _borrowShares, _allowedSpread, msg.sender, _ethBack, _isAave)); }
Possible Optimization 2 =
_userData
.Here is the optimized code:
function receiveFlashLoan(IERC20[] memory _flashloanToken, uint256[] memory _flashloanAmounts, uint256[] memory _feeAmounts, bytes memory _userData) external { require(allowEnter, "AccessDenied"); allowEnter = false; require(_flashloanToken.length > 0, "InvalidParam"); require(msg.sender == BALANCER_ADDRESS, "NotBalancerVault"); uint256 totalDebtBalancer = _flashloanAmounts[0] + _feeAmounts[0]; (uint256 nftId, uint256 initialAmount, uint256 lendingShares, uint256 borrowShares, uint256 allowedSpread, address caller, bool ethBack, bool isAave) = abi.decode(_userData, (uint256, uint256, uint256, uint256, uint256, address, bool, bool)); if (initialAmount > 0) { _logicOpenPosition(isAave, nftId, _flashloanAmounts[0] + initialAmount, totalDebtBalancer, allowedSpread); } else { _logicClosePosition(nftId, borrowShares, lendingShares, totalDebtBalancer, allowedSpread, caller, ethBack, isAave); } }
Possible Optimization 1 =
Here is the optimized code snippet:
function _validateAnswerOptimized(address _tokenAddress) internal view returns (uint256) { if (priceFeed[_tokenAddress] == ZERO_FEED) { revert ChainLinkOracleNotSet(); } (, int256 answer, , ,) = priceFeed[_tokenAddress].latestRoundData(); uint256 answerUint = uint256(answer); // Simplify validation logic by directly comparing fetched answer with thresholds if (tokenAggregatorFromTokenAddress[_tokenAddress] > ZERO_AGGREGATOR) { IAggregator aggregator = tokenAggregatorFromTokenAddress[_tokenAddress]; int192 maxAnswer = aggregator.maxAnswer(); int192 minAnswer = aggregator.minAnswer(); if (answer > maxAnswer || answer < minAnswer) { revert OracleIsDead(); } } // Directly return the validated answer return answerUint; }
Possible Optimization 2 =
TWAP
calculation by reducing the number of operations and memory allocations involved in fetching and computing TWAP
values, for
the _getAverageTick() function.Here is the optimized code:
function _getAverageTickOptimized(address _oracle) internal view returns (int24) { (int56 tickCumulativeStart, , , ) = IUniswapV3Pool(_oracle).observations(0); (int56 tickCumulativeEnd, , , ) = IUniswapV3Pool(_oracle).observations(IUniswapV3Pool(_oracle).observationCount() - 1); int24 averageTick = int24((tickCumulativeEnd - tickCumulativeStart) / int56(TWAP_PERIOD)); // Adjust for potential negative delta and ensure proper rounding if (tickCumulativeEnd < tickCumulativeStart && ((tickCumulativeEnd - tickCumulativeStart) % int56(TWAP_PERIOD) != 0)) { averageTick--; } return averageTick; }
TWAP
calculation. Savings arise from minimizing the number of observations calls and streamlining the tick calculation.Possible Optimization 3 =
[]
incurs extra gas due to dynamic memory allocation. For [1]
arrays, especially small ones, using stack allocation direct or fixed-size arrays can save gas.Here is the optimized code:
function _getAverageTick(address _oracle) internal view returns (int24) { uint32[] memory secondsAgo = new uint32[](2); // Dynamic allocation // Can be optimized to: uint32[2] memory secondsAgo = [TWAP_PERIOD, 0]; // Fixed-size stack allocation ... }
Possible Optimization 1 =
Here is the optimized code snippet:
// Original function iterates over childInfo.reservedForCompound array to update values // Optimization: Perform calculations in memory and then update storage once at the end function increaseReservedForCompoundOptimized(address _pendleMarket, uint256[] calldata _amounts) external onlyChildContract(_pendleMarket) { CompoundStruct storage childInfo = pendleChildCompoundInfo[_pendleMarket]; require(_amounts.length == childInfo.rewardTokens.length, "Mismatched lengths"); // Perform calculation in memory to reduce storage access uint256[] memory updatedReserves = new uint256[](_amounts.length); for (uint256 i = 0; i < _amounts.length; ++i) { updatedReserves[i] = childInfo.reservedForCompound[i] + _amounts[i]; } // Update storage in one go for (uint256 i = 0; i < updatedReserves.length; ++i) { childInfo.reservedForCompound[i] = updatedReserves[i]; } }
Possible Optimization 2 =
lastIndex
for each token in a loop, directly writing to storage multiple times. By calculating the new indexes first and then updating storage, gas efficiency can be improved.Here is the optimized code:
// Optimize by reducing repeated storage writes function overWriteIndexAllOptimized(address _pendleMarket) external onlyChildContract(_pendleMarket) { CompoundStruct storage childInfo = pendleChildCompoundInfo[_pendleMarket]; uint256 length = childInfo.rewardTokens.length; uint128[] memory newIndexes = new uint128[](length); for (uint256 i = 0; i < length; ++i) { newIndexes[i] = _getUserRewardIndex(_pendleMarket, childInfo.rewardTokens[i], address(this)); } // Update storage after computing new values for (uint256 i = 0; i < length; ++i) { childInfo.lastIndex[i] = newIndexes[i]; } }
Possible Optimization 3 =
ETH
without checking the contract's balance or the success of the transfer. Adding checks can prevent failed transactions and unnecessary gas expenditure.Here is the optimized code snippet:
// Add balance check and ensure transfer success function forwardETHOptimized(address _to, uint256 _amount) external onlyMaster { require(address(this).balance >= _amount, "Insufficient balance"); (bool success, ) = _to.call{value: _amount}(""); require(success, "ETH transfer failed"); }
Possible Optimization 1 =
NFT ID
. This process can be optimized by directly initializing the arrays with known sizes to avoid dynamic array resizing, which is more gas-intensive.Here is the optimized code snippet:
function _prepareAssociatedTokensOptimized(uint256 _nftId, address _poolTokenLend, address _poolTokenBorrow) internal returns (address[] memory lendTokens, address[] memory borrowTokens) { uint256 lendTokensCount = _countTokens(positionLendTokenData, _nftId, _poolTokenLend); uint256 borrowTokensCount = _countTokens(positionBorrowTokenData, _nftId, _poolTokenBorrow); lendTokens = new address[](lendTokensCount); borrowTokens = new address[](borrowTokensCount); for (uint256 i = 0; i < lendTokensCount; i++) { lendTokens[i] = _getTokenAt(positionLendTokenData, _nftId, _poolTokenLend, i); } for (uint256 i = 0; i < borrowTokensCount; i++) { borrowTokens[i] = _getTokenAt(positionBorrowTokenData, _nftId, _poolTokenBorrow, i); } }
NFT ID
.Possible Optimization 2 =
Here is the optimized code:
function _coreWithdrawTokenOptimized(address _caller, uint256 _nftId, address _poolToken, uint256 _amount, uint256 _shares, bool _onBehalf) internal { (address[] memory lendTokens, address[] memory borrowTokens) = _prepareAssociatedTokens(_nftId, _poolToken, ZERO_ADDRESS); WISE_SECURITY.performWithdrawAndBorrowChecks(_nftId, _caller, _poolToken, _amount, lendTokens, borrowTokens); // Assuming this is a new optimized function in WISE_SECURITY _coreWithdrawBare(_nftId, _poolToken, _amount, _shares); emit WithdrawEvent(_caller, _nftId, _poolToken, _amount, _shares, _onBehalf, block.timestamp); // Consolidated event for both cases }
WISE_SECURITY
.Possible Optimization =
FundsDeposited
, FundsSolelyDeposited
). Consolidating these into fewer events with additional parameters to cover all scenarios can reduce the gas cost associated with event logging.Here is the optimized code snippet:
event FundsOperation( address indexed sender, uint256 indexed nftId, address indexed token, uint256 amount, uint256 shares, uint256 timestamp, string operationType // "deposit", "withdraw", "borrow", etc. ); function emitFundsOperation( address sender, uint256 nftId, address token, uint256 amount, uint256 shares, string memory operationType ) internal { emit FundsOperation(sender, nftId, token, amount, shares, block.timestamp, operationType); }
Possible Optimization =
TWAP
oracles via _addTwapOracle() involves multiple validations and external calls. By streamlining these checks and reducing the number of state changes, we can save gas.Here is the optimized code snippet:
// Optimized function to add a TWAP oracle with streamlined checks and reduced state changes. function addTwapOracleOptimized( address _tokenAddress, address _uniPoolAddress, address _token0, address _token1, uint24 _fee ) external onlyMaster { // Validate the pool address directly to avoid multiple external calls and state reads. address calculatedPool = _getPool(_token0, _token1, _fee); require(calculatedPool == _uniPoolAddress, "Pool address mismatch"); // Ensure the price feed for the token is already set, avoiding redundant ZERO_FEED check. require(priceFeed[_tokenAddress] != ZERO_FEED, "Oracle not set"); // Check if TWAP Oracle is already set for the token, avoiding unnecessary mapping lookup. require(uniTwapPoolInfo[_tokenAddress].oracle == ZERO_ADDRESS, "TWAP Oracle already set"); // Directly write the new TWAP pool information, minimizing state changes. uniTwapPoolInfo[_tokenAddress] = UniTwapPoolInfo({ oracle: _uniPoolAddress, isUniPool: true }); // Emit an event to indicate the successful addition of a TWAP oracle. emit TwapOracleAdded(_tokenAddress, _uniPoolAddress); }
Possible Optimization 1 =
aaveTokenAddress
mapping every time it's called, even if the new address is the same as the current one. Checking if the new address is different before writing can save gas by avoiding unnecessary state changes.Here is the optimized code snippet:
function _setAaveTokenAddress(address _underlyingAsset, address _aaveToken) internal { if (aaveTokenAddress[_underlyingAsset] != _aaveToken) { aaveTokenAddress[_underlyingAsset] = _aaveToken; emit SetAaveTokenAddress(_underlyingAsset, _aaveToken, block.timestamp); } // Approvals can remain outside the conditional block if they need to be refreshed regardless. _safeApprove(_aaveToken, address(WISE_LENDING), MAX_AMOUNT); _safeApprove(_underlyingAsset, AAVE_ADDRESS, MAX_AMOUNT); }
SSTORE
operations when the address doesn't change. The exact savings depend on the frequency of calls with unchanged addresses but can be significant over many transactions.Possible Optimization 2 =
block.timestamp
in every event might not always be necessary if the timestamp can be inferred or is not used.Here is the optimized code:
// Before emit IsDepositAave(_nftId, block.timestamp); // After: If block.timestamp is not essential for the event's purpose emit IsDepositAave(_nftId);
block.timestamp
, which is a uint256
, this could save around 200 or more gas per event emission, depending on the context and how data is packed.Possible Optimization 1 =
Here is the optimized code snippet:
// Optimized to use a memory array for read-heavy operation function getPositionLendingTokenByIndex( uint256 _nftId, uint256 _index ) public view returns (address) { // Copy storage array to memory for cheaper reads address[] memory lendingTokens = positionLendTokenData[_nftId]; // Access the memory array instead of storage, saving gas return lendingTokens[_index]; } // This function remains unchanged but is included for context function getPositionLendingTokenLength( uint256 _nftId ) public view returns (uint256) { return positionLendTokenData[_nftId].length; }
Possible Optimization 2 =
SSTORE
operations.Here is the optimized code:
// Batch update function to consolidate state changes function _batchUpdateTotalPoolAndShares( address _poolToken, uint256 _poolAmountDelta, uint256 _shareAmountDelta, bool _isIncrease ) internal { // Check if the operation is an increase or decrease if (_isIncrease) { // Increase both total pool and total deposit shares in one go globalPoolData[_poolToken].totalPool += _poolAmountDelta; lendingPoolData[_poolToken].totalDepositShares += _shareAmountDelta; } else { // Decrease both total pool and total deposit shares in one go globalPoolData[_poolToken].totalPool -= _poolAmountDelta; lendingPoolData[_poolToken].totalDepositShares -= _shareAmountDelta; } // This approach minimizes the number of state changes, saving gas }
Possible Optimization 3 =
modifier
is used to restrict function access. Inlining this check directly in functions that are not frequently updated could save gas by eliminating the overhead of an additional jump operation.Here is the optimized code snippet:
function setPoolFee(address _poolToken, uint256 _newFee) external { // Inlining the onlyFeeManager modifier logic to save gas if (msg.sender != address(FEE_MANAGER)) { revert InvalidCaller(); } globalPoolData[_poolToken].poolFee = _newFee; }
JUMP
and JUMPI
operations associated with the modifier's invocation and exit, thereby saving gas.Possible Optimization =
bytes
parameter for each event
namely ExchangeRewardsForCompounding and ExchangeLpFeesForPendle. This approach reduces the gas cost associated with logging multiple separate values, especially for uint256
types that occupy a full slot.Here is the optimized code snippet:
// Original Events event ExchangeRewardsForCompounding( address indexed _pendleMarket, address indexed _rewardToken, uint256 _rewardAmount, uint256 _sendingAmount ); event ExchangeLpFeesForPendle( address indexed _pendleMarket, uint256 _pendleChildShares, uint256 _tokenAmountSend, uint256 _withdrawnAmount ); // Refined Optimized combined event for both compounding and LP fee exchange scenarios event OptimizedEvent( address indexed _pendleMarket, bytes compressedData ); // Refined function to emit compressed event for ExchangeRewardsForCompounding function emitOptimizedExchangeRewardsForCompounding( address _pendleMarket, address _rewardToken, uint256 _rewardAmount, uint256 _sendingAmount ) internal { bytes memory compressedData = abi.encodePacked(_rewardToken, _rewardAmount, _sendingAmount); emit OptimizedEvent(_pendleMarket, compressedData); } // Refined function to emit compressed event for ExchangeLpFeesForPendle function emitOptimizedExchangeLpFeesForPendle( address _pendleMarket, uint256 _pendleChildShares, uint256 _tokenAmountSend, uint256 _withdrawnAmount ) internal { bytes memory compressedData = abi.encodePacked(_pendleChildShares, _tokenAmountSend, _withdrawnAmount); emit OptimizedEvent(_pendleMarket, compressedData); }
uint256
values into a single bytes
parameter, the contract can significantly reduce the gas cost associated with event emission, by reducing the number of LOG
operations required to emit event
data, by packing multiple values into a single bytes
array.Possible Optimization 1 =
Here is the optimized code snippet:
function _wrapDepositExactAmount(uint256 _nftId, address _underlyingAsset, uint256 _depositAmount) internal returns (uint256) { // Inline checks previously in nonReentrant() and validToken() modifiers if (sendingProgressAaveHub == true || WISE_LENDING.sendingProgress() == true) { revert InvalidAction(); } if (WISE_LENDING.getTotalDepositShares(aaveTokenAddress[_underlyingAsset]) == 0) { revert InvalidToken(); } uint256 actualDepositAmount = _wrapAaveReturnValueDeposit(_underlyingAsset, _depositAmount, address(this)); if (POSITION_NFT.isOwner(_nftId, msg.sender) == false) { revert InvalidAction(); } return WISE_LENDING.depositExactAmount(_nftId, aaveTokenAddress[_underlyingAsset], actualDepositAmount); }
JUMP
and JUMPI
opcodes associated with entering and exiting modifiers, leading to a more linear and slightly more efficient execution path.Possible Optimization 2 =
AAVE.supply
function to calculate the deposited amount. This pattern can be optimized by checking if the _depositAmount
is actually available in the contract's balance before attempting the deposit, potentially avoiding unnecessary calls to AAVE.supply
.Here is the optimized code:
function _wrapAaveReturnValueDeposit( address _underlyingAsset, uint256 _depositAmount, address _targetAddress ) internal returns (uint256 res) { IERC20 token = IERC20(aaveTokenAddress[_underlyingAsset]); uint256 balanceBefore = token.balanceOf(address(this)); // Check if the deposit amount is available to avoid unnecessary operations require(token.balanceOf(address(this)) >= _depositAmount, "Insufficient balance for deposit"); AAVE.supply(_underlyingAsset, _depositAmount, _targetAddress, REF_CODE); uint256 balanceAfter = token.balanceOf(address(this)); res = balanceAfter - balanceBefore; }
CALL
operations and balance checks.Possible Optimization 1 =
ID
. This process can be optimized by preparing the array size in advance to avoid resizing and by using a single loop where possible.Here is the optimized code snippet:
function walletOfOwner(address _owner) external view returns (uint256[] memory) { uint256 ownerTokenCount = balanceOf(_owner); uint256 reservedId = reserved[_owner]; // Pre-calculate the total count to allocate memory efficiently. uint256 totalTokenCount = ownerTokenCount + (reservedId > 0 ? 1 : 0); uint256[] memory tokenIds = new uint256[](totalTokenCount); for (uint256 i = 0; i < ownerTokenCount; i++) { tokenIds[i] = tokenOfOwnerByIndex(_owner, i); // Populate with owned tokens. } if (reservedId > 0) { tokenIds[ownerTokenCount] = reservedId; // Append the reservedId at the end, if any. } return tokenIds; }
Possible Optimization 2 =
mapping
and totalReserved counter can be streamlined, especially in _mintPositionForUser(), to minimize storage access and updates.Here is the optimized code:
function _mintPositionForUser(address _user) internal returns (uint256) { uint256 nftId = reserved[_user]; // Check if a reserved ID exists for the user and use it, otherwise, generate a new ID. if (nftId == 0) { nftId = totalSupply() + 1; // Use totalSupply() to determine the next ID. } else { // If using a reserved ID, remove it from the mapping to prevent reuse. delete reserved[_user]; // Adjust totalReserved only when utilizing a reserved ID. totalReserved = totalReserved > 0 ? totalReserved - 1 : 0; } _mint(_user, nftId); return nftId; }
Possible Optimization =
Here is the optimized code snippet:
// Internal function to validate liquidation setting values against specified ranges. // This approach centralizes the validation logic, making the code cleaner and easier to maintain. function _validateLiquidationSetting(uint256 value, uint256 min, uint256 max, string memory errorMessage) internal pure { // Ensure the value falls within the [min, max] range, using a single, reusable function. require(value >= min && value <= max, errorMessage); } function _setLiquidationSettings(uint256 _baseReward, uint256 _baseRewardFarm, uint256 _newMaxFeeETH, uint256 _newMaxFeeFarmETH) internal { // Validate each setting with the centralized validation function, reducing code duplication. _validateLiquidationSetting(_baseReward, LIQUIDATION_INCENTIVE_MIN, LIQUIDATION_INCENTIVE_MAX, "Invalid base reward"); baseRewardLiquidation = _baseReward; _validateLiquidationSetting(_baseRewardFarm, LIQUIDATION_INCENTIVE_MIN, LIQUIDATION_INCENTIVE_POWERFARM_MAX, "Invalid farm base reward"); baseRewardLiquidationFarm = _baseRewardFarm; _validateLiquidationSetting(_newMaxFeeETH, LIQUIDATION_FEE_MIN_ETH, LIQUIDATION_FEE_MAX_ETH, "Invalid max fee ETH"); maxFeeETH = _newMaxFeeETH; _validateLiquidationSetting(_newMaxFeeFarmETH, LIQUIDATION_FEE_MIN_NON_ETH, LIQUIDATION_FEE_MAX_NON_ETH, "Invalid max fee farm ETH"); maxFeeFarmETH = _newMaxFeeFarmETH; }
REVERT
paths and associated JUMPI
instructions, leading to a slightly more efficient execution path for setting liquidation settings. While direct gas savings per transaction may be minimal, this approach reduces the bytecode size slightly, leading to marginal gas savings during contract deployment.Possible Optimization 1 =
Here is the optimized code snippet:
function _createPool(CreatePool calldata _params) private { require(_params.poolToken != ZERO_ADDRESS, "InvalidAddress"); require(timestampsPoolData[_params.poolToken].timeStamp == 0, "PoolExists"); // Consolidate validation for poolMulFactor and poolCollFactor into a single call _validatePoolFactors(_params.poolMulFactor, _params.poolCollFactor); uint256 staticMinPole = _calculateStaticPole(_params.poolMulFactor, true); uint256 staticMaxPole = _calculateStaticPole(_params.poolMulFactor, false); uint256 staticDeltaPole = staticMaxPole - staticMinPole; uint256 startValuePole = (staticMaxPole + staticMinPole) / 2; // Initialize pool data structures with calculated values _initializePoolData(_params, startValuePole, staticDeltaPole, staticMinPole, staticMaxPole); // Transfer any tokens already in the contract to the master _transferInitialBalance(_params.poolToken); } // New helper function to validate pool factors function _validatePoolFactors(uint256 poolMulFactor, uint256 poolCollFactor) private pure { require(poolMulFactor <= PRECISION_FACTOR_E18, "InvalidMulFactor"); require(poolCollFactor <= MAX_COLLATERAL_FACTOR, "InvalidCollFactor"); } // New helper function for static pole calculation function _calculateStaticPole(uint256 poolMulFactor, bool isMinPole) private pure returns (uint256) { uint256 boundRate = isMinPole ? UPPER_BOUND_MAX_RATE : LOWER_BOUND_MAX_RATE; return PRECISION_FACTOR_E18 / 2 + (PRECISION_FACTOR_E36 / 4 + poolMulFactor * PRECISION_FACTOR_E36 / boundRate).sqrt(); } // New helper function to initialize pool data structures function _initializePoolData(CreatePool calldata _params, uint256 startValuePole, uint256 staticDeltaPole, uint256 staticMinPole, uint256 staticMaxPole) private { // Implementation of data structure initialization } // New helper function to transfer initial balance function _transferInitialBalance(address poolToken) private { uint256 fetchBalance = _getBalance(poolToken); if (fetchBalance > 0) { _safeTransfer(poolToken, master, fetchBalance); } }
Possible Optimization 2 =
Here is the optimized code:
function createCurvePool(CreatePool calldata _params, CurvePoolSettings calldata _settings) external onlyMaster { _createPool(_params); // Directly update Curve pool settings in a single step curveSwapInfoData[_params.poolToken] = _settings.curveSecuritySwapsData; curveSwapInfoToken[_params.poolToken] = _settings.curveSecuritySwapsToken; // Emit an event to indicate Curve pool creation with settings emit CurvePoolCreated(_params.poolToken, _settings.curveSecuritySwapsData, _settings.curveSecuritySwapsToken); } // Additional event for Curve pool creation event CurvePoolCreated(address indexed poolToken, CurveSwapStructData curveData, CurveSwapStructToken curveToken);
Possible Optimization 1 =
Here is the optimized code snippet:
// Optimized function to prepare both borrow and lend tokens in a single batch operation function _prepareTokens(uint256 _nftId) internal { // Combine borrow and lend token lengths to prepare for batch processing uint256 borrowLength = WISE_LENDING.getPositionBorrowTokenLength(_nftId); uint256 lendLength = WISE_LENDING.getPositionLendingTokenLength(_nftId); address[] memory tokens = new address[](borrowLength + lendLength); // Aggregate borrow tokens for batch processing for (uint256 i = 0; i < borrowLength; i++) { tokens[i] = WISE_LENDING.getPositionBorrowTokenByIndex(_nftId, i); } // Aggregate lend tokens for batch processing, continuing from where borrow tokens left off for (uint256 i = 0; i < lendLength; i++) { tokens[borrowLength + i] = WISE_LENDING.getPositionLendingTokenByIndex(_nftId, i); } // Perform a single batch sync operation instead of multiple individual syncs WISE_LENDING.syncManuallyBatch(tokens); }
Possible Optimization 2 =
Here is the optimized code:
// Optimized function to update user's bad debt with minimal storage access function _updateUserBadDebtOptimized(uint256 _nftId) internal { uint256 currentBorrowETH = WISE_SECURITY.overallETHBorrowHeartbeat(_nftId); uint256 currentCollateralBareETH = WISE_SECURITY.overallETHCollateralsBare(_nftId); uint256 newBadDebt = currentBorrowETH > currentCollateralBareETH ? currentBorrowETH - currentCollateralBareETH : 0; // Retrieve the current bad debt for comparison uint256 currentBadDebt = badDebtPosition[_nftId]; // Only proceed if there's a change in the bad debt amount if (newBadDebt != currentBadDebt) { // Update the bad debt position with the new calculated value badDebtPosition[_nftId] = newBadDebt; // Adjust the total bad debt accordingly if (newBadDebt > currentBadDebt) { _increaseTotalBadDebt(newBadDebt - currentBadDebt); } else if (currentBadDebt > 0) { _decreaseTotalBadDebt(currentBadDebt - newBadDebt); } // Emit an event for the bad debt update emit BadDebtUpdated(_nftId, newBadDebt); } }
Possible Optimization =
bool
state variable, which involves multiple SLOAD
operations. Combine these state variables into a single state variable using bitwise operations. This reduces the number of state reads required to perform reentrancy checks.Here is the optimized code:
// Assuming a single uint256 to store the state of reentrancy flags uint256 private reentrancyState; // Flags for different reentrancy states uint256 private constant FLAG_SENDING_PROGRESS = 1; uint256 private constant FLAG_WISE_LENDING_SENDING_PROGRESS = 2; uint256 private constant FLAG_AAVE_HUB_SENDING_PROGRESS = 4; function _checkReentrancyOptimized() private view { require(reentrancyState & (FLAG_SENDING_PROGRESS | FLAG_WISE_LENDING_SENDING_PROGRESS | FLAG_AAVE_HUB_SENDING_PROGRESS) == 0, "AccessDenied"); }
SLOAD
operations from three to one for the reentrancy check, saving gas for each transaction that performs this check.Possible Optimization 1 =
Here is the optimized code snippet:
// Optimization to reuse NFTs for farm entries and exits, reducing minting and burning operations function _getReusableWiseLendingNFT() internal returns (uint256) { if (availableNFTCount > 0) { return availableNFTs[availableNFTCount--]; } else { uint256 nftId = POSITION_NFT.mintPosition(); _registrationFarm(nftId); POSITION_NFT.approve(AAVE_HUB_ADDRESS, nftId); return nftId; } } function _returnNFTToPool(uint256 nftId) internal { availableNFTs[++availableNFTCount] = nftId; } // Update enterFarm, enterFarmETH, and exitFarm functions to use _getReusableWiseLendingNFT and _returnNFTToPool
Possible Optimization 2 =
Here is the optimized code:
function enterFarm( bool _isAave, uint256 _amount, uint256 _leverage, uint256 _allowedSpread ) external isActive updatePools returns (uint256) { uint256 wiseLendingNFT; if (availableNFTCount == 0) { wiseLendingNFT = POSITION_NFT.mintPosition(); _registrationFarm(wiseLendingNFT); POSITION_NFT.approve(AAVE_HUB_ADDRESS, wiseLendingNFT); } else { wiseLendingNFT = availableNFTs[availableNFTCount--]; } // ... (rest of the function) } function enterFarmETH( bool _isAave, uint256 _leverage, uint256 _allowedSpread ) external payable isActive updatePools returns (uint256) { uint256 wiseLendingNFT; if (availableNFTCount == 0) { wiseLendingNFT = POSITION_NFT.mintPosition(); _registrationFarm(wiseLendingNFT); POSITION_NFT.approve(AAVE_HUB_ADDRESS, wiseLendingNFT); } else { wiseLendingNFT = availableNFTs[availableNFTCount--]; } // ... (rest of the function) }
Possible Optimization 3 =
Here is the optimized code snippet:
// Batch processing for farm status updates to reduce gas costs function _batchUpdateFarmStatus(bool _isShutdown) internal { // Assuming multiple state variables need to be updated when shutting down the farm isShutdown = _isShutdown; // Additional state updates can be batched here // Emit a single event to confirm the status update emit FarmStatus(_isShutdown, block.timestamp); } // Update shutDownFarm function to use _batchUpdateFarmStatus function shutDownFarm(bool _state) external onlyMaster { _batchUpdateFarmStatus(_state); }
Possible Optimization 1 =
startTime
by directly using block.timestamp
without unnecessary division and multiplication.Here is the optimized code snippet:
function _calcExpiry(uint128 _weeks) internal view returns (uint128) { return uint128(block.timestamp) + (_weeks * WEEK); // Simplified calculation }
_calcExpiry
. The exact savings depend on the frequency of calls and current gas prices.Possible Optimization 2 =
Here is the optimized code:
function _batchUpdateCompoundInfo(address _pendleMarket, uint256[] memory _reservedForCompound, uint128[] memory _lastIndex, address[] memory _rewardTokens) internal { pendleChildCompoundInfo[_pendleMarket].reservedForCompound = _reservedForCompound; pendleChildCompoundInfo[_pendleMarket].lastIndex = _lastIndex; pendleChildCompoundInfo[_pendleMarket].rewardTokens = _rewardTokens; }
#0 - c4-pre-sort
2024-03-17T10:49:06Z
GalloDaSballo marked the issue as sufficient quality report
#1 - c4-judge
2024-03-26T11:37:27Z
trust1995 marked the issue as grade-a