Issue | Contexts | |
---|---|---|
GAS‑1 | ++i /i++ Should Be unchecked{++i} /unchecked{i++} When It Is Not Possible For Them To Overflow, As Is The Case When Used In For- And While-loops |
4 |
GAS‑2 | Splitting require() Statements That Use && Saves Gas |
2 |
GAS‑3 | abi.encode() is less efficient than abi.encodepacked() |
17 |
GAS‑4 | Use calldata instead of memory for function parameters | 3 |
GAS‑5 | Public Functions To External | 2 |
GAS‑6 | Usage of uints /ints smaller than 32 bytes (256 bits) incurs overhead |
11 |
GAS‑7 | Optimize names to save gas | All in-scope contracts |
GAS‑8 | Use uint256(1) /uint256(2) instead for true and false boolean states |
16 |
GAS‑9 | Structs can be packed into fewer storage slots | 2 |
GAS‑10 | Superfluous event fields | 2 |
GAS‑11 | Do not calculate constants | 3 |
Total: 80 contexts over 11 issues
[GAS‑1] ++i
/i++
Should Be unchecked{++i}
/unchecked{i++}
When It Is Not Possible For Them To Overflow, As Is The Case When Used In For- And While-loops
The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas PER LOOP
94: for (uint256 i = 0; i < facetCutsLength; ++i) {
132: for (uint256 i = 0; i < selectorsLength; ++i) {
153: for (uint256 i = 0; i < selectorsLength; ++i) {
173: for (uint256 i = 0; i < selectorsLength; ++i) {
Instead of using operator &&
on a single require
. Using a two require
can save more gas.
i.e.
for require(version == 1 && _bytecodeHash[1] == bytes1(0), "zf");
use:
require(version == 1);
require(_bytecodeHash[1] == bytes1(0));
65: require(version == 1 && _bytecodeHash[1] == bytes1(0), "zf");
65: require(version == 1 && _bytecodeHash[1] == bytes1(0), "zf");
See for more information: https://github.com/ConnorBlockchain/Solidity-Encode-Gas-Comparison
82: bytes memory create2Input = abi.encode(address(this), l2ProxyTokenBytecodeHash, _governor);
168: data = abi.encode(data1, data2, data3);
283: bytes32 constructorInputHash = keccak256(abi.encode(address(l2TokenFactory), ""));
58: bytes memory create2Input = abi.encode(address(this));
54: s.storedBlockHashes[0] = keccak256(abi.encode(storedBlockZero));
27: s.diamondCutStorage.proposedDiamondCutHash = keccak256(abi.encode(_facetCuts, _initAddress));
61: keccak256(abi.encode(_diamondCut.facetCuts, _diamondCut.initAddress)),
85: return keccak256(abi.encode(_previousBlock.blockHash, _newBlock.newStateRoot));
122: chainedPriorityTxsHash = keccak256(abi.encode(chainedPriorityTxsHash, canonicalTxHash));
182: concatHash = keccak256(abi.encode(concatHash, priorityOp.canonicalTxHash));
359: return keccak256(abi.encode(passThroughDataHash, metadataHash, auxiliaryOutputHash));
381: return abi.encode(_block.l2LogsTreeRoot, l2ToL1LogsHash, initialStorageChangesHash, repeatedStorageChangesHash);
386: return keccak256(abi.encode(_storedBlockInfo));
163: canonicalTxHash = keccak256(abi.encode(transaction));
30: currentHash = keccak256(abi.encode(currentHash, _path[i]));
32: currentHash = keccak256(abi.encode(_path[i], currentHash));
114: bytes32 constructorInputHash = keccak256(abi.encode(address(l2TokenFactory), ""));
In some cases, having function arguments in calldata instead of memory is more optimal.
Consider the following generic example:
contract C {
function add(uint[] memory arr) external returns (uint sum) {
uint length = arr.length;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
}
In the above example, the dynamic array arr has the storage location memory. When the function gets called externally, the array values are kept in calldata and copied to memory during ABI decoding (using the opcode calldataload and mstore). And during the for loop, arr[i] accesses the value in memory using a mload. However, for the above example this is inefficient. Consider the following snippet instead:
contract C {
function add(uint[] calldata arr) external returns (uint sum) {
uint length = arr.length;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
}
In the above snippet, instead of going via memory, the value is directly read from calldata using calldataload. That is, there are no intermediate memory operations that carries this value.
Gas savings: In the former example, the ABI decoding begins with copying value from calldata to memory in a for loop. Each iteration would cost at least 60 gas. In the latter example, this can be completely avoided. This will also reduce the number of instructions and therefore reduces the deploy time cost of the contract.
In short, use calldata instead of memory if the function argument is only read.
Note that in older Solidity versions, changing some function arguments from memory to calldata may cause "unimplemented feature error". This can be avoided by using a newer (0.8.*) Solidity compiler.
Examples Note: The following pattern is prevalent in the codebase:
function f(bytes memory data) external {
(...) = abi.decode(data, (..., types, ...));
}
Here, changing to bytes calldata will decrease the gas. The total savings for this change across all such uses would be quite significant.
function _addFunctions(
address _facet,
bytes4[] memory _selectors,
bool _isFacetFreezable
) private {
function _replaceFunctions(
address _facet,
bytes4[] memory _selectors,
bool _isFacetFreezable
) private {
function _removeFunctions(address _facet, bytes4[] memory _selectors) private {
The following functions could be set external to save gas and improve code quality. External call cost is less expensive than of public functions.
function l2TransactionBaseCost(
uint256,
uint256,
uint32
) public pure returns (uint256) {
function l2TokenAddress(address _l1Token) public view override returns (address) {
When using elements that are smaller than 32 bytes, your contract's gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html
Each operation involving a uint8
costs an extra 22-28 gas (depending on whether the other operand is also a variable of type uint8
) as compared to ones involving uint256
, due to the compiler having to clear the higher bits of the memory word before operating on the uint8
, as well as the associated stack operations of doing so. Use a larger size then downcast where needed
26: uint160 constant SYSTEM_CONTRACTS_OFFSET = 0x8000;
64: uint8 version = uint8(_bytecodeHash[0]);
40: uint8 l2ShardId;
https://github.com/code-423n4/2022-10-zksync/tree/main/ethereum/contracts/zksync/Storage.sol#L40
54: uint16 txNumberInBlock;
https://github.com/code-423n4/2022-10-zksync/tree/main/ethereum/contracts/zksync/Storage.sol#L54
125: uint64 expirationBlock = uint64(block.number + PRIORITY_EXPIRATION);
41: uint64 blockNumber;
42: uint64 timestamp;
43: uint64 indexRepeatedStorageChanges;
24: uint16 selectorPosition;
33: uint16 facetPosition;
207: uint16 selectorPosition = uint16(ds.facetToSelectors[_facet].selectors.length);
12: uint64 expirationBlock;
13: uint192 layer2Tip;
17: uint160 constant SYSTEM_CONTRACTS_OFFSET = 0x8000;
https://github.com/code-423n4/2022-10-zksync/tree/main/zksync/contracts/L2ContractHelper.sol#L17
29: uint8 private decimals_;
public
/external
function names and public
member variable names can be optimized to save gas. See this link for an example of how it works. Method IDs that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shifted
Relevant to all in-scope contracts.
If you don't use boolean for storage you will avoid Gwarmaccess 100 gas. In addition, state changes of boolean from true
to false
can cost up to ~20000 gas rather than uint256(2)
to uint256(1)
that would cost significantly less.
40: mapping(uint256 => mapping(uint256 => bool)) public isWithdrawalFinalized;
252: isWithdrawalFinalized[_l2BlockNumber][_l2MessageIndex] = true;
37: mapping(uint256 => mapping(uint256 => bool)) public isWithdrawalFinalized;
207: isWithdrawalFinalized[_l2BlockNumber][_l2MessageIndex] = true;
26: mapping(address => bool) public isAccessPublic;
30: mapping(address => mapping(address => mapping(bytes4 => bool))) public hasSpecialAccessToCall;
40: s.validators[_validator] = true;
22: mapping(address => bool) securityCouncilMembers;
https://github.com/code-423n4/2022-10-zksync/tree/main/ethereum/contracts/zksync/Storage.sol#L22
77: mapping(address => bool) validators;
https://github.com/code-423n4/2022-10-zksync/tree/main/ethereum/contracts/zksync/Storage.sol#L77
68: diamondStorage.isFrozen = false;
98: diamondStorage.isFrozen = false;
83: diamondStorage.isFrozen = true;
133: isSystemContextLogProcessed = true;
72: getters.ignoreName = true;
78: getters.ignoreSymbol = true;
88: getters.ignoreDecimals = true;
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
struct AppStorage {
DiamondCutStorage diamondCutStorage;
address governor;
address pendingGovernor;
mapping(address => bool) validators;
Verifier verifier;
uint256 totalBlocksExecuted;
uint256 totalBlocksVerified;
uint256 totalBlocksCommitted;
mapping(uint256 => bytes32) storedBlockHashes;
mapping(uint256 => bytes32) l2LogsRootHashes;
PriorityQueue.Queue priorityQueue;
IAllowList allowList;
VerifierParams verifierParams;
bytes32 l2BootloaderBytecodeHash;
bytes32 l2DefaultAccountBytecodeHash;
bool zkPorterIsAvailable;
}
Saving 1 slot by changing to:
struct AppStorage {
DiamondCutStorage diamondCutStorage;
bool zkPorterIsAvailable; //@audit 8 bits
address governor; //@audit 160 bits
address pendingGovernor;
mapping(address => bool) validators;
Verifier verifier;
uint256 totalBlocksExecuted;
uint256 totalBlocksVerified;
uint256 totalBlocksCommitted;
mapping(uint256 => bytes32) storedBlockHashes;
mapping(uint256 => bytes32) l2LogsRootHashes;
PriorityQueue.Queue priorityQueue;
IAllowList allowList;
VerifierParams verifierParams;
bytes32 l2BootloaderBytecodeHash;
bytes32 l2DefaultAccountBytecodeHash;
}
struct StoredBlockInfo {
uint64 blockNumber;
bytes32 blockHash;
uint64 indexRepeatedStorageChanges;
uint256 numberOfLayer1Txs;
bytes32 priorityOperationsHash;
bytes32 l2LogsTreeRoot;
uint256 timestamp;
bytes32 commitment;
}
Saving 1 slot by changing to:
struct StoredBlockInfo {
uint64 blockNumber; //@audit 64 bits
uint64 indexRepeatedStorageChanges; //@audit 64 bits
bytes32 blockHash;
uint256 numberOfLayer1Txs;
bytes32 priorityOperationsHash;
bytes32 l2LogsTreeRoot;
uint256 timestamp;
bytes32 commitment;
}
block.number
and block.timestamp
are added to the event information by default, so adding them manually will waste additional gas.
76: event BlockCommit(uint256 indexed blockNumber, bytes32 indexed blockHash, bytes32 indexed commitment);
82: event BlockExecution(uint256 indexed blockNumber, bytes32 indexed blockHash, bytes32 indexed commitment);
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.
12: uint256 constant L2_TO_L1_LOGS_COMMITMENT_BYTES = 4 + L2_TO_L1_LOG_SERIALIZE_SIZE * 512;
https://github.com/code-423n4/2022-10-zksync/tree/main/ethereum/contracts/zksync/Config.sol#L12
23: uint256 constant INITIAL_STORAGE_CHANGES_COMMITMENT_BYTES = 4 + 64 * 4896;
https://github.com/code-423n4/2022-10-zksync/tree/main/ethereum/contracts/zksync/Config.sol#L23
26: uint256 constant REPEATED_STORAGE_CHANGES_COMMITMENT_BYTES = 4 + 40 * 7787;
https://github.com/code-423n4/2022-10-zksync/tree/main/ethereum/contracts/zksync/Config.sol#L26