Alchemix contest - BowTiedWardens's results

A protocol for self-repaying loans with no liquidation risk.

General Information

Platform: Code4rena

Start Date: 05/05/2022

Pot Size: $125,000 DAI

Total HM: 17

Participants: 62

Period: 14 days

Judge: leastwood

Total Solo HM: 15

Id: 120

League: ETH

Alchemix

Findings Distribution

Researcher Performance

Rank: 15/62

Findings: 2

Award: $706.87

🌟 Selected for report: 0

🚀 Solo Findings: 0

Table of Contents:

[L-01] Add constructor initializers

As per OpenZeppelin’s (OZ) recommendation, “The guidelines are now to make it impossible for anyone to run initialize on an implementation contract, by adding an empty constructor with the initializer modifier. So the implementation contract gets initialized automatically upon deployment.”

Note that this behaviour is also incorporated the OZ Wizard since the UUPS vulnerability discovery: “Additionally, we modified the code generated by the Wizard 19 to include a constructor that automatically initializes the implementation when deployed.”

Furthermore, this thwarts any attempts to frontrun the initialization tx of these contracts:

contracts-full/CrossChainCanonicalAlchemicTokenV2.sol:
   8:   function initialize(
   9        string memory name, 
  10        string memory symbol, 
  11        address[] memory _bridgeTokens
  12:   ) public initializer {


contracts-full/CrossChainCanonicalGALCX.sol:
   7:   function initialize(
   8        string memory name, 
   9        string memory symbol, 
  10        address[] memory _bridgeTokens
  11:   ) public initializer {

[L-02] Deprecated safeApprove() function

Using this deprecated function can lead to unintended reverts and potentially the locking of funds. A deeper discussion on the deprecation of this function is in OZ issue #2219 (OpenZeppelin/openzeppelin-contracts#2219). The OpenZeppelin ERC20 safeApprove() function has been deprecated, as seen in the comments of the OpenZeppelin code.

As recommended by the OpenZeppelin comment, I suggest replacing safeApprove() with safeIncreaseAllowance() or safeDecreaseAllowance() instead:

contracts-full/AlchemistV2.sol:
  382:         TokenUtils.safeApprove(yieldToken, config.adapter, type(uint256).max);
  383:         TokenUtils.safeApprove(adapter.underlyingToken(), config.adapter, type(uint256).max);
  478:         TokenUtils.safeApprove(yieldToken, adapter, type(uint256).max);
  479:         TokenUtils.safeApprove(ITokenAdapter(adapter).underlyingToken(), adapter, type(uint256).max);

contracts-full/EthAssetManager.sol:
  576:             SafeERC20.safeApprove(address(tokens[i]), address(metaPool), 0);
  577:             SafeERC20.safeApprove(address(tokens[i]), address(metaPool), amounts[i]);
  620:             SafeERC20.safeApprove(address(token), address(metaPool), 0);
  621:             SafeERC20.safeApprove(address(token), address(metaPool), amount);
  671:         SafeERC20.safeApprove(address(metaPool), address(convexBooster), 0);
  672:         SafeERC20.safeApprove(address(metaPool), address(convexBooster), amount);

contracts-full/ThreePoolAssetManager.sol:
  782:             SafeERC20.safeApprove(address(tokens[i]), address(threePool), 0);
  783:             SafeERC20.safeApprove(address(tokens[i]), address(threePool), amounts[i]);
  838:         SafeERC20.safeApprove(address(token), address(threePool), 0);
  839:         SafeERC20.safeApprove(address(token), address(threePool), amount);
  879:         SafeERC20.safeApprove(address(threePoolToken), address(threePool), 0);
  880:         SafeERC20.safeApprove(address(threePoolToken), address(threePool), amount);
  908:             SafeERC20.safeApprove(address(tokens[i]), address(metaPool), 0);
  909:             SafeERC20.safeApprove(address(tokens[i]), address(metaPool), amounts[i]);
  944:         SafeERC20.safeApprove(address(token), address(metaPool), 0);
  945:         SafeERC20.safeApprove(address(token), address(metaPool), amount);
  987:         SafeERC20.safeApprove(address(metaPool), address(convexBooster), 0);
  988:         SafeERC20.safeApprove(address(metaPool), address(convexBooster), amount);

contracts-full/TransmuterBuffer.sol:
  236:                 TokenUtils.safeApprove(registeredUnderlyings[i], alchemist, 0); ////
  238:             TokenUtils.safeApprove(debtToken, alchemist, 0); ////
  243:             TokenUtils.safeApprove(registeredUnderlyings[i], alchemist, type(uint256).max); ////
  245:         TokenUtils.safeApprove(debtToken, alchemist, type(uint256).max); ////
  284:         TokenUtils.safeApprove(underlyingToken, alchemist, type(uint256).max); ////

contracts-full/adapters/fuse/FuseTokenAdapterV1.sol:
  71:         SafeERC20.safeApprove(underlyingToken, token, amount);

contracts-full/adapters/lido/WstETHAdapterV1.sol:
  105:         SafeERC20.safeApprove(parentToken, address(token), mintedStEth);
  129:         SafeERC20.safeApprove(parentToken, curvePool, unwrappedStEth);

contracts-full/adapters/vesper/VesperAdapterV1.sol:
  62:         SafeERC20.safeApprove(underlyingToken, token, amount);

contracts-full/adapters/yearn/YearnTokenAdapter.sol:
  32:         TokenUtils.safeApprove(underlyingToken, token, amount);

[N-01] Open TODOS

Consider resolving the TODOs before deploying.

File: IStableMetaPool.sol
6: /// @dev TODO
7: uint256 constant N_COINS = 2;

[N-02] Deprecated library used for Solidity 0.8.+ SafeMath

contracts-full/TransmuterBuffer.sol:
   7: import "@openzeppelin/contracts/utils/math/SafeMath.sol";
  27:     using SafeMath for uint256;

[N-03] Unused named returns

Using both named returns and a return statement isn't necessary. Removing one of those can improve code clarity:

contracts-full/EthAssetManager.sol:
  367:     ) external lock onlyOperator returns (uint256 minted) { //@audit unused named returns
  380:     ) external lock onlyOperator returns (uint256 minted) { //@audit unused named returns
  393:     ) external lock onlyOperator returns (uint256 withdrawn) { //@audit unused named returns
  404:     ) external lock onlyOperator returns (bool success) { //@audit unused named returns
  415:     ) external lock onlyOperator returns (bool success) { //@audit unused named returns
  521:     ) external lock onlyAdmin returns (bytes memory result) { //@audit unused named returns

contracts-full/ThreePoolAssetManager.sol:
  334:     ) public view returns (uint256 delta, bool add) { //@audit unused named returns
  534:     ) external lock onlyOperator returns (uint256 minted) { //@audit unused named returns
  547:     ) external lock onlyOperator returns (uint256 minted) { //@audit unused named returns
  560:     ) external lock onlyOperator returns (uint256 withdrawn) { //@audit unused named returns
  571:     ) external lock onlyOperator returns (uint256 minted) { //@audit unused named returns
  584:     ) external lock onlyOperator returns (uint256 minted) { //@audit unused named returns
  597:     ) external lock onlyOperator returns (uint256 withdrawn) { //@audit unused named returns
  608:     ) external lock onlyOperator returns (bool success) { //@audit unused named returns
  619:     ) external lock onlyOperator returns (bool success) { //@audit unused named returns

contracts-full/TransmuterBuffer.sol:
  134:         returns (uint256 weight) //@audit unused named returns

contracts-full/TransmuterV2.sol:
  350:   function getUnexchangedBalance(address owner) external view override returns (uint256 unexchangedBalance) { //@audit unused named returns
  370:   function getExchangedBalance(address owner) external view override returns (uint256 exchangedBalance) { //@audit unused named returns
  374:   function getClaimableBalance(address owner) external view override returns (uint256 claimableBalance) { //@audit unused named returns
  554:   function _getExchangedBalance(address owner) internal view returns (uint256 exchangedBalance) { //@audit unused named returns

#0 - 0xfoobar

2022-05-30T07:40:42Z

Good QA

Table of Contents:

Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it

To help the optimizer, declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array.

The effect can be quite significant.

As an example, instead of repeatedly calling someMap[someIndex], save its reference like this: SomeStruct storage someStruct = someMap[someIndex] and use it.

Instances include (check the @audit tags):

  • contracts-full/AlchemistV2.sol:
   897:         uint256 shares = _yieldTokens[yieldToken].totalShares - _accounts[msg.sender].balances[yieldToken]; //@audit gas: for storage optimization, declare "YieldTokenParams storage _yieldToken = _yieldTokens[yieldToken];" like L912 and "Account storage _account = _accounts[msg.sender];" like L1365
   899:         _yieldTokens[yieldToken].accruedWeight += amount * FIXED_POINT_SCALAR / shares;//@audit gas: use suggested _yieldToken
   900:         _accounts[msg.sender].lastAccruedWeights[yieldToken] = _yieldTokens[yieldToken].accruedWeight;//@audit gas: use suggested _account and _yieldToken

  1003:         uint256 activeBalance = _yieldTokens[yieldToken].activeBalance;//@audit gas: for storage optimization, declare "YieldTokenParams storage _yieldToken = _yieldTokens[yieldToken];" 
  1009:         uint256 expectedValue = _yieldTokens[yieldToken].expectedValue; //@audit gas: use suggested _yieldToken
  1018:         _yieldTokens[yieldToken].activeBalance -= harvestable; //@audit gas: use suggested _yieldToken
  1019:         _yieldTokens[yieldToken].harvestableBalance += harvestable; //@audit gas: use suggested _yieldToken

  1460:         Sets.AddressSet storage depositedTokens = _accounts[owner].depositedTokens; //@audit gas: should declare "Account storage _account = _accounts[owner];" like L1365
  1464:             uint256 shares                 = _accounts[owner].balances[yieldToken]; //@audit gas: should use suggested _account

  1521:         int256 debt = _accounts[owner].debt; //@audit gas: should declare "Account storage _account = _accounts[owner];"
  1523:         Sets.AddressSet storage depositedTokens = _accounts[owner].depositedTokens; //@audit gas: should use suggested _account
  1527:             uint256 currentAccruedWeight = _yieldTokens[yieldToken].accruedWeight;//@audit gas: for storage optimization, declare "YieldTokenParams storage _yieldToken = _yieldTokens[yieldToken];" 
  1528:             uint256 lastAccruedWeight    = _accounts[owner].lastAccruedWeights[yieldToken]; //@audit gas: should use suggested _account
  1532:                 ? unlockedCredit * FIXED_POINT_SCALAR / _yieldTokens[yieldToken].totalShares  //@audit gas: should use suggested _yieldToken
  1539:             uint256 balance = _accounts[owner].balances[yieldToken]; //@audit gas: should use suggested _account

Use of the memory keyword when storage should be used

When copying a state struct in memory, there are as many SLOADs and MSTOREs as there are slots. When reading the whole struct multiple times is not needed, it's better to actually only read the relevant field(s). When only some of the fields are read several times, these particular values should be cached instead of the whole state struct.

Here, the storage keyword should be used instead of memory (see @audit tags):

  • contracts-full/AlchemistV2.sol:
  1222      function _sync(
  1223          address yieldToken,
  1224          uint256 amount,
  1225          function(uint256, uint256) internal pure returns (uint256) operation
  1226      ) internal {
  1227:         YieldTokenParams memory yieldTokenParams = _yieldTokens[yieldToken]; //@audit gas: should use storage instead of memory (only 2 SLOADs are necessary vs 14 done here for the copy in memory)
  1228  
  1229          uint256 amountUnderlyingTokens = _convertYieldTokensToUnderlying(yieldToken, amount);
  1230          uint256 updatedActiveBalance   = operation(yieldTokenParams.activeBalance, amount); //@audit gas: yieldTokenParams occurence 1
  1231          uint256 updatedExpectedValue   = operation(yieldTokenParams.expectedValue, amountUnderlyingTokens); //@audit gas: yieldTokenParams occurence 2
  1232  
  1233          _yieldTokens[yieldToken].activeBalance = updatedActiveBalance;
  1234          _yieldTokens[yieldToken].expectedValue = updatedExpectedValue;
  1235      }

  1243      function _loss(address yieldToken) internal view returns (uint256) {
  1244:         YieldTokenParams memory yieldTokenParams = _yieldTokens[yieldToken];  //@audit gas: should use storage instead of memory (only 2 SLOADs are necessary vs 14 done here for the copy in memory)
  1245  
  1246          uint256 amountUnderlyingTokens = _convertYieldTokensToUnderlying(yieldToken, yieldTokenParams.activeBalance); //@audit gas: yieldTokenParams occurence 1
  1247          uint256 expectedUnderlyingValue = yieldTokenParams.expectedValue; //@audit gas: yieldTokenParams occurence 2
  1248  
  1249          return expectedUnderlyingValue > amountUnderlyingTokens
  1250              ? ((expectedUnderlyingValue - amountUnderlyingTokens) * BPS) / expectedUnderlyingValue
  1251              : 0;
  1252      }

Caching storage values in memory

The code can be optimized by minimising the number of SLOADs.

SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.

See the @audit tags for details about the multiple SLOADs where a cached value should be used instead of SLOAD 2 and above:

  • contracts-full/AlchemistV2.sol:
   289:         _checkState(pendingAdmin != address(0)); //@audit gas: SLOAD 1 (pendingAdmin) 
   291:         if (msg.sender != pendingAdmin) { //@audit gas: SLOAD 2 (pendingAdmin)
   295:         admin = pendingAdmin;//@audit gas: SLOAD 3 (pendingAdmin)
   298:         emit AdminUpdated(admin); //@audit gas: SLOAD 4 (previous pendingAdmin that should've been cached)

   876:         TokenUtils.safeTransfer(underlyingToken, transmuter, amountUnderlyingTokens); //@audit gas: SLOAD 1 (transmuter)
   879:         IERC20TokenReceiver(transmuter).onERC20Received(underlyingToken, amountUnderlyingTokens); //@audit gas: SLOAD 2 (transmuter)

   942:         TokenUtils.safeTransfer(underlyingToken, transmuter, distributeAmount); //@audit gas: SLOAD 1 (transmuter)
   945:         IERC20TokenReceiver(transmuter).onERC20Received(underlyingToken, distributeAmount); //@audit gas: SLOAD 2 (transmuter)

  1608:         if (_yieldTokens[yieldToken].totalShares == 0) { //@audit gas: SLOAD 1 (_yieldTokens[yieldToken].totalShares), should cache like in _convertSharesToYieldTokens
  1611:         return amount * _yieldTokens[yieldToken].totalShares / _calculateUnrealizedActiveBalance(yieldToken); //@audit gas: SLOAD 2 (_yieldTokens[yieldToken].totalShares)
  • contracts-full/EthAssetManager.sol:
  304:         if (pendingAdmin == address(0)) { //@audit gas: SLOAD 1 (pendingAdmin)
  308:         if (pendingAdmin != msg.sender) { //@audit gas: SLOAD 2 (pendingAdmin)
  312:         admin = pendingAdmin; //@audit gas: SLOAD 3 (pendingAdmin)
  315:         emit AdminUpdated(admin); //@audit gas: SLOAD 4 (pendingAdmin), as this SLOAD could be avoided by using the suggested cache
  
  496:         if (amount > (balance = weth.balanceOf(address(this)))) weth.deposit{value: amount - balance}(); //@audit gas: SLOAD 1 & 2 (weth)
  498:         SafeERC20.safeTransfer(address(weth), transmuterBuffer, amount); //@audit gas: SLOAD 1 (transmuterBuffer) + SLOAD 3 (weth)
  500:         IERC20TokenReceiver(transmuterBuffer).onERC20Received(address(weth), amount); //@audit gas: SLOAD 2 (transmuterBuffer) + SLOAD 4 (weth) 

  698:         SafeERC20.safeTransfer(address(curveToken), rewardReceiver, curveBalance);  //@audit gas: SLOAD 1 (rewardReceiver)
  699:         SafeERC20.safeTransfer(address(convexToken), rewardReceiver, convexBalance); //@audit gas: SLOAD 2 (rewardReceiver)
  • contracts-full/gALCX.sol:
  50:         uint poolBalance = pools.getStakeTotalDeposited(address(this), poolId); //@audit SLOAD 1 (poolId)
  51:         pools.withdraw(poolId, poolBalance); //@audit SLOAD 2 (poolId)
  58:         pools.deposit(poolId, balance); //@audit SLOAD 3 (poolId), should use _poolId
  
  71:         pools.claim(poolId); //@audit SLOAD 1 (poolId)
  76:             exchangeRate += (balance * exchangeRatePrecision) / totalSupply; //@audit SLOAD 1 (exchangeRate), should cache the calculation in a var
  77:             emit ExchangeRateChange(exchangeRate); //@audit SLOAD 2 (exchangeRate)
  79:             pools.deposit(poolId, balance); //@audit SLOAD 2 (poolId)
  • contracts-full/StakingPools.sol:
  131:     require(msg.sender == pendingGovernance, "StakingPools: only pending governance"); //@audit SLOAD 1 (pendingGovernance)
  133:     address _pendingGovernance = pendingGovernance; //@audit SLOAD 2 (pendingGovernance), should declare this earlier and use _pendingGovernance L131
  • contracts-full/ThreePoolAssetManager.sol:
   456:         if (pendingAdmin == address(0)) { //@audit SLOAD 1 (pendingAdmin)
   460:         if (pendingAdmin != msg.sender) { //@audit SLOAD 2 (pendingAdmin)
   464:         admin = pendingAdmin; //@audit SLOAD 3 (pendingAdmin)
   467:         emit AdminUpdated(admin); //@audit SLOAD 4 (pendingAdmin), could've been avoided by caching pendingAdmin in memory before doing it in "admin"

   632:         SafeERC20.safeTransfer(address(curveToken), rewardReceiver, curveBalance); //@audit SLOAD 1 (rewardReceiver)
   633:         SafeERC20.safeTransfer(address(convexToken), rewardReceiver, convexBalance); //@audit SLOAD 2 (rewardReceiver)

   719:         SafeERC20.safeTransfer(address(token), transmuterBuffer, amount); //@audit SLOAD 1 (transmuterBuffer)
   721:         IERC20TokenReceiver(transmuterBuffer).onERC20Received(address(token), amount); //@audit SLOAD 2 (transmuterBuffer)

  1014:         SafeERC20.safeTransfer(address(curveToken), rewardReceiver, curveBalance); //@audit SLOAD 1 (rewardReceiver)
  1015:         SafeERC20.safeTransfer(address(convexToken), rewardReceiver, convexBalance); //@audit SLOAD 2 (rewardReceiver)
  • contracts-full/TransmuterBuffer.sol:
  151:         if (totalUnderlyingBuffered < flowAvailable[underlyingToken]) {//@audit SLOAD 1 (flowAvailable[underlyingToken])
  154:             return flowAvailable[underlyingToken];//@audit SLOAD 2 (flowAvailable[underlyingToken])

  231:         sources[alchemist] = false; //@audit SLOAD 1 (alchemist)
  234:         if (alchemist != address(0)) { //@audit SLOAD 2 (alchemist)
  236:                 TokenUtils.safeApprove(registeredUnderlyings[i], alchemist, 0); //@audit SLOAD 3 (alchemist)
  238:             TokenUtils.safeApprove(debtToken, alchemist, 0); //@audit SLOAD 4 (alchemist)
  243:             TokenUtils.safeApprove(registeredUnderlyings[i], alchemist, type(uint256).max); //@audit SLOAD 5, should use _alchemist
  245:         TokenUtils.safeApprove(debtToken, alchemist, type(uint256).max); //@audit SLOAD 6, should use _alchemist
  247:         emit SetAlchemist(alchemist); //@audit SLOAD 7, should use _alchemist

  267:         if (!IAlchemistV2(alchemist).isSupportedUnderlyingToken(underlyingToken)) { //@audit SLOAD 1 (alchemist)
  284:         TokenUtils.safeApprove(underlyingToken, alchemist, type(uint256).max); //@audit SLOAD 2 (alchemist)

  315:             if (localBalance < flowAvailable[underlyingToken]) { //@audit SLOAD 1 (flowAvailable[underlyingToken])
  319:                 uint256 exchangeable = flowAvailable[underlyingToken] - currentExchanged[underlyingToken]; //@audit SLOAD 2 (flowAvailable[underlyingToken]) + SLOAD 1 (currentExchanged[underlyingToken])
  320:                 currentExchanged[underlyingToken] += exchangeable;//@audit SLOAD 2 (currentExchanged[underlyingToken])

  346:         if (amount > flowAvailable[underlyingToken]) { //@audit SLOAD 1 (flowAvailable[underlyingToken])
  355:         flowAvailable[underlyingToken] -= amount;  //@audit gas: should be unchecked due to L346-L347 + //@audit SLOAD 2 (flowAvailable[underlyingToken])

  372:         address[] memory supportedYieldTokens = IAlchemistV2(alchemist) //@audit SLOAD 1 (alchemist)
  374:         address[] memory supportedUnderlyingTokens = IAlchemistV2(alchemist) //@audit SLOAD 2 (alchemist)
  377:         if (registeredUnderlyings.length != supportedUnderlyingTokens.length) { //@audit SLOAD 1 (registeredUnderlyings.length)
  382:         for (uint256 j = 0; j < registeredUnderlyings.length; j++) { //@audit SLOAD 2+ in loop (registeredUnderlyings.length)
  390:             IAlchemistV2.YieldTokenParams memory params = IAlchemistV2(alchemist) //@audit SLOAD 3+ in loop (alchemist)

  401:         IAlchemistV2(alchemist).poke(address(this)); //@audit SLOAD 1 (alchemist)
  406:         IAlchemistV2(alchemist).mint(credit, address(this)); //@audit SLOAD 2 (alchemist)

  443:         (uint256 balance, ) = IAlchemistV2(alchemist).positions(address(this), yieldToken); //@audit SLOAD 1 (alchemist)
  444:         IAlchemistV2.YieldTokenParams memory params = IAlchemistV2(alchemist) //@audit SLOAD 2 (alchemist)
  446:         uint256 tokensPerShare = IAlchemistV2(alchemist) //@audit SLOAD 3 (alchemist)

  457:             lastFlowrateUpdate[underlyingToken]) * flowRate[underlyingToken]; //@audit SLOAD 1 (flowRate[underlyingToken]) and SLOAD 1 (lastFlowrateUpdate[underlyingToken]) 
  458:         flowAvailable[underlyingToken] += marginalFlow; //@audit SLOAD 2 (flowRate[underlyingToken])
  459:         lastFlowrateUpdate[underlyingToken] = block.timestamp; //@audit SLOAD 2 (lastFlowrateUpdate[underlyingToken]) 

  513:         uint256 pricePerShare = IAlchemistV2(alchemist).getUnderlyingTokensPerShare(token); //@audit SLOAD 1 (alchemist)
  515:         (uint256 availableShares, uint256 lastAccruedWeight) = IAlchemistV2(alchemist).positions(address(this), token); //@audit SLOAD 2 (alchemist)
  522:             IAlchemistV2(alchemist).withdrawUnderlying(token, wantShares, address(this), minimumAmountOut); //@audit SLOAD 3 (alchemist)

  536:         if (totalUnderlyingBuffered < flowAvailable[underlyingToken]) { //@audit SLOAD 1 (flowAvailable[underlyingToken])
  539:         } else if (initialLocalBalance < flowAvailable[underlyingToken]) { //@audit SLOAD 2 (flowAvailable[underlyingToken])
  541:             want = flowAvailable[underlyingToken] - initialLocalBalance; //@audit SLOAD 3 (flowAvailable[underlyingToken])
  550:         if (localBalance > flowAvailable[underlyingToken]) { //@audit SLOAD 4 (flowAvailable[underlyingToken])
  551:             exchangeDelta = flowAvailable[underlyingToken] - currentExchanged[underlyingToken];  //@audit SLOAD 5 (flowAvailable[underlyingToken]) +  //@audit SLOAD 1 (currentExchanged[underlyingToken])
  553:             exchangeDelta = localBalance - currentExchanged[underlyingToken]; //@audit SLOAD 1 (currentExchanged[underlyingToken])
  557:             currentExchanged[underlyingToken] += exchangeDelta; //@audit SLOAD 2 (currentExchanged[underlyingToken])

  567:         TokenUtils.safeTransfer(underlyingToken, amos[underlyingToken], amount);  //@audit gas: SLOAD 1 (amos[underlyingToken])
  568:         IERC20TokenReceiver(amos[underlyingToken]).onERC20Received(underlyingToken, amount); //@audit gas: SLOAD 2 (amos[underlyingToken])
  • contracts-full/TransmuterConduit.sol:
  35:         IERC20(token).safeTransferFrom(origin, sinkTransmuter, amount);//@audit SLOAD 1 (token) and SLOAD 1 (sinkTransmuter)
  36:         IERC20TokenReceiver(sinkTransmuter).onERC20Received(token, amount);//@audit SLOAD 2 (token) and SLOAD 2 (sinkTransmuter)
  • contracts-full/TransmuterV2.sol:
  557:     if (account.occupiedTick <= satisfiedTick) { //@audit SLOAD 1 (account.occupiedTick)
  567:       ticks.getWeight(account.occupiedTick, ticks.position) //@audit SLOAD 2 (account.occupiedTick)

Avoid writing twice in storage

SSTOREs are expensive.

Instead of potentially making multiple double-SSTOREs in for-loops here:

File: EthAssetManager.sol
214:         for (uint256 i = 0; i < NUM_META_COINS; i++) {
215:             _metaPoolAssetCache[i] = params.metaPool.coins(i); //@audit gas SSTORE 1
216:             if (_metaPoolAssetCache[i] == IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { //@audit SLOAD
217:                 _metaPoolAssetCache[i] = weth; //@audit SSTORE 2 + SLOAD
218:             }
219:         }

Consider using a ternary operator L215 and removing the rest:

_metaPoolAssetCache[i] = params.metaPool.coins(i) == IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) ? weth : params.metaPool.coins(i);

Avoid writing twice in memory

While MSTOREs are not expensive, the following could be optimized (see @audit tags):

File: TransmuterV2.sol
554:   function _getExchangedBalance(address owner) internal view returns (uint256 exchangedBalance) {
555:     Account storage account = accounts[owner];
556: 
557:     if (account.occupiedTick <= satisfiedTick) {
558:       exchangedBalance = account.exchangedBalance; //@audit gas: see below
559:       exchangedBalance += account.unexchangedBalance; //@audit gas: would be less expensive with "exchangedBalance = account.exchangedBalance + account.unexchangedBalance;"
560:       return exchangedBalance;
561:     }
562: 
563:     exchangedBalance = account.exchangedBalance; //@audit gas: see L570
564: 
565:     uint256 exchanged = LiquidityMath.calculateProduct(
566:       account.unexchangedBalance,
567:       ticks.getWeight(account.occupiedTick, ticks.position)
568:     );
569: 
570:     exchangedBalance += exchanged; //@audit gas: would be less expensive with "exchangedBalance = account.exchangedBalance + exchanged;"
571: 
572:     return exchangedBalance;
573:   }

Avoid emitting a storage variable when a memory value is available

When they are the same, consider emitting the memory value instead of the storage value:

contracts-full/AlchemicTokenV2.sol:
  92    function setFlashFee(uint256 newFee) external onlyAdmin {
  93      flashMintFee = newFee;
  94:     emit SetFlashMintFee(flashMintFee); //@audit SLOAD (flashMintFee), should emit newFee
  95    }

contracts-full/EthAssetManager.sol:
  221:         emit AdminUpdated(admin); //@audit gas: SLOAD, use params.admin
  222:         emit OperatorUpdated(operator); //@audit gas: SLOAD, use params.operator
  223:         emit RewardReceiverUpdated(rewardReceiver); //@audit gas: SLOAD, use params.rewardReceiver
  224:         emit TransmuterBufferUpdated(transmuterBuffer); //@audit gas: SLOAD, use params.transmuterBuffer
  225:         emit MetaPoolSlippageUpdated(metaPoolSlippage); //@audit gas: SLOAD, use params.metaPoolSlippage

contracts-full/ThreePoolAssetManager.sol:
   258:         emit AdminUpdated(admin); //@audit gas: SLOAD, should use params.admin
   259:         emit OperatorUpdated(operator); //@audit gas: SLOAD, should use params.operator
   260:         emit RewardReceiverUpdated(rewardReceiver); //@audit gas: SLOAD, should use params.rewardReceiver
   261:         emit TransmuterBufferUpdated(transmuterBuffer); //@audit gas: SLOAD, should use params.transmuterBuffer
   262:         emit ThreePoolSlippageUpdated(threePoolSlippage); //@audit gas: SLOAD, should use params.threePoolSlippage
   263:         emit MetaPoolSlippageUpdated(metaPoolSlippage); //@audit gas: SLOAD, should use params.metaPoolSlippage

SafeMath is not needed when using Solidity version 0.8.*

Solidity version 0.8.already implements overflow and underflow checks by default. Using the SafeMath library from OpenZeppelin (which is more gas expensive than the 0.8. overflow checks) is therefore redundant.

Instances include:

TransmuterBuffer.sol:7:import "@openzeppelin/contracts/utils/math/SafeMath.sol"; TransmuterBuffer.sol:27: using SafeMath for uint256;

Consider using the built-in checks instead of SafeMath and remove SafeMath from the dependencies

Unchecking arithmetics operations that can't underflow/overflow

Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block: https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmetic

I suggest wrapping with an unchecked block here (see @audit tags for more details):

  • contracts-full/AlchemistV2.sol:
  1010          if (currentValue <= expectedValue) {
  1011              return;
  1012          }
  1013  
  1014:         uint256 harvestable = _convertUnderlyingTokensToYield(yieldToken, currentValue - expectedValue); //@audit gas: should be unchecked due to L1010-L1011
  • contracts-full/EthAssetManager.sol:
  496:         if (amount > (balance = weth.balanceOf(address(this)))) weth.deposit{value: amount - balance}(); //@audit gas: should be unchecked due to condition

  547          if (cliff >= totalCliffs) return 0;
  548  
  549:         uint256 reduction = totalCliffs - cliff; //@audit gas: should be unchecked due to L547
  550:         uint256 earned    = amountCurve * reduction / totalCliffs; //@audit gas: should be unchecked due to "(reduction / totalCliffs) < 1"

  589:         if (value > address(this).balance) weth.withdraw(value - address(this).balance);  //@audit gas: should be unchecked due to condition

  629:         if (value > address(this).balance) weth.withdraw(value - address(this).balance); //@audit gas: should be unchecked due to condition

  720      function abs(uint256 x , uint256 y) private pure returns (uint256) {
  721:         return x > y ? x - y : y - x; //@audit gas: should be unchecked due to condition 
  722      }
  • contracts-full/ThreePoolAssetManager.sol:
   399          return v.minimizedBalance > v.startingBalance
   400:             ? (v.minimizedBalance - v.startingBalance, true) //@audit gas: should be unchecked due to condition
   401:             : (v.startingBalance - v.minimizedBalance, false); //@audit gas: should be unchecked due to condition

   749          if (cliff >= totalCliffs) return 0;
   750  
   751:         uint256 reduction = totalCliffs - cliff; //@audit gas: should be unchecked due to L749

  1036      function abs(uint256 x , uint256 y) private pure returns (uint256) {
  1037:         return x > y ? x - y : y - x;  //@audit gas: should be unchecked due to condition
  1038      }
  1039  }
  • contracts-full/TransmuterBuffer.sol:
  346          if (amount > flowAvailable[underlyingToken]) { ////
  347              revert IllegalArgument();
  348          }
  349  
  350          uint256 localBalance = TokenUtils.safeBalanceOf(underlyingToken, address(this));
  351          if (amount > localBalance) {
  352              revert IllegalArgument();
  353          }
  354  
  355:         flowAvailable[underlyingToken] -= amount;  //@audit gas: should be unchecked due to L346-L347

  421          if (localBalance < amount) {
  422              revert IllegalArgument();
  423          }
  424          _updateFlow(underlyingToken);
  425          
  426          // Don't deposit exchanged funds into the Alchemist.
  427          // Doing so puts those funds at risk, and could lead to users being unable to claim
  428          // their transmuted funds in the event of a vault loss.
  429:         if (localBalance - amount < currentExchanged[underlyingToken]) { //@audit gas: should be unchecked due to L421

  539          } else if (initialLocalBalance < flowAvailable[underlyingToken]) { ////
  540              // totalUnderlyingBuffered > flowAvailable so we have funds available to pull.
  541:             want = flowAvailable[underlyingToken] - initialLocalBalance; //@audit gas: should be unchecked due to condition L539
  542          }
  • contracts-full/TransmuterV2.sol:
  306        if (state.maximumWeight.n < FixedPointMath.ONE) {
  307          break;
  308        }
  309  
  310        // Calculate how much weight of the distributed weight is dust.
  311:       state.dustedWeight = FixedPointMath.Number(state.maximumWeight.n - FixedPointMath.ONE); //@audit gas: should be unchecked due to L306-L307

Tighly pack struct IAlchemistV2State.UnderlyingTokenParams

From :

06:     /// @notice Defines underlying token parameters.
07:     struct UnderlyingTokenParams { //@audit gas: can be tightly packed
08:         // The number of decimals the token has. This value is cached once upon registering the token so it is important
09:         // that the decimals of the token are immutable or the system will begin to have computation errors.
10:         uint8 decimals; //@audit-info gas: 1 byte
11:         // A coefficient used to normalize the token to a value comparable to the debt token. For example, if the
12:         // underlying token is 8 decimals and the debt token is 18 decimals then the conversion factor will be
13:         // 10^10. One unit of the underlying token will be comparably equal to one unit of the debt token.
14:         uint256 conversionFactor; //@audit-info gas: 32 bytes
15:         // A flag to indicate if the token is enabled.
16:         bool enabled; //@audit-info gas: 1 byte
17:     }

to:

    struct UnderlyingTokenParams { //@audit gas: can be tightly packed
        // The number of decimals the token has. This value is cached once upon registering the token so it is important
        // that the decimals of the token are immutable or the system will begin to have computation errors.
        uint8 decimals; //@audit-info gas: 1 byte
                // A flag to indicate if the token is enabled.
        bool enabled; //@audit-info gas: 1 byte
        // A coefficient used to normalize the token to a value comparable to the debt token. For example, if the
        // underlying token is 8 decimals and the debt token is 18 decimals then the conversion factor will be
        // 10^10. One unit of the underlying token will be comparably equal to one unit of the debt token.
        uint256 conversionFactor; //@audit-info gas: 32 bytes
    }

Which would save 1 storage slot.

Tighly pack struct IAlchemistV2State.YieldTokenParams

From:

19:     /// @notice Defines yield token parameters.
20:     struct YieldTokenParams { //@audit gas: can be tightly packed
21:         // The number of decimals the token has. This value is cached once upon registering the token so it is important
22:         // that the decimals of the token are immutable or the system will begin to have computation errors.
23:         uint8 decimals; //@audit-info gas: 1 byte
24:         // The associated underlying token that can be redeemed for the yield-token.
25:         address underlyingToken; //@audit-info gas: 20 bytes
26:         // The adapter used by the system to wrap, unwrap, and lookup the conversion rate of this token into its
27:         // underlying token.
28:         address adapter; //@audit-info gas: 20 bytes
29:         // The maximum percentage loss that is acceptable before disabling certain actions.
30:         uint256 maximumLoss; //@audit-info gas: 32 bytes
31:         // The maximum value of yield tokens that the system can hold, measured in units of the underlying token.
32:         uint256 maximumExpectedValue;
33:         // The percent of credit that will be unlocked per block. The representation of this value is a 18  decimal
34:         // fixed point integer.
35:         uint256 creditUnlockRate;
36:         // The current balance of yield tokens which are held by users.
37:         uint256 activeBalance;
38:         // The current balance of yield tokens which are earmarked to be harvested by the system at a later time.
39:         uint256 harvestableBalance;
40:         // The total number of shares that have been minted for this token.
41:         uint256 totalShares;
42:         // The expected value of the tokens measured in underlying tokens. This value controls how much of the token
43:         // can be harvested. When users deposit yield tokens, it increases the expected value by how much the tokens
44:         // are exchangeable for in the underlying token. When users withdraw yield tokens, it decreases the expected
45:         // value by how much the tokens are exchangeable for in the underlying token.
46:         uint256 expectedValue;
47:         // The current amount of credit which is will be distributed over time to depositors.
48:         uint256 pendingCredit;
49:         // The amount of the pending credit that has been distributed.
50:         uint256 distributedCredit;
51:         // The block number which the last credit distribution occurred.
52:         uint256 lastDistributionBlock;
53:         // The total accrued weight. This is used to calculate how much credit a user has been granted over time. The
54:         // representation of this value is a 18 decimal fixed point integer.
55:         uint256 accruedWeight; //@audit-info gas: 32 bytes
56:         // A flag to indicate if the token is enabled.
57:         bool enabled; //@audit-info gas: 1 byte (should move as it's taking a whole slot but is just 1 byte)
58:     }

to:

    struct YieldTokenParams { //@audit gas: can be tightly packed
        // A flag to indicate if the token is enabled.
        bool enabled; //@audit-info gas: 1 byte (this moved up)
        // The number of decimals the token has. This value is cached once upon registering the token so it is important
        // that the decimals of the token are immutable or the system will begin to have computation errors.
        uint8 decimals; //@audit-info gas: 1 byte
        // The associated underlying token that can be redeemed for the yield-token.
        address underlyingToken; //@audit-info gas: 20 bytes
        // The adapter used by the system to wrap, unwrap, and lookup the conversion rate of this token into its
        // underlying token.
        address adapter; //@audit-info gas: 20 bytes
        // The maximum percentage loss that is acceptable before disabling certain actions.
        uint256 maximumLoss; //@audit-info gas: 32 bytes
        // The maximum value of yield tokens that the system can hold, measured in units of the underlying token.
        uint256 maximumExpectedValue;
        // The percent of credit that will be unlocked per block. The representation of this value is a 18  decimal
        // fixed point integer.
        uint256 creditUnlockRate;
        // The current balance of yield tokens which are held by users.
        uint256 activeBalance;
        // The current balance of yield tokens which are earmarked to be harvested by the system at a later time.
        uint256 harvestableBalance;
        // The total number of shares that have been minted for this token.
        uint256 totalShares;
        // The expected value of the tokens measured in underlying tokens. This value controls how much of the token
        // can be harvested. When users deposit yield tokens, it increases the expected value by how much the tokens
        // are exchangeable for in the underlying token. When users withdraw yield tokens, it decreases the expected
        // value by how much the tokens are exchangeable for in the underlying token.
        uint256 expectedValue;
        // The current amount of credit which is will be distributed over time to depositors.
        uint256 pendingCredit;
        // The amount of the pending credit that has been distributed.
        uint256 distributedCredit;
        // The block number which the last credit distribution occurred.
        uint256 lastDistributionBlock;
        // The total accrued weight. This is used to calculate how much credit a user has been granted over time. The
        // representation of this value is a 18 decimal fixed point integer.
        uint256 accruedWeight; //@audit-info gas: 32 bytes
    }

Which would save 1 storage slot.

Tighly pack struct IYearnVaultV2.StrategyParams

From

10:   struct StrategyParams { //@audit gas: can be tightly packed
11:     uint256 performanceFee;
12:     uint256 activation;
13:     uint256 debtRatio;
14:     uint256 minDebtPerHarvest;
15:     uint256 maxDebtPerHarvest;
16:     uint256 lastReport;
17:     uint256 totalDebt;
18:     uint256 totalGain;
19:     uint256 totalLoss; //@audit-info gas: 32 bytes
20:     bool enforceChangeLimit; //@audit-info gas: 1 byte (this should move to the end)
21:     uint256 profitLimitRatio; //@audit-info gas: 32 bytes
22:     uint256 lossLimitRatio; //@audit-info gas: 32 bytes
23:     address customCheck; //@audit-info gas: 20 bytes
24:   }

to

  struct StrategyParams { //@audit gas: can be tightly packed
    uint256 performanceFee;
    uint256 activation;
    uint256 debtRatio;
    uint256 minDebtPerHarvest;
    uint256 maxDebtPerHarvest;
    uint256 lastReport;
    uint256 totalDebt;
    uint256 totalGain;
    uint256 totalLoss; //@audit-info gas: 32 bytes
    uint256 profitLimitRatio; //@audit-info gas: 32 bytes
    uint256 lossLimitRatio; //@audit-info gas: 32 bytes
    address customCheck; //@audit-info gas: 20 bytes
    bool enforceChangeLimit; //@audit-info gas: 1 byte (this moved)
  }

Which would save 1 storage slot.

>= is cheaper than >

Strict inequalities (>) are more expensive than non-strict ones (>=). This is due to some supplementary checks (ISZERO, 3 gas)

I suggest using >= instead of > to avoid some opcodes here:

AlchemistV2.sol:729:        uint256 credit = amount > uint256(debt) ? uint256(debt) : amount;
AlchemistV2.sol:773:        uint256 actualAmount = amount > maximumAmount ? maximumAmount : amount;
AlchemistV2.sol:838:        uint256 actualShares = shares > maximumShares ? maximumShares : shares;
EthAssetManager.sol:553:        return earned > available ? available : earned;
EthAssetManager.sol:711:        return x > y ? y : x;
EthAssetManager.sol:721:        return x > y ? x - y : y - x;
ThreePoolAssetManager.sol:379:                v.minimizedBalance = currentDelta > examineDelta ? examineBalance : v.minimizedBalance;
ThreePoolAssetManager.sol:755:        return earned > available ? available : earned;
ThreePoolAssetManager.sol:1027:        return x > y ? y : x;
ThreePoolAssetManager.sol:1037:        return x > y ? x - y : y - x;

Shift Right instead of Dividing by 2

A division by 2 can be calculated by shifting one to the right.

While the DIV opcode uses 5 gas, the SHR opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.

I suggest replacing / 2 with >> 1 here:

ThreePoolAssetManager.sol:355:            if ((examineBalance = (v.maximum + v.minimum) / 2) == previousBalance) break;

Mutex.sol and Whitelist.sol: Switching between 1 and 2 instead of 0 and 1 (or false and true) is more gas efficient

Affected code:

SSTORE from false (0) to true (1) (or any non-zero value) costs 20000 gas. SSTORE from 1 to 2 (or any other non-zero value) costs 5000 gas.

By storing the original value once again, a refund is triggered (https://eips.ethereum.org/EIPS/eip-2200).

Since refunds are capped to a percentage of the total transaction's gas, it is best to keep them low, to increase the likelihood of the full refund coming into effect.

Therefore, switching between 1, 2 instead of 0, 1 will be more gas efficient.

See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/86bd4d73896afcb35a205456e361436701823c7a/contracts/security/ReentrancyGuard.sol#L29-L33

Bytes constants are more efficient than string constants

From the Solidity doc:

If you can limit the length to a certain number of bytes, always use one of bytes1 to bytes32 because they are much cheaper.

Why do Solidity examples use bytes32 type instead of string?

bytes32 uses less gas because it fits in a single word of the EVM, and string is a dynamically sized-type which has current limitations in Solidity (such as can't be returned from a function to a contract).

If data can fit into 32 bytes, then you should use bytes32 datatype rather than bytes or strings as it is cheaper in solidity. Basically, any fixed size variable in solidity is cheaper than variable size. That will save gas on the contract.

Instances of string constant that can be replaced by `bytes5 constant`` :

adapters/yearn/YearnTokenAdapter.sol:14: string public constant override version = "2.1.0"; AlchemistV2.sol:54: string public constant override version = "2.2.6"; TransmuterBuffer.sol:37: string public constant override version = "2.2.0"; TransmuterV2.sol:105: string public constant override version = "2.2.0"; WETHGateway.sol:14: string public constant version = "2.1.0";

An array's length should be cached to save gas in for-loops

Reading array length at each iteration of the loop consumes more gas than necessary.

In the best case scenario (length read on a memory variable), caching the array length in the stack saves around 3 gas per iteration. In the worst case scenario (external calls at each iteration), the amount of gas wasted can be massive.

Notice that, in StakingPools.sol, the array-reading is made from an external call.

Here, I suggest storing the array's length in a variable before the for-loop, and use this new variable instead:

base/Multicall.sol:14:        for (uint256 i = 0; i < data.length; i++) {
AlchemistV2.sol:990:        for (uint256 i = 0; i < depositedTokens.v@alues.length; i++) {
AlchemistV2.sol:1282:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1355:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1461:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1524:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
CrossChainCanonicalBase.sol:57:        for (uint256 i = 0; i < _bridgeTokens.length; i++){ 
CrossChainCanonicalBase.sol:141:        for (uint i = 0; i < bridgeTokensArray.length; i++){ 
StakingPools.sol:188:    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
StakingPools.sol:363:    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
TransmuterBuffer.sol:172:        for (uint256 i = 0; i < _yieldTokens[underlyingToken].length; i++) {
TransmuterBuffer.sol:186:        for (uint256 i = 0; i < tokens.length; i++) {
TransmuterBuffer.sol:235:            for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:242:        for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:272:        for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:382:        for (uint256 j = 0; j < registeredUnderlyings.length; j++) {
TransmuterBuffer.sol:479:        for (uint256 j = 0; j < weighting.tokens.length; j++) {

++i costs less gas compared to i++ or i += 1

++i costs less gas compared to i++ or i += 1 for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.

The same is also true for i--.

i++ increments i and returns the initial value of i. Which means:

uint i = 1;  
i++; // == 1 but i == 2  

But ++i returns the actual incremented value:

uint i = 1;  
++i; // == 2 and i == 2 too, so no need for a temporary variable  

In the first case, the compiler has to create a temporary variable (when used) for returning 1 instead of 2

Instances include:

base/Multicall.sol:14:        for (uint256 i = 0; i < data.length; i++) {
libraries/Tick.sol:38:    self.position++;
AlchemistV2.sol:990:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1282:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1355:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1461:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1524:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
CrossChainCanonicalBase.sol:57:        for (uint256 i = 0; i < _bridgeTokens.length; i++){ 
CrossChainCanonicalBase.sol:141:        for (uint i = 0; i < bridgeTokensArray.length; i++){ 
EthAssetManager.sol:214:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
EthAssetManager.sol:567:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
StakingPools.sol:188:    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
StakingPools.sol:363:    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
ThreePoolAssetManager.sol:250:        for (uint256 i = 0; i < NUM_STABLE_COINS; i++) {
ThreePoolAssetManager.sol:254:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
ThreePoolAssetManager.sol:353:        for (uint256 i = 0; i < 256; i++) {
ThreePoolAssetManager.sol:773:        for (uint256 i = 0; i < NUM_STABLE_COINS; i++) {
ThreePoolAssetManager.sol:902:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
TransmuterBuffer.sol:172:        for (uint256 i = 0; i < _yieldTokens[underlyingToken].length; i++) {
TransmuterBuffer.sol:186:        for (uint256 i = 0; i < tokens.length; i++) {
TransmuterBuffer.sol:235:            for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:242:        for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:272:        for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:382:        for (uint256 j = 0; j < registeredUnderlyings.length; j++) {
TransmuterBuffer.sol:387:        for (uint256 i = 0; i < numYTokens; i++) {
TransmuterBuffer.sol:479:        for (uint256 j = 0; j < weighting.tokens.length; j++) {

I suggest using ++i instead of i++ to increment the value of an uint variable.

Increments can be unchecked

In Solidity 0.8+, there's a default overflow check on unsigned integers. It's possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.

ethereum/solidity#10695

Instances include:

base/Multicall.sol:14:        for (uint256 i = 0; i < data.length; i++) {
AlchemistV2.sol:990:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1282:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1355:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1461:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1524:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
CrossChainCanonicalBase.sol:57:        for (uint256 i = 0; i < _bridgeTokens.length; i++){ 
CrossChainCanonicalBase.sol:141:        for (uint i = 0; i < bridgeTokensArray.length; i++){ 
EthAssetManager.sol:214:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
EthAssetManager.sol:567:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
StakingPools.sol:188:    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
StakingPools.sol:363:    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
ThreePoolAssetManager.sol:250:        for (uint256 i = 0; i < NUM_STABLE_COINS; i++) {
ThreePoolAssetManager.sol:254:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
ThreePoolAssetManager.sol:353:        for (uint256 i = 0; i < 256; i++) {
ThreePoolAssetManager.sol:773:        for (uint256 i = 0; i < NUM_STABLE_COINS; i++) {
ThreePoolAssetManager.sol:902:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
TransmuterBuffer.sol:172:        for (uint256 i = 0; i < _yieldTokens[underlyingToken].length; i++) {
TransmuterBuffer.sol:186:        for (uint256 i = 0; i < tokens.length; i++) {
TransmuterBuffer.sol:235:            for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:242:        for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:272:        for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:382:        for (uint256 j = 0; j < registeredUnderlyings.length; j++) {
TransmuterBuffer.sol:387:        for (uint256 i = 0; i < numYTokens; i++) {
TransmuterBuffer.sol:479:        for (uint256 j = 0; j < weighting.tokens.length; j++) {

The code would go from:

for (uint256 i; i < numIterations; i++) {  
 // ...  
}  

to:

for (uint256 i; i < numIterations;) {  
 // ...  
 unchecked { ++i; }  
}  

The risk of overflow is inexistant for uint256 here.

No need to explicitly initialize variables with default values

If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.

As an example: for (uint256 i = 0; i < numIterations; ++i) { should be replaced with for (uint256 i; i < numIterations; ++i) {

Instances include:

base/Multicall.sol:14:        for (uint256 i = 0; i < data.length; i++) {
AlchemistV2.sol:990:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1282:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1355:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1458:        uint256 totalValue = 0;
AlchemistV2.sol:1461:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
AlchemistV2.sol:1524:        for (uint256 i = 0; i < depositedTokens.values.length; i++) {
CrossChainCanonicalBase.sol:57:        for (uint256 i = 0; i < _bridgeTokens.length; i++){ 
CrossChainCanonicalBase.sol:141:        for (uint i = 0; i < bridgeTokensArray.length; i++){ 
EthAssetManager.sol:214:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
EthAssetManager.sol:566:        uint256 total = 0;
EthAssetManager.sol:567:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
StakingPools.sol:188:    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
StakingPools.sol:363:    for (uint256 _poolId = 0; _poolId < _pools.length(); _poolId++) {
ThreePoolAssetManager.sol:250:        for (uint256 i = 0; i < NUM_STABLE_COINS; i++) {
ThreePoolAssetManager.sol:254:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
ThreePoolAssetManager.sol:353:        for (uint256 i = 0; i < 256; i++) {
ThreePoolAssetManager.sol:771:        uint256 normalizedTotal   = 0;
ThreePoolAssetManager.sol:773:        for (uint256 i = 0; i < NUM_STABLE_COINS; i++) {
ThreePoolAssetManager.sol:901:        uint256 total = 0;
ThreePoolAssetManager.sol:902:        for (uint256 i = 0; i < NUM_META_COINS; i++) {
TransmuterBuffer.sol:172:        for (uint256 i = 0; i < _yieldTokens[underlyingToken].length; i++) {
TransmuterBuffer.sol:186:        for (uint256 i = 0; i < tokens.length; i++) {
TransmuterBuffer.sol:235:            for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:242:        for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:272:        for (uint256 i = 0; i < registeredUnderlyings.length; i++) {
TransmuterBuffer.sol:382:        for (uint256 j = 0; j < registeredUnderlyings.length; j++) {
TransmuterBuffer.sol:387:        for (uint256 i = 0; i < numYTokens; i++) {
TransmuterBuffer.sol:479:        for (uint256 j = 0; j < weighting.tokens.length; j++) {
TransmuterBuffer.sol:534:        uint256 want = 0;
TransmuterBuffer.sol:549:        uint256 exchangeDelta = 0;

I suggest removing explicit initializations for default values.

Some variables should be immutable

These variables are only set in the constructor and are never edited after that:

contracts-full/EthAssetManager.sol:
  157:     IWETH9 public weth; //@audit gas: should be immutable

contracts-full/TransmuterConduit.sol:
  13:     address public token; //@audit gas: should be immutable
  16:     address public sourceTransmuter; //@audit gas: should be immutable
  19:     address public sinkTransmuter; //@audit gas: should be immutable

contracts-full/WETHGateway.sol:
  20:     address public whitelist; //@audit gas: should be immutable

Consider marking them as immutable.

Reduce the size of error messages (Long revert Strings)

Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.

Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.

Revert strings > 32 bytes:

adapters/lido/WstETHAdapterV1.sol:77:            revert Unauthorized("Payments only permitted from WETH or curve pool");
adapters/rocket/RETHAdapterV1.sol:54:            revert Unauthorized("Payments only permitted from WETH or rETH");
libraries/RocketPool.sol:14:            keccak256(abi.encodePacked("contract.address", "rocketTokenRETH"))
AlchemicTokenV1.sol:51:    require(whiteList[msg.sender], "AlTokenV1: Alchemist is not whitelisted");
AlchemicTokenV1.sol:81:    require(total <= ceiling[msg.sender], "AlUSD: Alchemist's ceiling was breached.");
AutoleverageCurveFactoryethpool.sol:23:            if (msg.value != collateralInitial) revert IllegalArgument("msg.value doesn't match collateralInitial");
EthAssetManager.sol:19:} from "./interfaces/external/curve/IEthStableMetaPool.sol";
StakingPools.sol:106:    require(_governance != address(0), "StakingPools: governance address cannot be 0x0");
StakingPools.sol:124:    require(_pendingGovernance != address(0), "StakingPools: pending governance address cannot be 0x0");
StakingPools.sol:131:    require(msg.sender == pendingGovernance, "StakingPools: only pending governance");
StakingPools.sol:160:    require(tokenPoolIds[_token] == 0, "StakingPools: token already has a pool");
StakingPools.sol:183:    require(_rewardWeights.length == _pools.length(), "StakingPools: weights length mismatch");
ThreePoolAssetManager.sol:19:} from "./interfaces/external/curve/IStableMetaPool.sol";
ThreePoolAssetManager.sol:24:} from "./interfaces/external/curve/IStableSwap3Pool.sol"; 

I suggest shortening the revert strings to fit in 32 bytes, or using custom errors as described next.

Use Custom Errors instead of Revert Strings to save Gas

Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)

Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:

Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.

Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).

Instances include:

gALCX.sol:33:        require(msg.sender == owner, "Not owner");
gALCX.sol:90:        require(success, "Transfer failed");
gALCX.sol:107:        require(success, "Transfer failed"); 
StakingPools.sol:106:    require(_governance != address(0), "StakingPools: governance address cannot be 0x0");
StakingPools.sol:114:    require(msg.sender == governance, "StakingPools: only governance");
StakingPools.sol:124:    require(_pendingGovernance != address(0), "StakingPools: pending governance address cannot be 0x0");
StakingPools.sol:131:    require(msg.sender == pendingGovernance, "StakingPools: only pending governance");
StakingPools.sol:160:    require(tokenPoolIds[_token] == 0, "StakingPools: token already has a pool");
StakingPools.sol:183:    require(_rewardWeights.length == _pools.length(), "StakingPools: weights length mismatch");

I suggest replacing require statements with custom errors.

#0 - 0xfoobar

2022-05-30T07:02:51Z

Great details, clean writeup

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter