Description:
OpenZeppelin uint256 and bool comparison:
Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled. The values being non-zero value makes deployment a bit more expensive.
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from ‘false’ to ‘true’, after having been ‘true’ in the past.
Recommendation:
Use uint256(1) and uint256(2) instead of bool.
Proof Of Concept:
The optimizer was turned on and set to 10000 runs.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public {
c0._setPolicyPermissions(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 79, true);
c1._setPolicyPermissions(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 79, 2);
}
}
contract Contract0 {
mapping(address => mapping(uint256 => bool)) public modulePermissions;
error boolcheck();
function _setPolicyPermissions(address addr_, uint256 id, bool log) external {
modulePermissions[addr_][id] = log;
if(modulePermissions[addr_][id] = false) revert boolcheck();
}
}
contract Contract1 {
mapping(address => mapping(uint256 => uint256)) public modulePermissions;
error boolcheck();
// Use uint256 instead of bool (1 = false , 2 = true)
function _setPolicyPermissions(address addr_, uint256 id, uint256 log) external {
modulePermissions[addr_][id] == log;
if(modulePermissions[addr_][id] == 1) revert boolcheck();
}
}
Proof Of Concept:
The optimizer was turned on and set to 10000 runs.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public view {
c0.setOperator(address(this));
c1.setOperator(address(this));
}
}
contract Contract0 {
function setOperator(address operator_) public pure {
require(operator_) != address(0), "INVALID_RECIPIENT"); }
}
contract Contract1 {
function setOperator(address operator_) public pure {
assembly {
if iszero(operator_) {
mstore(0x00, "Callback_InvalidParams")
revert(0x00, 0x20)
}
}
}
}
Description:
Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they’re hitby avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas.
https://blog.soliditylang.org/2021/04/21/custom-errors/
Proof Of Concept:
The optimizer was turned on and set to 10000 runs.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public {
c0.foo();
c1.foo();
}
}
contract Contract0 {
uint256 version;
error Error_Message();
function foo() external {
if (version != 0) {
revert Error_Message();
}
}
}
contract Contract1 {
uint256 version;
function foo() external {
require(version == 0, "error");
}
}
Description:
If a function modifier or require such as onlyOwner-admin is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
Recommendation:
Functions guaranteed to revert when called by normal users can be marked payable (for only onlyGovernor or admin functions)
Proof Of Concept:
The optimizer was turned on and set to 10000 runs.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public {
c0.foo();
c1.foo();
}
}
contract Contract0 {
uint256 versionNFTDropCollection;
function foo() external {
versionNFTDropCollection++;
}
}
contract Contract1 {
uint256 versionNFTDropCollection;
function foo() external payable {
versionNFTDropCollection++;
}
}
Description:
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.
Recommendation:
Revert strings > 32 bytes or use Custom Error()
Proof Of Concept:
The optimizer was turned on and set to 10000 runs.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public {
c0.foo();
c1.foo();
}
}
contract Contract0 {
uint256 test1;
function foo() external {
require(test1 != 0, "This is Error");
}
}
contract Contract1 {
uint256 test1;
function foo() external {
require(test1 != 0, "Your Project Drop List Has Error with ID and other parameters");
}
}
Description:
Using double require instead of operator && can save more gas
When having a require statement with 2 or more expressions needed, place the expression that cost less gas first.
Recommendation:
Governed.sol#L54-L57;
require(
pendingGovernor != address(0) && msg.sender == pendingGovernor,
"Caller must be pending governor"
);
Recommendation Code:
require(pendingGovernor != address(0), ”Invalid pending governor”);
require(msg.sender == pendingGovernor, ” Caller must be pending governor”);
[G-08] Structs can be packed into fewer storage slots (20k gas)
Recommendation:
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.
[G-09] Optimize names to save gas [22 gas per instance]
Context:
All Contracts
Description:
Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.
Recommendation:
Find a lower method ID name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas
For example, the function IDs in the L1GraphTokenGateway.sol contract will be the most used; A lower method ID may be given.
Description:
OpenZeppelin uint256 and bool comparison :
// Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
Recommendation:
Use uint256(1) ve uint256(2) instead of bool
[S-01] Generate perfect code headers every time
Description:
I recommend using header for Solidity code layout and readability
[S-02] Mark visibility of initialize(...) functions as external
Description:
If someone wants to extend via inheritance, it might make more sense that the overridden initialize(...) function calls the internal {...}_init function, not the parent public initialize(...) function.
External instead of public would give more the sense of the initialize(...) functions to behave like a constructor (only called on deployment, so should only be called externally)
Security point of view, it might be safer so that it cannot be called internally by accident in the child contract
It might cost a bit less gas to use external over public
It is possible to override a function from external to public (= "opening it up") ✅
but it is not possible to override a function from public to external (= "narrow it down"). ❌
For above reasons you can change initialize(...) to external
Gas Optimizations List
uint256
instead ofbool
in mappings is more gas efficientassembly
to write address storage valuesaddress(0)
Custom Errors
rather than revert() / require() strings to save deployment gas`payable
double require
instead of using&&
Total 11 issues
[G-01] Using
uint256
instead ofbool
in mappings is more gas efficient [146 gas per instance]Context: L1GraphTokenGateway.sol#L35 GraphTokenUpgradeable.sol#L51
Description: OpenZeppelin uint256 and bool comparison: Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled. The values being non-zero value makes deployment a bit more expensive.
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from ‘false’ to ‘true’, after having been ‘true’ in the past.
Recommendation: Use uint256(1) and uint256(2) instead of bool.
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-02] Use
assembly
to write address storage values [33 gas per instance]Context: L1GraphTokenGateway.sol#L112-L113, L1GraphTokenGateway.sol#L123, L1GraphTokenGateway.sol#L133, L1GraphTokenGateway.sol#L143, Governed.sol#L44, Pausable.sol#L57, L2GraphTokenGateway.sol#L99, L2GraphTokenGateway.sol#L109, L2GraphTokenGateway.sol#L119, L2GraphToken.sol#L61, L2GraphToken.sol#L71
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-03] Use assembly to check for
address(0)
[6 gas per instance]Context: L1GraphTokenGateway.sol#L74, L1GraphTokenGateway.sol#L110-L111, L1GraphTokenGateway.sol#L122, L1GraphTokenGateway.sol#L132, L1GraphTokenGateway.sol#L153, L1GraphTokenGateway.sol#L165, L1GraphTokenGateway.sol#L202, Governed.sol#L41, Managed.sol#L104, L2GraphTokenGateway.sol#L98, L2GraphTokenGateway.sol#L108, L2GraphTokenGateway.sol#L118, L2GraphTokenGateway.sol#L148, GraphTokenUpgradeable.sol#L106, L2GraphToken.sol#L49, L2GraphToken.sol#L60, L2GraphToken.sol#L70, GraphProxy.sol#L105
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-04] Use
Custom Errors
rather than revert() / require() strings to save deployment gas [68 gas per instance]Context: GraphTokenGateway.sol#L21, GraphTokenGateway.sol#L31, GraphTokenGateway.sol#L40, L1GraphTokenGateway.sol#L74, L1GraphTokenGateway.sol#L78, L1GraphTokenGateway.sol#L82, L1GraphTokenGateway.sol#L110-L111, L1GraphTokenGateway.sol#L122, L1GraphTokenGateway.sol#L132, L1GraphTokenGateway.sol#L142, L1GraphTokenGateway.sol#L153-L154, L1GraphTokenGateway.sol#L165-L166, L1GraphTokenGateway.sol#L200-L202, L1GraphTokenGateway.sol#L213-L217, L1GraphTokenGateway.sol#L224, L1GraphTokenGateway.sol#L271, L1GraphTokenGateway.sol#L275, Governed.sol#L24, Governed.sol#L41, Governed.sol#L54-L57, Managed.sol#L44-L45, Managed.sol#L49, Managed.sol#L53, Managed.sol#L57, Managed.sol#L104, L2GraphTokenGateway.sol#L69-L72, L2GraphTokenGateway.sol#L98, L2GraphTokenGateway.sol#L108, L2GraphTokenGateway.sol#L118, L2GraphTokenGateway.sol#L145-L148, L2GraphTokenGateway.sol#L153, L2GraphTokenGateway.sol#L233-L234, GraphTokenUpgradeable.sol#L60, GraphTokenUpgradeable.sol#L94-L95, GraphTokenUpgradeable.sol#L106, GraphTokenUpgradeable.sol#L115, GraphTokenUpgradeable.sol#L123, L2GraphToken.sol#L36, L2GraphToken.sol#L49, L2GraphToken.sol#L60, L2GraphToken.sol#L70, GraphProxy.sol#L105, GraphProxy.sol#L133, GraphProxy.sol#L141-L145, GraphProxy.sol#L157, GraphProxyAdmin.sol#L34, GraphProxyAdmin.sol#L47, GraphProxyAdmin.sol#L59, GraphProxyStorage.sol#L62, GraphUpgradeable.sol#L24, GraphUpgradeable.sol#L32
Description: Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they’re hitby avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas. https://blog.soliditylang.org/2021/04/21/custom-errors/
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-05] Functions guaranteed to revert when callled by normal users can be marked
payable
[24 gas per instance]Context: BridgeEscrow.sol#L28, BridgeEscrow.sol#L36, L1GraphTokenGateway.sol#L109, L1GraphTokenGateway.sol#L121, L1GraphTokenGateway.sol#L131, L1GraphTokenGateway.sol#L141, L1GraphTokenGateway.sol#L152, L1GraphTokenGateway.sol#L164, Governed.sol#L40, L2GraphTokenGateway.sol#L97, L2GraphTokenGateway.sol#L107, L2GraphTokenGateway.sol#L117, GraphTokenUpgradeable.sol#L105, GraphTokenUpgradeable.sol#L114, L2GraphToken.sol#L59, L2GraphToken.sol#L69, GraphProxyAdmin.sol#L100
Description: If a function modifier or require such as onlyOwner-admin is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
Recommendation: Functions guaranteed to revert when called by normal users can be marked payable (for only
onlyGovernor or admin
functions)Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-06] Reduce the size of error messages (Long revert Strings) [18 gas per instance]
Context: GraphTokenGateway.sol#L21, Managed.sol#L53, GraphProxy.sol#L105, GraphProxy.sol#L141, GraphProxy.sol#L144, GraphUpgradeable.sol#L32
Description: 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.Recommendation: Revert strings > 32 bytes or use Custom Error()
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-07] Use
double require
instead of using&&
Context: L1GraphTokenGateway.sol#L142 GraphProxy.sol#L143 Governed.sol#L55
Description: Using double require instead of operator && can save more gas When having a require statement with 2 or more expressions needed, place the expression that cost less gas first.
Recommendation: Governed.sol#L54-L57;
Recommendation Code:
[G-08] Structs can be packed into fewer storage slots (20k gas)
Context: IStakingData.sol#L10-L19
Recommendation: 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.
[G-09] Optimize names to save gas [22 gas per instance]
Context: All Contracts
Description: Contracts most called functions could simply save gas by function ordering via
Method ID
. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because22 gas
are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.Recommendation: Find a lower
method ID
name for the most called functions for example Call() vs. Call1() is cheaper by22 gas
For example, the function IDs in theL1GraphTokenGateway.sol
contract will be the most used; A lower method ID may be given.Proof of Consept: https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92
L1GraphTokenGateway.sol function names can be named and sorted according to METHOD ID
[G-10] Instead of pre-calculating the state to be broadcast in an emit, calculate in the emit, this method saves gas
Context: L1GraphTokenGateway.sol#L114, L1GraphTokenGateway.sol#L124, L1GraphTokenGateway.sol#L134, L1GraphTokenGateway.sol#L144, L1GraphTokenGateway.sol#L156, L1GraphTokenGateway.sol#L168, Governed.sol#L46, Governed.sol#L65-L66, L2GraphTokenGateway.sol#L100, L2GraphTokenGateway.sol#L110, L2GraphTokenGateway.sol#L120, L2GraphToken.sol#L62, L2GraphToken.sol#L72
[G-11] Using uint256 instead of bool in mappings is more gas efficient
Context: L1GraphTokenGateway.sol#L35 GraphTokenUpgradeable.sol#L51
Description: OpenZeppelin uint256 and bool comparison : // Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled. // The values being non-zero value makes deployment a bit more expensive,
Recommendation: Use uint256(1) ve uint256(2) instead of bool
[S-01] Generate perfect code headers every time
Description: I recommend using header for Solidity code layout and readability
https://github.com/transmissions11/headers
[S-02] Mark visibility of initialize(...) functions as
external
Description: If someone wants to extend via inheritance, it might make more sense that the overridden initialize(...) function calls the internal {...}_init function, not the parent public initialize(...) function.
External instead of public would give more the sense of the initialize(...) functions to behave like a constructor (only called on deployment, so should only be called externally)
Security point of view, it might be safer so that it cannot be called internally by accident in the child contract
It might cost a bit less gas to use external over public
It is possible to override a function from external to public (= "opening it up") ✅ but it is not possible to override a function from public to external (= "narrow it down"). ❌
For above reasons you can change initialize(...) to external
https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3750