The following sections detail the gas optimizations found throughout the codebase.
Each optimization is documented with the setup, an explainer for the optimization, a gas report and line identifiers for each optimization across the codebase. For each section's gas report, the optimizer was turned on and set to 10000 runs.
You can replicate any tests/gas reports by heading to 0xKitsune/gas-lab and cloning the repo. Then, simply copy/paste the contract examples from any section and run forge test --gas-report.
You can also easily update the optimizer runs in the foundry.toml.
Consider marking constants as private
Marking constant variables in storage as constant saves gas. Unless a constant variable should be easily accessible by another protocol or offchain logic, consider marking it as private.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public view {
uint256 a = 100;
c0.addPublicConstant(a);
c1.addPrivateConstant(a);
}
}
contract Contract0 {
uint256 constant public x = 100;
function addPublicConstant(uint256 a) external pure returns (uint256) {
return a + x;
}
}
contract Contract1 {
uint256 constant private x = 100;
function addPrivateConstant(uint256 a) external pure returns (uint256) {
return a +x;
}
}
unchecked{++i} instead of i++ (or use assembly when applicable)
Use ++i instead of i++. This is especially useful in for loops but this optimization can be used anywhere in your code. You can also use unchecked{++i;} for even more gas savings but this will not check to see if i overflows. For extra safety if you are worried about this, you can add a require statement after the loop checking if i is equal to the final incremented value. For best gas savings, use inline assembly, however this limits the functionality you can achieve. For example you cant use Solidity syntax to internally call your own contract within an assembly block and external calls must be done with the call() or delegatecall() instruction. However when applicable, inline assembly will save much more gas.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
Contract2 c2;
Contract3 c3;
Contract4 c4;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
c2 = new Contract2();
c3 = new Contract3();
c4 = new Contract4();
}
function testGas() public {
c0.iPlusPlus();
c1.plusPlusI();
c2.uncheckedPlusPlusI();
c3.safeUncheckedPlusPlusI();
c4.inlineAssemblyLoop();
}
}
contract Contract0 {
//loop with i++
function iPlusPlus() public pure {
uint256 j = 0;
for (uint256 i; i < 10; i++) {
j++;
}
}
}
contract Contract1 {
//loop with ++i
function plusPlusI() public pure {
uint256 j = 0;
for (uint256 i; i < 10; ++i) {
j++;
}
}
}
contract Contract2 {
//loop with unchecked{++i}
function uncheckedPlusPlusI() public pure {
uint256 j = 0;
for (uint256 i; i < 10; ) {
j++;
unchecked {
++i;
}
}
}
}
contract Contract3 {
//loop with unchecked{++i} with additional overflow check
function safeUncheckedPlusPlusI() public pure {
uint256 j = 0;
uint256 i = 0;
for (i; i < 10; ) {
j++;
unchecked {
++i;
}
}
//check for overflow
assembly {
if lt(i, 10) {
mstore(0x00, "loop overflow")
revert(0x00, 0x20)
}
}
}
}
contract Contract4 {
//loop with inline assembly
function inlineAssemblyLoop() public pure {
assembly {
let j := 0
for {
let i := 0
} lt(i, 10) {
i := add(i, 0x01)
} {
j := add(j, 0x01)
}
}
}
}
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public view {
c0.solidityHash(2309349, 2304923409);
c1.assemblyHash(2309349, 2304923409);
}
}
contract Contract0 {
function solidityHash(uint256 a, uint256 b) public view {
//unoptimized
keccak256(abi.encodePacked(a, b));
}
}
contract Contract1 {
function assemblyHash(uint256 a, uint256 b) public view {
//optimized
assembly {
mstore(0x00, a)
mstore(0x20, b)
let hashedVal := keccak256(0x00, 0x40)
}
}
}
Use calldata instead of memory for function arguments that do not get mutated.
Mark data types as calldata instead of memory where possible. This makes it so that the data is not automatically loaded into memory. If the data passed into the function does not need to be changed (like updating values in an array), it can be passed in as calldata. The one exception to this is if the argument must later be passed into another function that takes an argument that specifies memory storage.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
Contract2 c2;
Contract3 c3;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
c2 = new Contract2();
c3 = new Contract3();
}
function testGas() public {
uint256[] memory arr = new uint256[](10);
c0.calldataArray(arr);
c1.memoryArray(arr);
bytes memory data = abi.encode("someText");
c2.calldataBytes(data);
c3.memoryBytes(data);
}
}
contract Contract0 {
function calldataArray(uint256[] calldata arr) public {
uint256 j;
for (uint256 i; i < arr.length; i++) {
j = arr[i] + 10;
}
}
}
contract Contract1 {
function memoryArray(uint256[] memory arr) public {
uint256 j;
for (uint256 i; i < arr.length; i++) {
j = arr[i] + 10;
}
}
}
contract Contract2 {
function calldataBytes(bytes calldata data) public {
bytes32 val;
for (uint256 i; i < 10; i++) {
val = keccak256(abi.encode(data, i));
}
}
}
contract Contract3 {
function memoryBytes(bytes memory data) public {
bytes32 val;
for (uint256 i; i < 10; i++) {
val = keccak256(abi.encode(data, i));
}
}
}
Use assembly for math instead of Solidity. You can check for overflow/underflow in assembly to ensure safety. If using Solidity versions < 0.8.0 and you are using Safemath, you can gain significant gas savings by using assembly to calculate values and checking for overflow/underflow.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
Contract2 c2;
Contract3 c3;
Contract4 c4;
Contract5 c5;
Contract6 c6;
Contract7 c7;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
c2 = new Contract2();
c3 = new Contract3();
c4 = new Contract4();
c5 = new Contract5();
c6 = new Contract6();
c7 = new Contract7();
}
function testGas() public {
c0.addTest(34598345, 100);
c1.addAssemblyTest(34598345, 100);
c2.subTest(34598345, 100);
c3.subAssemblyTest(34598345, 100);
c4.mulTest(34598345, 100);
c5.mulAssemblyTest(34598345, 100);
c6.divTest(34598345, 100);
c7.divAssemblyTest(34598345, 100);
}
}
contract Contract0 {
//addition in Solidity
function addTest(uint256 a, uint256 b) public pure {
uint256 c = a + b;
}
}
contract Contract1 {
//addition in assembly
function addAssemblyTest(uint256 a, uint256 b) public pure {
assembly {
let c := add(a, b)
if lt(c, a) {
mstore(0x00, "overflow")
revert(0x00, 0x20)
}
}
}
}
contract Contract2 {
//subtraction in Solidity
function subTest(uint256 a, uint256 b) public pure {
uint256 c = a - b;
}
}
contract Contract3 {
//subtraction in assembly
function subAssemblyTest(uint256 a, uint256 b) public pure {
assembly {
let c := sub(a, b)
if gt(c, a) {
mstore(0x00, "underflow")
revert(0x00, 0x20)
}
}
}
}
contract Contract4 {
//multiplication in Solidity
function mulTest(uint256 a, uint256 b) public pure {
uint256 c = a * b;
}
}
contract Contract5 {
//multiplication in assembly
function mulAssemblyTest(uint256 a, uint256 b) public pure {
assembly {
let c := mul(a, b)
if lt(c, a) {
mstore(0x00, "overflow")
revert(0x00, 0x20)
}
}
}
}
contract Contract6 {
//division in Solidity
function divTest(uint256 a, uint256 b) public pure {
uint256 c = a * b;
}
}
contract Contract7 {
//division in assembly
function divAssemblyTest(uint256 a, uint256 b) public pure {
assembly {
let c := div(a, b)
if gt(c, a) {
mstore(0x00, "underflow")
revert(0x00, 0x20)
}
}
}
}
When defining storage variables, make sure to declare them in ascending order, according to size. When multiple variables are able to fit into one 256 bit slot, this will save storage size and gas during runtime. For example, if you have a bool, uint256 and a bool, instead of defining the variables in the previously mentioned order, defining the two boolean variables first will pack them both into one storage slot since they only take up one byte of storage.
You can mark public or external functions as payable to save gas. Functions that are not payable have additional logic to check if there was a value sent with a call, however, making a function payable eliminates this check. This optimization should be carefully considered due to potentially unwanted behavior when a function does not need to accept ether.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public {
c0.isNotPayable();
c1.isPayable();
}
}
contract Contract0 {
function isNotPayable() public view {
uint256 val = 0;
val++;
}
}
contract Contract1 {
function isPayable() public payable {
uint256 val = 0;
val++;
}
}
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public view {
c0.ownerNotZero(address(this));
c1.assemblyOwnerNotZero(address(this));
}
}
contract Contract0 {
function ownerNotZero(address _addr) public pure {
require(_addr != address(0), "zero address)");
}
}
contract Contract1 {
function assemblyOwnerNotZero(address _addr) public pure {
assembly {
if iszero(_addr) {
mstore(0x00, "zero address")
revert(0x00, 0x20)
}
}
}
}
Instead of if (x == bool), use if(x) or when applicable, use assembly with iszero(iszero(x)).
It is redundant to check if(x == true) or any form of boolean comparison. You can slightly reduce gas consumption by using if (x) instead. When applicable, you can also use assembly to save more gas by using iszeroiszero(x) instead of if (x) and iszero(x) for if (!x)
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
Contract2 c2;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
c2 = new Contract2();
}
function testGas() public view {
bool check = true;
c0.ifBoolEqualsBool(check);
c1.ifBool(check);
c2.iszeroIszero(check);
}
}
contract Contract0 {
function ifBoolEqualsBool(bool check) public pure {
if (check == true) {
return;
}
}
}
contract Contract1 {
function ifBool(bool check) public pure {
if (check) {
return;
}
}
}
contract Contract2 {
function iszeroIszero(bool check) public pure {
assembly {
if iszero(iszero(check)) {
revert(0x00, 0x00)
}
}
}
}
Mark storage variables as immutable if they never change after contract initialization.
State variables can be declared as constant or immutable. In both cases, the variables cannot be modified after the contract has been constructed. For constant variables, the value has to be fixed at compile-time, while for immutable, it can still be assigned at construction time.
The compiler does not reserve a storage slot for these variables, and every occurrence is inlined by the respective value.
Compared to regular state variables, the gas costs of constant and immutable variables are much lower. For a constant variable, the expression assigned to it is copied to all the places where it is accessed and also re-evaluated each time. This allows for local optimizations. Immutable variables are evaluated once at construction time and their value is copied to all the places in the code where they are accessed. For these values, 32 bytes are reserved, even if they would fit in fewer bytes. Due to this, constant values can sometimes be cheaper than immutable values.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
Contract2 c2;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
c2 = new Contract2();
}
function testGas() public view {
c0.addValue();
c1.addImmutableValue();
c2.addConstantValue();
}
}
contract Contract0 {
uint256 val;
constructor() {
val = 10000;
}
function addValue() public view {
uint256 newVal = val + 1000;
}
}
contract Contract1 {
uint256 immutable val;
constructor() {
val = 10000;
}
function addImmutableValue() public view {
uint256 newVal = val + 1000;
}
}
contract Contract2 {
uint256 constant val = 10;
function addConstantValue() public view {
uint256 newVal = val + 1000;
}
}
c4udit Report
Files analyzed
Issues found
Use immutable for OpenZeppelin AccessControl's Roles Declarations
Impact
Issue Information: G006
Findings:
Tools used
c4udit
Long Revert Strings
Impact
Issue Information: G007
Findings:
Tools used
c4udit
Gas Optimizations - (Total Optimizations 286)
The following sections detail the gas optimizations found throughout the codebase. Each optimization is documented with the setup, an explainer for the optimization, a gas report and line identifiers for each optimization across the codebase. For each section's gas report, the optimizer was turned on and set to 10000 runs. You can replicate any tests/gas reports by heading to 0xKitsune/gas-lab and cloning the repo. Then, simply copy/paste the contract examples from any section and run
forge test --gas-report
. You can also easily update the optimizer runs in thefoundry.toml
.Consider marking constants as private
Marking constant variables in storage as constant saves gas. Unless a constant variable should be easily accessible by another protocol or offchain logic, consider marking it as private.
Gas Report
Lines
Use custom errors instead of string error messages
Gas Report
Lines
unchecked{++i}
instead ofi++
(or use assembly when applicable)Use
++i
instead ofi++
. This is especially useful in for loops but this optimization can be used anywhere in your code. You can also useunchecked{++i;}
for even more gas savings but this will not check to see ifi
overflows. For extra safety if you are worried about this, you can add a require statement after the loop checking ifi
is equal to the final incremented value. For best gas savings, use inline assembly, however this limits the functionality you can achieve. For example you cant use Solidity syntax to internally call your own contract within an assembly block and external calls must be done with thecall()
ordelegatecall()
instruction. However when applicable, inline assembly will save much more gas.Gas Report
Lines
Use assembly to hash instead of Solidity
Gas Report
Lines
Use
calldata
instead ofmemory
for function arguments that do not get mutated.Mark data types as
calldata
instead ofmemory
where possible. This makes it so that the data is not automatically loaded into memory. If the data passed into the function does not need to be changed (like updating values in an array), it can be passed in ascalldata
. The one exception to this is if the argument must later be passed into another function that takes an argument that specifiesmemory
storage.Gas Report
Lines
Use assembly for math (add, sub, mul, div)
Use assembly for math instead of Solidity. You can check for overflow/underflow in assembly to ensure safety. If using Solidity versions < 0.8.0 and you are using Safemath, you can gain significant gas savings by using assembly to calculate values and checking for overflow/underflow.
Gas Report
Lines
Tightly pack storage variables
When defining storage variables, make sure to declare them in ascending order, according to size. When multiple variables are able to fit into one 256 bit slot, this will save storage size and gas during runtime. For example, if you have a
bool
,uint256
and abool
, instead of defining the variables in the previously mentioned order, defining the two boolean variables first will pack them both into one storage slot since they only take up one byte of storage.Gas Report
Lines
Use assembly to write storage values
Gas Report
Lines
Mark functions as payable (with discretion)
You can mark public or external functions as payable to save gas. Functions that are not payable have additional logic to check if there was a value sent with a call, however, making a function payable eliminates this check. This optimization should be carefully considered due to potentially unwanted behavior when a function does not need to accept ether.
Gas Report
Lines
Use assembly to check for address(0)
Gas Report
Lines
Instead of
if (x == bool)
, useif(x)
or when applicable, use assembly withiszero(iszero(x))
.It is redundant to check
if(x == true)
or any form of boolean comparison. You can slightly reduce gas consumption by usingif (x)
instead. When applicable, you can also use assembly to save more gas by usingiszeroiszero(x)
instead ofif (x)
andiszero(x)
forif (!x)
Gas Report
Lines
Mark storage variables as
immutable
if they never change after contract initialization.State variables can be declared as constant or immutable. In both cases, the variables cannot be modified after the contract has been constructed. For constant variables, the value has to be fixed at compile-time, while for immutable, it can still be assigned at construction time.
The compiler does not reserve a storage slot for these variables, and every occurrence is inlined by the respective value.
Compared to regular state variables, the gas costs of constant and immutable variables are much lower. For a constant variable, the expression assigned to it is copied to all the places where it is accessed and also re-evaluated each time. This allows for local optimizations. Immutable variables are evaluated once at construction time and their value is copied to all the places in the code where they are accessed. For these values, 32 bytes are reserved, even if they would fit in fewer bytes. Due to this, constant values can sometimes be cheaper than immutable values.
Gas Report
Lines
Use multiple require() statments insted of require(expression && expression && ...)
You can safe gas by breaking up a require statement with multiple conditions, into multiple require statements with a single condition.
Gas Report
Lines