Platform: Code4rena
Start Date: 08/06/2022
Pot Size: $115,000 USDC
Total HM: 26
Participants: 72
Period: 11 days
Judge: leastwood
Total Solo HM: 14
Id: 132
League: ETH
Rank: 25/72
Findings: 2
Award: $333.78
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: BowTiedWardens
Also found by: 0x1f8b, 0x29A, 0x52, 0xNazgul, 0xNineDec, 0xf15ers, 0xkatana, 0xmint, Chom, ElKu, Funen, IllIllI, JMukesh, Jujic, Kaiziron, Lambda, MiloTruck, Ruhum, SmartSek, SooYa, TerrierLover, TomJ, WatchPug, Waze, _Adam, asutorufos, auditor0517, bardamu, c3phas, catchup, cccz, ch13fd357r0y3r, cloudjunky, cmichel, cryptphi, csanuragjain, defsec, fatherOfBlocks, hansfriese, hyh, jayjonah8, joestakey, k, kenta, obtarian, oyc_109, robee, sach1r0, shenwilly, simon135, slywaters, sorrynotsorry, tintin, unforgiven, xiaoming90, zzzitron
163.3266 USDC - $163.33
Few vulnerabilities were found examining the contracts. The main concerns are with the setters not checking the input value properly and the use of deprecated functions.
Some of the function comments are missing function parameters or returns
Non-Critical
Instances include:
address _stableSwapPool
ConnextMessage.TokenId calldata _canonical, address _stableSwapPool
XCallArgs calldata _args,address _asset,bytes32 _transferId,uint256 _amount
ExecuteArgs calldata _args
XCallArgs calldata _args, ConnextMessage.TokenId memory _canonical
ExecuteArgs calldata _args
CallParams calldata _params,uint256 _amount,uint256 _nonce,bytes32 _canonicalId,uint32 _canonicalDomain,address _originSender
bytes32 _transferId,bool _isFast,ExecuteArgs calldata _args
ExecuteArgs calldata _args,uint256 _amount,address _asset, // adopted (or local if specified)bytes32 _transferId,bool _reconciled
bytes32 _transferId,uint256 _fastTransferAmount,address _local,address _router
bytes memory _message
uint256 minAmountOut,uint256 deadline
uint256 maxAmountIn,uint256 deadline
address from,address to,uint256 amount
uint256 _maxIn
uint256 _slippageTol
uint256 _maxIn
Manual Analysis
Add a comment for these parameters
Events should use indexed fields when possible, up to three per event.
Non-Critical
Instances include:
event WrapperUpdated(address oldWrapper, address newWrapper, address caller)
event TokenRegistryUpdated(address oldTokenRegistry, address newTokenRegistry, address caller)
event StableSwapAdded(bytes32 canonicalId, uint32 domain, address swapPool, address caller)
event AssetAdded(bytes32 canonicalId, uint32 domain, address adoptedAsset, address supportedAsset, address caller)
event AssetRemoved(bytes32 canonicalId, address caller)
event XCalled
event Reconciled
event Executed
event TransferRelayerFeesUpdated
event ForcedReceiveLocal
event AavePortalMintUnbacked
event AavePortalRepayment
event AavePortalRepaymentDebt
event SponsorVaultUpdated
event PromiseRouterUpdated
event ExecutorUpdated
event AavePortalRouterRepayment
event RouterOwnershipRenunciationProposed(uint256 timestamp)
event RouterOwnershipRenounced(bool renounced)
event AssetOwnershipRenunciationProposed(uint256 timestamp)
event AssetOwnershipRenounced(bool renounced)
event RelayerFeeRouterUpdated
event RelayerAdded
event RelayerRemoved
event InitiatedClaim
event Claimed
event RouterAdded
event RouterRemoved
event MaxRoutersPerTransferUpdated
event LiquidityFeeNumeratorUpdated
event RouterApprovedForPortal
event RouterUnapprovedForPortal
event RouterLiquidityAdded
event RouterLiquidityRemoved
event NewAdmin(address oldAdmin, address newAdmin)
event PriceRecordUpdated(address token, address baseToken, address lpToken, bool _active)
event DirectPriceUpdated(address token, uint256 oldPrice, uint256 newPrice)
event AggregatorUpdated(address tokenAddress, address source)
event V1PriceOracleUpdated(address oldAddress, address newAddress)
event RouterOwnershipRenunciationProposed(uint256 timestamp)
event RouterOwnershipRenounced(bool renounced)
event AssetOwnershipRenunciationProposed(uint256 timestamp)
event AssetOwnershipRenounced(bool renounced)
event OwnershipProposed(address indexed proposedOwner)
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
event ConnextUpdated
event RateUpdated
event RelayerFeeCapUpdated
event GasTokenOracleUpdated
event TokenExchangeUpdated
event ReimburseLiquidityFees
event ReimburseRelayerFees
event Deposit
event Withdraw
Manual Analysis
Add indexed fields to these events, so that there is up to three indexed fields when possible.
It is not recommended to emit events before the end of the computations, as the function might revert based on conditions ahead of the event emission
Non-Critical
Instances include:
emit AssetAdded(_canonical.id, _canonical.domain, _adoptedAssetId, supported, msg.sender) emitted before call to _addStableSwapPool()
emit DiamondCut(_diamondCut, _init, _calldata) emitted before call to initializeDiamondCut()
Manual Analysis
Place the event emission in the last position in the function.
Setters should emit an event so that Dapps can detect important changes to storage
Non-Critical
Instances include:
setAavePool(address _aavePool)
setAavePortalFee(uint256 _aavePortalFeeNumerator)
setSwapAdminFee(bytes32 canonicalId, uint256 newAdminFee)
setSwapFee(bytes32 canonicalId, uint256 newSwapFee)
Manual Analysis
Emit an event in all setters.
Some functions are missing comments.
Non-Critical
Instances include:
All the getters
All the getters and admin methods.
proposeDiamondCut rescindDiamondCut
All the getters
All the getters
All the internal functions
All the getters
Some of the getters
All the functions
All the internal functions
Almost all functions
Manual Analysis
Add comments to these functions
There are open TODOs in the code. Code architecture, incentives, and error handling/reporting questions/issues should be resolved before deployment.
Non-Critical
Instances include:
Manual Analysis
Remove the TODOs
In the case of an ERC20 token, functions should check msg.value is zero.
Non-Critical
Instances include:
The execute()
function does not check whether msg.value
is zero when the token transferred is not ether. Any ETH sent in this situation would get locked as there is no withdrawal function. This is non-critical since the function has the onlyConnext
modifier.
Manual Analysis
Check hat msg.value == 0
when !isNative == true
Public functions that are not called by the contract should be declared external.
Non-Critical
Instances include:
redeemMaturedPositions(ISetToken _setToken)
canonicalToAdopted()
adoptedToCanonical()
approvedAssets()
adoptedToLocalPools()
wrapper()
tokenRegistry()
executor()
nonce()
sponsorVault()
promiseRouter()
routerOwnershipRenounced()
assetOwnershipRenounced()
proposed()
proposedTimestamp()
routerOwnershipTimestamp()
assetOwnershipTimestamp()
delay()
proposeRouterOwnershipRenunciation()
renounceRouterOwnership()
proposeAssetOwnershipRenunciation()
renounceAssetOwnership()
renounced()
proposeNewOwner(address newlyProposed)
renounceOwnership()
acceptProposedOwner()
pause()
unpause()
transferRelayer(bytes32 _transferId)
approvedRelayers(address _relayer)
getProposedRouterOwner
getProposedRouterOwnerTimestamp
maxRoutersPerTransfer
routerBalances
getRouterApprovalForPortal
Manual Analysis
Make these functions external
Use scientific notation rather than exponentiation, e.g: 10e10
instead of 10**10
Non-critical
Instances include:
uint256 internal constant MAX_SWAP_FEE = 10**8
uint256 internal constant MAX_ADMIN_FEE = 10**10
uint256 internal constant MAX_LOOP_LIMIT = 256
Manual Analysis
Replace **
with e
constructors should check the address written in an immutable address variable is not the zero address
Low
Instances include:
Manual Analysis
Add a zero address check for the function argument.
This function is deprecated.
Low
Instances include:
SafeERC20.safeApprove(IERC20(_assetIn), address(pool), _amountIn)
Manual Analysis
safeIncreaseAllowance()
and safeDecreaseAllowance()
should be used instead.
Setters should check the input value - ie make revert if it is the zero address or zero
Low
Instances include:
setXAppConnectionManager(address _xAppConnectionManager)
setAavePool(address _aavePool)
setV1PriceOracle(address _v1PriceOracle)
setAdmin(address newAdmin)
setGasTokenOracle(address _gasTokenOracle)
Manual Analysis
Add zero address checks to these setters
Setters that change the fees should use a timelock to give time for users to react. Even if it does not directly impact users, the admin fee can be set as high as a 100%, impacting the earning of liquidity pools. Adding a timelock would give the protocol more legitimacy.
Low
Instances include:
Manual Analysis
Add a timelock to these setters
#0 - jakekidd
2022-07-01T22:25:44Z
(-)
(+)
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, 0xmint, BowTiedWardens, ElKu, Fitraldys, Funen, Kaiziron, Lambda, Metatron, MiloTruck, Randyyy, Ruhum, SmartSek, TomJ, Tomio, UnusualTurtle, Waze, _Adam, apostle0x01, asutorufos, c3phas, catchup, csanuragjain, defsec, fatherOfBlocks, hansfriese, hyh, ignacio, joestakey, k, kaden, nahnah, oyc_109, rfa, robee, sach1r0, simon135, slywaters
170.4491 USDC - $170.45
report submitted via email on 2022/06/13 at 08:43 UTC
Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
In particular, in for
loops, when using the length of a storage array as the condition being checked after each loop, caching the array length in memory can yield significant gas savings if the array length is high
Instances include:
scope: getTokenPrice()
v1PriceOracle
is read twice:scope: renounceRouterOwnership()
_routerOwnershipTimestamp
is read twice:scope: renounceAssetOwnership()
_routerOwnershipTimestamp
is read twice:scope: renounceOwnership()
_routerOwnershipTimestamp
is read twice:proposed
is read twice:scope: acceptProposedOwner()
proposed
is read twice:scope: reimburseLiquidityFees()
tokenExchanges[_token]
is read twice:scope: reimburseRelayerFees()
gasTokenOracle
is read twice:relayerFeeCap
is read twice, but only if sponsoredFee
is greater relayerFeeCap
than:scope: renounceOwnership()
_proposedOwnershipTimestamp
is read twice:_proposedO
is read twice:Manual Analysis
cache these storage variables in memory
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.
Instances include:
scope: handle()
scope: _reconcile()
scope: _getTransferId()
ConnextMessage.TokenId memory _canonical
scope: _reconcileProcessMessage()
scope: handle()
scope: handle()
scope: swapToLocalAssetIfNeeded()
ConnextMessage.TokenId memory _canonical
scope: formatTokenId()
scope: formatDetailsHash()
string memory _name
string memory _symbol
scope: parseDomainAndSenderBytes()
scope: proposeDiamondCut()
IDiamondCut.FacetCut[] memory _diamondCut
bytes memory _calldata
scope: rescindDiamondCut()
IDiamondCut.FacetCut[] memory _diamondCut
bytes memory _calldata
scope: diamondCut()
IDiamondCut.FacetCut[] memory _diamondCut
bytes memory _calldata
scope: addFunctions()
bytes4[] memory _functionSelectors
scope: replaceFunctions()
bytes4[] memory _functionSelectors
scope: removeFunctions()
bytes4[] memory _functionSelectors
scope: initializeDiamondCut()
Manual Analysis
Replace memory
with calldata
In the EVM, there is no opcode for >=
or <=
.
When using greater than or equal, two operations are performed: >
and =
.
Using strict comparison operators hence saves gas
Instances include:
(block.timestamp - s._routerOwnershipTimestamp) <= _delay
(block.timestamp - s._assetOwnershipTimestamp) <= _delay
(block.timestamp - s._proposedOwnershipTimestamp) <= _delay
(block.timestamp - s._proposedOwnershipTimestamp) <= _delay
block.timestamp - s.routerPermissionInfo.proposedRouterTimestamp[router] <= _delay
index >= s.swapStorages[canonicalId].pooledTokens.length
index >= s.swapStorages[canonicalId].balances.length
_pooledTokens.length <= 1
_a >= AmplificationUtils.MAX_A
_fee >= SwapUtils.MAX_SWAP_FEE
_adminFee >= SwapUtils.MAX_ADMIN_FEE
(block.timestamp - _routerOwnershipTimestamp) <= _delay
(block.timestamp - _assetOwnershipTimestamp) <= _delay
(block.timestamp - _proposedOwnershipTimestamp) <= _delay
(block.timestamp - _proposedOwnershipTimestamp) <= _delay
_pooledTokens.length <= 32
decimals[i] <= SwapUtils.POOL_PRECISION_DECIMALS
block.timestamp <= deadline
(block.timestamp - _proposedOwnershipTimestamp) <= _delay
(block.timestamp - _proposedOwnershipTimestamp) <= _delay
block.timestamp >= self.initialATime.add(1 days)
futureTime_ >= block.timestamp.add(MIN_RAMP_TIME)
futureAPrecise.mul(MAX_A_CHANGE) >= initialAPrecise
futureAPrecise <= initialAPrecise.mul(MAX_A_CHANGE)
_maxIn >= ipool.calculateSwapInv(tokenIndexIn, tokenIndexOut, _amountOut)
_amountIn <= _maxIn
tokenAmount <= xp[tokenIndex]
amount <= totalSupply
dx <= tokenFrom.balanceOf(msg.sender)
dy >= minDy
dy <= self.balances[tokenIndexTo]
dx <= maxDx
dx <= tokenFrom.balanceOf(msg.sender)
dx <= tokenFrom.balanceOf(msg.sender)
dy >= minDy
dy <= self.balances[tokenIndexTo]
dx <= maxDx
toMint >= minToMint
amount <= lpToken.balanceOf(msg.sender)
amounts[i] >= minAmounts[i]
tokenAmount <= lpToken.balanceOf(msg.sender)
dy >= minAmount
maxBurnAmount <= v.lpToken.balanceOf(msg.sender)https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/libraries/SwapUtils.sol#L1007\
tokenAmount <= maxBurnAmount
newAdminFee <= MAX_ADMIN_FEE
newSwapFee <= MAX_SWAP_FEE
_len <= LENGTH_RETURNDATA_START
_len >= MIN_CLAIM_LEN
Manual Analysis
Replace <=
with <
, and >=
with >
. Do not forget to increment/decrement the compared variable
example:
-(block.timestamp - s._proposedOwnershipTimestamp) <= _delay +(block.timestamp - s._proposedOwnershipTimestamp) < _delay - 1;
However, if 1
is negligible compared to the value of the variable, we can omit the increment.
example:
-(block.timestamp - s._proposedOwnershipTimestamp) <= _delay; +(block.timestamp - s._proposedOwnershipTimestamp) <= _delay;
Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters: it costs approximately 400
less gas in deployment per variable hardcoded.
Instances include:
Manual Analysis
Hardcode variables with their initial value instead of writing it during contract deployment with constructor parameters.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here
Custom errors are defined using the error statement
Instances include:
require(s._status != _ENTERED, "ReentrancyGuard: reentrant call")
require(_remote != bytes32(0), "!remote")
require(baseTokenPrice > 0, "invalid base token")
require(amount != 0, "LPToken: cannot mint 0")
require(to != address(this), "LPToken: cannot send to itself")
require(_pooledTokens.length > 1, "_pooledTokens.length <= 1")
require(_pooledTokens.length <= 32, "_pooledTokens.length > 32")
require(_pooledTokens.length == decimals.length, "_pooledTokens decimals mismatch")
require(tokenIndexes[address(_pooledTokens[i])] == 0 && _pooledTokens[0] != _pooledTokens[i],"Duplicate tokens")
require(address(_pooledTokens[i]) != address(0), "The 0 address isn't an ERC-20")
require(decimals[i] <= SwapUtils.POOL_PRECISION_DECIMALS, "Token decimals exceeds max")
require(_a < AmplificationUtils.MAX_A, "_a exceeds maximum")
require(_fee < SwapUtils.MAX_SWAP_FEE, "_fee exceeds maximum")
require(_adminFee < SwapUtils.MAX_ADMIN_FEE, "_adminFee exceeds maximum")
require(lpToken.initialize(lpTokenName, lpTokenSymbol), "could not init lpToken clone")
require(index < swapStorage.pooledTokens.length, "Out of range")
require(address(getToken(index)) == tokenAddress, "Token does not exist")
require(index < swapStorage.pooledTokens.length, "Index out of range")
require(block.timestamp >= self.initialATime.add(1 days), "Wait 1 day before starting ramp")
require(futureTime_ >= block.timestamp.add(MIN_RAMP_TIME), "Insufficient ramp time")
require(futureA_ > 0 && futureA_ < MAX_A, "futureA_ must be > 0 and < MAX_A")
require(futureAPrecise.mul(MAX_A_CHANGE) >= initialAPrecise, "futureA_ is too small")
require(futureAPrecise <= initialAPrecise.mul(MAX_A_CHANGE), "futureA_ is too large")
require(self.futureATime > block.timestamp, "Ramp is already stopped")
require(isValidAction(_action), "!action")
require(msg.sender == diamondStorage().contractOwner, "LibDiamond: Must be contract owner")
require(diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] < block.timestamp,"LibDiamond: delay not elapsed")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)")
require(oldFacetAddress == address(0), "LibDiamondCut: Can't add function that already exists")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)")
require(oldFacetAddress != _facetAddress, "LibDiamondCut: Can't replace function with same function")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_facetAddress == address(0), "LibDiamondCut: Remove facet address must be address(0)")
require(_facetAddress != address(0), "LibDiamondCut: Can't remove function that doesn't exist")
require(_facetAddress != address(this), "LibDiamondCut: Can't remove immutable function")
require(_calldata.length == 0, "LibDiamondCut: _init is address(0) but_calldata is not empty")
require(_calldata.length > 0, "LibDiamondCut: _calldata is empty but _init is not address(0)")
require(contractSize > 0, _errorMessage)
require(tokenIndex < xp.length, "Token index out of range")
require(tokenAmount <= xp[tokenIndex], "Withdraw exceeds available")
require(tokenIndex < numTokens, "Token not found"
require(numTokens == precisionMultipliers.length, "Balances must match multipliers")
require(tokenIndexFrom != tokenIndexTo, "Can't compare token to itself")
require(tokenIndexFrom < numTokens && tokenIndexTo < numTokens, "Tokens must be in pool")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(amount <= totalSupply, "Cannot exceed total supply")
require(index < self.pooledTokens.length, "Token index out of range")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(dy >= minDy, "Swap didn't result in min tokens")
require(dy <= self.balances[tokenIndexTo], "Cannot get more than pool balance")
require(dx <= maxDx, "Swap needs more than max tokens")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(dx == tokenFrom.balanceOf(address(this)).sub(beforeBalance), "not support fee token")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(dy >= minDy, "Swap didn't result in min tokens")
require(dy <= self.balances[tokenIndexTo], "Cannot get more than pool balance")
require(dx <= maxDx, "Swap didn't result in min tokens")
require(amounts.length == pooledTokens.length, "Amounts must match pooled tokens")
require(v.totalSupply != 0 || amounts[i] > 0, "Must supply all tokens in pool")
require(v.d1 > v.d0, "D should increase")
require(toMint >= minToMint, "Couldn't mint min requested")
require(amount <= lpToken.balanceOf(msg.sender), ">LP.balanceOf")
require(minAmounts.length == pooledTokens.length, "minAmounts must match poolTokens")
require(amounts[i] >= minAmounts[i], "amounts[i] < minAmounts[i]")
require(tokenAmount <= lpToken.balanceOf(msg.sender), ">LP.balanceOf")
require(tokenIndex < pooledTokens.length, "Token not found")
require(dy >= minAmount, "dy < minAmount")
require(amounts.length == pooledTokens.length, "Amounts should match pool tokens")
require(maxBurnAmount <= v.lpToken.balanceOf(msg.sender) && maxBurnAmount != 0, ">LP.balanceOf")
require(tokenAmount != 0, "Burnt amount cannot be zero")
require(tokenAmount <= maxBurnAmount, "tokenAmount > maxBurnAmount")
require(newAdminFee <= MAX_ADMIN_FEE, "Fee is too high")
require(newSwapFee <= MAX_SWAP_FEE, "Fee is too high")
Manual Analysis
Replace require and revert statements with custom errors.
For instance, in StableSwap.sol
:
Replace
require(_pooledTokens.length > 1, "_pooledTokens.length <= 1")
with
if (_pooledTokens.length < 2) { revert IncorrectPoolTokenLength(); }
and define the custom error in the contract
error IncorrectPoolTokenLength();
If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Instances include:
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 j = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
Manual Analysis
Remove explicit initialization for default values.
When emitting an event, using a local variable instead of a storage variable saves gas.
Instances include:
emit RouterOwnershipRenunciationProposed(s._routerOwnershipTimestamp)
emit AssetOwnershipRenunciationProposed(s._assetOwnershipTimestamp)
emit OwnershipProposed(s._proposed)
emit RouterOwnershipRenunciationProposed(_routerOwnershipTimestamp)
emit AssetOwnershipRenunciationProposed(_assetOwnershipTimestamp)
emit OwnershipProposed(_proposed)
Manual Analysis
In all instances, the storage variable can be replaced by the function argument that is written to that storage variable.
You can group repetitive require statements into a modifier to save on deployment costs. There is however a tradeoff to doing this: declaring the logic inline is cheaper than having it in a modifier when you call the function that uses that modifier.
Instances include:
require(index < swapStorage.pooledTokens.length, "Out of range")
require(index < swapStorage.pooledTokens.length, "Index out of range")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
Manual Analysis
You can create modifiers for repetitive require statements and apply them to the functions where the require statement mentioned are called. Again, this one is more of a suggestion because there is a trade-off.
If a storage variable is never modified, marking it as immutable can save gas, as it is saved as a constant instead of a storage variable, saving a SLOAD
operation (100 gas) every time the variable is read.
Instances include:
wrapped
is initialized once and can not be modified afterwards. It is read once in every call to getTokenPrice
.
Manual Analysis
add the immutable
keyword to address public wrapped
Prefix increments are cheaper than postfix increments: it returns the incremented variable instead of returning a temporary variable storing the initial value of the variable. It saves 5 gas per iteration
Instances include:
facetIndex++
selectorIndex++
selectorPosition++
selectorIndex++
selectorPosition++
selectorIndex++
i++
i++
i++
i++
i++
j++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
Manual Analysis
change variable++
to ++variable
.
As of Solidity 0.8.0, underflow and overflow checks are performed by default on additions and subtractions. Using an external library for these computations should be avoided as it will waste gas.
Several contracts in scope use the SafeMath library of OpenZeppelin and its functions sub
and add
ConnextPriceOracle.sol AmplificationUtils.sol SwapUtils.sol
Manual Analysis
Use the native additions and subtractions.
Require statements including conditions with the &&
operator can be broken down in multiple require statements to save gas.
Instances include:
require(futureA_ > 0 && futureA_ < MAX_A, "futureA_ must be > 0 and < MAX_A")
require(tokenIndexFrom < numTokens && tokenIndexTo < numTokens, "Tokens must be in pool")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(maxBurnAmount <= v.lpToken.balanceOf(msg.sender) && maxBurnAmount != 0, ">LP.balanceOf")
Manual Analysis
Split the require statement in several statements. For instance:
-require(futureA_ > 0 && futureA_ < MAX_A, "futureA_ must be > 0 and < MAX_A") +require(futureA_ > 0) +require(futureA_ < MAX_A)
To improve the savings, you can also use custom errors instead of require statements
Solidity contracts have contiguous 32 bytes (256 bits) slots used in storage. By arranging the variables, it is possible to minimize the number of slots used within a contract's storage and therefore reduce deployment costs.
address type variables are each of 20 bytes size (way less than 32 bytes). However, they here take up a whole 32 bytes slot (they are contiguous).
As bool type variables are of size 1 byte, there's a slot here that can get saved by moving them closer to an address
Instances include:
address private _owner; address private _proposed; uint256 private _proposedOwnershipTimestamp; bool private _routerOwnershipRenounced; uint256 private _routerOwnershipTimestamp; bool private _assetOwnershipRenounced;
Manual Analysis
Place _routerOwnershipRenounced
after _owner
and _assetOwnershipRenounced
_proposed
to save two storage slots
address private _owner; +bool private _routerOwnershipRenounced; address private _proposed; +bool private _assetOwnershipRenounced; uint256 private _proposedOwnershipTimestamp; uint256 private _routerOwnershipTimestamp;
The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked
block will save gas. This is particularly true in for loops, as it saves some gas at each iteration.
Instances include:
availableAmount - backUnbackedAmount, cannot overflow because of the condition line 1099
facetIndex++
selectorIndex++
selectorIndex++
selectorIndex++
i++
i++
i++
i++
i++
j++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
Manual Analysis
Place the arithmetic operations in an unchecked
block
When emitting an event that includes a new and an old value, it is cheaper in gas to avoid caching the old value in memory. Instead, emit the event, then save the new value in storage.
Instances include:
address oldAdmin = admin; admin = newAdmin; emit NewAdmin(oldAdmin, newAdmin)
address previousOwner = ds.contractOwner; ds.contractOwner = _newOwner; emit OwnershipTransferred(previousOwner, _newOwner)
address oldOwner = _owner; _owner = newOwner; _proposedOwnershipTimestamp = 0; _proposed = address(0); emit OwnershipTransferred(oldOwner, newOwner)
Manual Analysis
Emit the old storage variable before writing the new value.
e.g:
emit NewAdmin(admin, newAdmin) admin = newAdmin
#0 - kartoonjoy
2022-06-13T15:50:24Z
Added the file per the email submission for joestakey. Thanks!