Platform: Code4rena
Start Date: 12/07/2023
Pot Size: $80,000 USDC
Total HM: 11
Participants: 47
Period: 9 days
Judge: berndartmueller
Total Solo HM: 1
Id: 260
League: ETH
Rank: 44/47
Findings: 1
Award: $19.28
š Selected for report: 0
š Solo Findings: 0
š Selected for report: Sathish9098
Also found by: 0x11singh99, 0xAnah, 0xn006e7, Arz, DavidGiladi, K42, Raihan, ReyAdmirado, Rolezn, SAQ, SM3_SS, SY_S, Walter, dharma09, flutter_developer, hunter_w3b, matrix_0wl, naman1778, petrichor, ybansal2403
19.2767 USDC - $19.28
Number | Optimization Details | Instances |
---|---|---|
[G-01] | Use delete for state variables to reinitialize with their default value rather than assigning default value saves gas. | 2 |
[G-02] | Use assembly for address(0) comparison to save gas | 13 |
[G-03] | Do not emit block.timestamp since it is already included in event data and can be accessed from event data. | 1 |
[G-04] | State variables can be re-arranged to be packed into fewer storage slots | 1 |
[G-05] | Do not calculate constant variables | 28 |
[G-06] | Missing zero-address check in constructor | 4 |
[G-07] | Use nested if and, avoid multiple check combinations using && | 1 |
[G-08] | Using ternary operator instead of if else saves gas | 2 |
[G-09] | Use unchecked{} whenever underflow/overflow not possible | 2 |
Total 9 issues.
Note: Here some gas findings may be similar to automated report but all the instances are different that were missed in automated report.
delete
for state variables to reinitialize with their default value rather than assigning default value saves gas.This gas-saving behavior is due to how the delete operation is implemented in the Ethereum Virtual Machine (EVM).
When you use the delete keyword on a state variable, the EVM internally performs a low-level operation called SSTORE to update the variable's storage slot. The SSTORE operation clears the storage slot, effectively resetting the variable to its default value.
In contrast, if you manually assign a default value to a state variable, it involves writing a value to the storage slot. This operation is performed using the SSTORE opcode as well but with an extra cost compared to the delete operation. Writing the default value to the storage slot consumes more gas than simply clearing the storage slot with the delete operation.
2 instances - 1 file:
File: contracts/cgp/auth/MultisigBase.sol - 66: voting.voteCount = 0; + 66: delete voting.voteCount;
File: contracts/cgp/auth/MultisigBase.sol 70: for (uint256 i; i < count; ++i) { - 71: voting.hasVoted[signers.accounts[i]] = false; + 71: delete voting.hasVoted[signers.accounts[i]]; 72: }
address(0)
comparison to save gas13 instances - 7 files:
File : contracts/cgp/auth/MultisigBase.sol 173: if (account == address(0)) revert InvalidSigners();
https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/cgp/auth/MultisigBase.sol#L173
Recommended Changes:
+ bool isAddressZero; + assembly { + isAddressZero := eq(address(account), 0) + } - if (account == address(0)) revert InvalidSigners(); + if(isAddressZero) revert InvalidSigners();
File: contracts/cgp/util/Upgradable.sol 25: if (newOwner == address(0)) revert InvalidOwner(); ... 63: if (implementation() == address(0)) revert NotProxy();
https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/cgp/util/Upgradable.sol#L25 https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/cgp/util/Upgradable.sol#L63
Recommended Changes : Like Instance#1 assembly can be used, or separate function can be made to check for address(0) using assembly.
File : /contracts/cgp/AdminMultisigBase.sol 157: if (threshold == uint256(0)) revert InvalidAdminThreshold();
https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/cgp/AdminMultisigBase.sol#L157
File : /contracts/gmp-sdk/deploy/ConstAddressDeployer.sol 88: if (deployedAddress_ == address(0)) revert FailedDeploy();
File : /contracts/gmp-sdk/upgradable/InitProxy.sol 47: if (implementation() != address(0)) revert AlreadyInitialized();
Recommended Changes : Like Instance#1 of [G-02] recommendation assembly can be used, with little change use if(!isAddressZero)
instead of if(isAddressZero)
File : /contracts/gmp-sdk/upgradable/Proxy.sol 30: if (owner == address(0)) revert InvalidOwner();
https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/gmp-sdk/upgradable/Proxy.sol#L30
Recommended Changes : Like Instance#1 of [G-02] recommendation assembly can be used
File: contracts/its/interchain-token-service/InterchainTokenService.sol 92: if ( 93: remoteAddressValidator_ == address(0) || 94: gasService_ == address(0) || 95: tokenManagerDeployer_ == address(0) || 96: standardizedTokenDeployer_ == address(0) ) revert ZeroAddress(); ... 609: if (expressCaller == address(0)) { ... 652: if (expressCaller != address(0)) {
block.timestamp
since it is already included in event data and can be accessed from event data.1 instance - 1 file:
File: contracts/cgp/governance/InterchainGovernance.sol 78: emit ProposalExecuted(proposalHash, target, callData, nativeValue, block.timestamp);
Each slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct. Subsequent reads as well as writes have smaller gas savings.
1 instance - 1 file:
tokenManager
and decimals
can be declared one after another so that they can be fit into one storage slot.File: /contracts/its/token-implementations/StandardizedToken.sol 22: address public tokenManager; string public name; string public symbol; 25: uint8 public decimals;
Recommended Changes:
22: address public tokenManager; + uint8 public decimals; string public name; string public symbol; - 25: uint8 public decimals;
Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas each time of use.
Total 28 instances - 8 files:
File: contracts/cgp/AdminMultisigBase.sol 19: bytes32 internal constant KEY_ADMIN_EPOCH = keccak256('admin-epoch'); 21: bytes32 internal constant PREFIX_ADMIN = keccak256('admin'); 22: bytes32 internal constant PREFIX_ADMIN_COUNT = keccak256('admin-count'); 23: bytes32 internal constant PREFIX_ADMIN_THRESHOLD = keccak256('admin-threshold'); 24: bytes32 internal constant PREFIX_ADMIN_VOTE_COUNTS = keccak256('admin-vote-counts'); 25: bytes32 internal constant PREFIX_ADMIN_VOTED = keccak256('admin-voted'); 26: bytes32 internal constant PREFIX_IS_ADMIN = keccak256('is-admin');
Recommended Changes : Assign direct hash value and comment these calculating lines to show that assigning hash belongs to which string.
File: /contracts/cgp/AxelarGateway.sol 44: bytes32 internal constant PREFIX_COMMAND_EXECUTED = keccak256('command-executed'); 45: bytes32 internal constant PREFIX_TOKEN_ADDRESS = keccak256('token-address'); 46: bytes32 internal constant PREFIX_TOKEN_TYPE = keccak256('token-type'); 47: bytes32 internal constant PREFIX_CONTRACT_CALL_APPROVED = keccak256('contract-call-approved'); 48: bytes32 internal constant PREFIX_CONTRACT_CALL_APPROVED_WITH_MINT = keccak256('contract-call-approved-with-mint'); 49: bytes32 internal constant PREFIX_TOKEN_MINT_LIMIT = keccak256('token-mint-limit'); 50: bytes32 internal constant PREFIX_TOKEN_MINT_AMOUNT = keccak256('token-mint-amount'); 52: bytes32 internal constant SELECTOR_BURN_TOKEN = keccak256('burnToken'); 53: bytes32 internal constant SELECTOR_DEPLOY_TOKEN = keccak256('deployToken'); 54: bytes32 internal constant SELECTOR_MINT_TOKEN = keccak256('mintToken'); 55: bytes32 internal constant SELECTOR_APPROVE_CONTRACT_CALL = keccak256('approveContractCall'); 56: bytes32 internal constant SELECTOR_APPROVE_CONTRACT_CALL_WITH_MINT = keccak256('approveContractCallWithMint'); 57: bytes32 internal constant SELECTOR_TRANSFER_OPERATORSHIP = keccak256('transferOperatorship');
https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/cgp/AxelarGateway.sol#L44C5-L58C1
Recommended Changes : Assign direct hash value and comment these calculating lines to show that assigning hash belongs to which string.
File: /contracts/gmp-sdk/deploy/Create3.sol 41: bytes32 internal constant DEPLOYER_BYTECODE_HASH = keccak256(type(CreateDeployer).creationCode);
https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/gmp-sdk/deploy/Create3.sol#L41
Recommended Changes : Assign direct hash value and comment these calculating line to show that assigning hash belongs to which string.
File: /contracts/gmp-sdk/util/TimeLock.sol 13: bytes32 internal constant PREFIX_TIME_LOCK = keccak256('time-lock');
https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/gmp-sdk/util/TimeLock.sol#L13
Recommended Changes : Assign direct hash value and comment these calculating line to show that assigning hash belongs to which string.
File: /contracts/its/interchain-token-service/InterchainTokenService.sol 62: bytes32 internal constant PREFIX_CUSTOM_TOKEN_ID = keccak256('its-custom-token-id'); 63: bytes32 internal constant PREFIX_STANDARDIZED_TOKEN_ID = keccak256('its-standardized-token-id'); 64: bytes32 internal constant PREFIX_STANDARDIZED_TOKEN_SALT = keccak256('its-standardized-token-salt'); ... 71: bytes32 private constant CONTRACT_ID = keccak256('interchain-token-service');
Recommended Changes : Assign direct hash value and comment these calculating lines to show that assigning hash belongs to which string.
File: contracts/its/proxies/InterchainTokenServiceProxy.sol 12: bytes32 private constant CONTRACT_ID = keccak256('interchain-token-service');
Recommended Changes : Assign direct hash value and comment these calculating line to show that assigning hash belongs to which string.
File: contracts/its/proxies/RemoteAddressValidatorProxy.sol 12: bytes32 private constant CONTRACT_ID = keccak256('remote-address-validator');
Recommended Changes : Assign direct hash value and comment these calculating line to show that assigning hash belongs to which string.
File: contracts/its/proxies/StandardizedTokenProxy.sol 14: bytes32 private constant CONTRACT_ID = keccak256('standardized-token');
Recommended Changes : Assign direct hash value and comment these calculating line to show that assigning hash belongs to which string.
zero-address
check in constructor
Missing checks for zero-addresses may lead to infunctional protocol, if the variable addresses are updated incorrectly. It also wastes gas as it requires the redeployment of the contract.
Recommendation : Check input address if it is zero, using assembly to save more gas see: [G-02]
4 instances - 3 files:
implementationAddress
can be checked for address(0)File : /contracts/gmp-sdk/upgradable/FixedProxy.sol 23: constructor(address implementationAddress) { 24: implementation = implementationAddress; 25: }
_gateway
and _gasService
can be checked for address(0) before assigning to state variables.File : /contracts/interchain-governance-executor/InterchainProposalSender.sol 42: constructor(address _gateway, address _gasService) { 43: gateway = IAxelarGateway(_gateway); 44: gasService = IAxelarGasService(_gasService); 45: }
interchainTokenServiceAddress_
must be checked for address(0) before assigning to immutable variable interchainTokenServiceAddress
.File : contracts/its/proxies/TokenManagerProxy.sol 25: constructor( 26: address interchainTokenServiceAddress_, uint256 implementationType_, bytes32 tokenId_, bytes memory params ) { 31: interchainTokenServiceAddress = IInterchainTokenService(interchainTokenServiceAddress_);
Using nested if is cheaper gas wise than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
1 instance - 1 file:
File : contracts/gmp-sdk/upgradable/Proxy.sol 33: if (id != bytes32(0) && IUpgradable(implementationAddress).contractId() != id) revert InvalidImplementation();
https://github.com/code-423n4/2023-07-axelar/blob/main/contracts/gmp-sdk/upgradable/Proxy.sol#L33
Recommended Changes: Use nested if
- 33: if (id != bytes32(0) && IUpgradable(implementationAddress).contractId() != id) revert InvalidImplementation(); + if (id != bytes32(0)) { + if (IUpgradable(implementationAddress).contractId() != id) { + revert InvalidImplementation(); + } + }
When using the if-else construct, Solidity generates bytecode that includes a JUMP operation. The JUMP operation requires the EVM to change the program counter to a different location in the bytecode, resulting in additional gas costs. On the other hand, the ternary operator doesn't involve a JUMP operation, as it generates bytecode that utilizes conditional stack manipulation. The exact amount of gas saved by using the ternary operator instead of the if-else construct in Solidity can vary depending on the specific scenario, the complexity of the surrounding code, and the conditions involved
2 instances - 2 files:
File : contracts/interchain-governance-executor/InterchainProposalExecutor.sol 78: if (!success) { _onTargetExecutionFailed(call, result); } else { _onTargetExecuted(call, result); 82: }
Recommended Changes :
- 78: if (!success) { - _onTargetExecutionFailed(call, result); - } else { - _onTargetExecuted(call, result); - 82: } + !success ? _onTargetExecutionFailed(call, result) : _onTargetExecuted(call, result);
File: /contracts/its/token-manager/TokenManager.sol 68: if (operatorBytes.length == 0) { 69: operator_ = address(interchainTokenService); 70: } else { 71: operator_ = operatorBytes.toAddress(); 72: }
Recommended Changes :
- 68: if (operatorBytes.length == 0) { - 69: operator_ = address(interchainTokenService); - 70: } else { - 71: operator_ = operatorBytes.toAddress(); - 72: } + operatorBytes.length == 0 ? operator_ = address(interchainTokenService) : operator_ = operatorBytes.toAddress();
Because of previous condition check before the operation(+/-), it can be decided that underflow/overflow not possible.
2 instances - 1 file:
type(uint256).max - amount
can't underflow because amount
is type of uint256 so it can't go above it's max value type(uint256).max
so this subtraction can't be less than zero this means no chances for underflow .So this subtraction can be unchecked and cached rather than re-calculating same thing.File: contracts/its/interchain-token/InterchainToken.sol 43: function interchainTransfer( string calldata destinationChain, bytes calldata recipient, 46: uint256 amount, ... 56: if (allowance_ != type(uint256).max) { 57: if (allowance_ > type(uint256).max - amount) { 58: allowance_ = type(uint256).max - amount; }
Recommended Changes :
56: if (allowance_ != type(uint256).max) { - 57: if (allowance_ > type(uint256).max - amount) { - 58: allowance_ = type(uint256).max - amount; + unchecked{ + uint256 diffAmount = type(uint256).max - amount; + } + if (allowance_ > diffAmount) { + allowance_ = diffAmount; } ...
type(uint256).max - amount
can't underflow same as above instance#1File: contracts/its/interchain-token/InterchainToken.sol 83: uint256 amount ... 95: if (allowance_ != type(uint256).max) { 96: if (allowance_ > type(uint256).max - amount) { 97: allowance_ = type(uint256).max - amount; }
Recommended Changes : Same as above instance#1 recommendation changes. type(uint256).max - amount
subtraction can be unchecked and cached rather than re-calculating same thing.
#0 - deanamiel
2023-08-31T00:10:15Z
We added zero address checks in the constructors of the Interchain executor contracts. See PR here
#1 - deanamiel
2023-08-31T00:22:26Z
We implemented the suggestion from issue G-01 to use the delete
keyword. See PR here
#2 - c4-judge
2023-09-04T11:09:25Z
berndartmueller marked the issue as grade-b
#3 - deanamiel
2023-09-06T23:17:26Z
Implemented the suggestion from issue G-04. See PR here