Gas Optimization for Smart Contracts
What is Gas
- In Ethereum, "Gas" refers to the unit that measures the amount of computational effort required to execute operations like transactions or smart contracts. It's an essential concept, similar to the fuel that powers car engines, but in this case, it's for executing operations on the Ethereum network.
- All transactions on Ethereum requires Gas
How tx cost is calculated when transferring ETH
- The price of Gas Units is measures usually measured in Gwei(
1 ETH = 1,000,000,000 gwei
).
- The total cost of
tx = Gas Used * Gas Price(in Gwei) / 10**9 ETH
- An ETH transfer takes 21,000 Gas Units.
- In the below example you can see that the Gas Price is 15.59027 Gwei and the Gas Used is 21,000 Units
- The total
tx cost = 21,000 * 15.59027 / 10**9 = 0.000327 ETH
Gas after EIP-1559
- Before EIP-1559, the entire gas fee in a tx will go to the miner
- After EIP-1559, the gas fee is divided into
MaxFeePerGas
- This is the maximum amount of Gwei you are willing to pay for a tx
MaxPriorityFeePerGas
- This is a subset ofMaxFeePerGas
. This is how much the miner might receive for adding your tx to the block.
BaseFee
- This is the amount that gets burnt on executing the Tx.
- BaseFee is determined at the protocol level. This fee is calculated for every block and every tx in that block has to burn the calculated amount as BaseFee.
- Roughly, the BaseFee increases by 12% if the last block was full and decreases by 12% if the last block was empty.
- You can see that the Gas target for block
18924920
is 100 % and the BaseFee for the block is11
.
334 Gwei
. The BaseFee for the next block18924921
is 12% higher at11.334 * 1.12 = 12.7 Gwei.
- You can set a
MaxBaseFee
and aPriorityFee
in many wallets. The MaxBaseFee is the maximum you are willing to pay to have your tx recorded on the chain. This is typically higher than the current block’sBaseFee
. Any extra amount will be refunded to your wallet.
- The
PriorityFee
here is actually theMaxPriorityFeePerGas
. This is the maximum amount you are willing to give the miner after theBaseFee
has been burnt.
- In a tx, you typically specify a
MaxBaseFee
and aPriorityFee
.- MaxBaseFee - BaseFee = leftover
- if (
leftover > PriorityFee
) → the miner gets the PriorityFee and you get a refund of(leftover - PriorityFee)
- else if (
leftover ≤ PriorityFee
) - > the miner getsleftover
and you get0
refund.
Txs when interacting with smart contracts
- Here is a tx interacting with the USDC contract to transfer $USDC to an address. You can see that the gas used here is
48,537
Gas units.
- Here is a tx interacting with the BAYC contract to transfer an NFT. This transfer uses
107,687
Gas units
- These transfers cost more Gas Units than ETH transfers because they are computationally more expensive for the EVM.
Heavy and Light Functions in Solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;
contract Sample {
uint256 myVar = 20;
bytes32 myBytes;
function lightFunction() external returns(uint256) {
myVar++;
return myVar;
}
function heavyFunction() external returns(bytes32) {
bytes32 x = keccak256(abi.encode(myVar));
for(uint256 i = 0; i < 100; i++) {
x = keccak256(abi.encode(x));
}
myBytes = x;
return x;
}
}
- The
lightFunction()
here incrementsmyVar
and theheavyFunction()
calculates the hash ofx
a hundred times.
- The gas used for the light function is
26658
units.
- Thye gas used for the
heavyFunction()
is67814
units.
- The gas used on the
heavyFunction
is significantly higher because the EVM needs to do a lot more work to calculate 100 different hashes compared to just incrementing a variable by 1.
Understanding Block Limit
- Bitcoin has historically limited it’s block size to 1MB.
- Ethereum does not have an explicit byte limit for the block size.
- Instead Ethereum limits the amount of computations per block, or gas.
- Each computation has a gas cost associated with it
- If a block has too many computations on it then it’ll be difficult for the nodes to verify the transactions quickly.
- In Ethereum, Each block has a target size of 15 million gas but the size of blocks will increase or decrease in accordance with network demands, up until the block limit of 30 million gas (2x target block size). The total amount of gas expended by all transactions in the block must be less than the block gas limit.
- At 30 million Gas block limit and 21,000 Gas required for ETH transaction, one block could theoretically hold
~1400
ETH transfers. This is the highest number of txs a single block can hold in ethereum.
- On the other end of this, a tx to Tornado Cash costs
~1 million
Gas. A single block can only hold 30 of these txs.
- A new block in added to the chain roughly every
15 seconds
- Ethereum roughly does 13 tx per second (TPS).
- If a a function requires over 30 million gas to execute, then the tx will get reverted because it can’t fit in a block.
Gas Efficient Chains - Write about gas in L2s, Avalanche and Solana
Storage Slots in Solidity
- All variables in EVM are stored in 32 byte slots.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract StorageSlots {
uint256 a = 10;
uint256 b = 20;
uint256 c = 30;
function getValueAtSlot(uint256 slot) external view returns(uint256 value) {
assembly {
value := sload(slot)
}
}
}
- Here, the value of
a
is stored in slot0
,b
in slot1
, andc
in slotc
- You can access the value stored in the slot from the Yul(assembly) code.
Opcodes
- EVM opcodes are low-level instructions that the EVM executes.
- The solidity code is first translated to these opcodes and those in turn are translated to binary
- High-Level Functions and Ambiguity: A simple function, such as adding one to a variable and returning it, may seem straightforward in high-level programming but is ambiguous for a computer. Computers require detailed instructions for execution.
- Memory and Storage in EVM: For a computer, specifically the EVM, to perform operations, it needs to know the exact location in memory or storage of the variables involved. For example, adding numbers involves moving the number from storage into memory.
- Specificity of Operations: Operations in EVM need to be very specific. For example, when adding two numbers, both numbers must be loaded into specific memory locations for the addition operation to find and process them.
- Assembly Code in Ethereum: When Solidity code is compiled for Ethereum, it is translated into assembly code. This code consists of simple, precise operations executed by the EVM.
- Understanding Smart Contract Operations: The actual operation of a smart contract involves loading numbers from storage, performing computations, and managing the data flow. For example, loading a value from a specific storage location into memory before an operation.
- Stack-Based Architecture: EVM uses a stack-based architecture for operations. Values are pushed onto the stack, where they are used in computations. The
PUSH
operation is used to place numbers on the stack.
- Addition and Stack Operations: In an addition operation, the EVM loads the numbers to be added onto the top of the stack. The
ADD
operation then pops these values, adds them, and pushes the result back onto the stack.
Opcodes Gas Cost
- Each opcode in EVM has a specific gas cost associated with it.
- You can find the gas cost here:
- The gas cost of a function is just the sum of gas costs for all the opcodes in that function.
Calling an Empty Function
- Calling an empty function still costs some gas because the EVM has to do some work to go the function selector of the empty function
Non-payable Functions
- Payable functions are slightly cheaper than non-payable functions because the non-payable function checks if you send Ether with the transaction and if you do it, reverts the function.
What is Gas Limit?
- The gas limit is the maximum amount of gas a sender is willing to consume for a transaction.
- If the account balance can't cover this cost, the transaction is invalid. Unused gas is refunded.
- The actual gas used may be less than the gas limit. If the gas limit set is too low and less than the gas needed, the transaction will fail as the EVM executes opcodes and reduces the available gas.
- The gas limit exists because Ethereum nodes can't predict the exact gas usage in advance. They need to execute the transaction to determine the actual gas consumed.
- All transactions require a minimum of 21,000 gas to cover the basic costs of transaction execution, including checks for transaction validity, signature verification, nonce correctness, and upfront payment.
Why 21,000 Gas?
- In Ethereum, transactions have intrinsic gas costs for data processing, contract creation, and basic computational work.
- The base cost for a transaction is 21,000 gas.
Solidity Optimizer
- Solidity provides an optimizer for your smart contract
- It optimizes for deployment cost and execution cost.
- If the number of runs are less, it optimizes for deployment cost but the execution cost may be higher.
- If the number of runs are higher, it optimizes for execution cost but the deployment cost may be higher
- How does the optimizer work?
- Simplification and Algebraic Laws: The optimizer applies various algebraic laws and simplification rules to the code. This includes removing dead code (code that doesn't affect the outcome), simplifying expressions, and reordering operations in a way that reduces the total number of computational steps.
- Constant Evaluation and Propagation: The optimizer evaluates expressions that can be computed at compile-time, replacing them with their computed values. This process is known as constant folding. It also propagates constants to reduce computation at runtime.
- Common Subexpression Elimination: If the same expression is computed multiple times, the optimizer will detect this and reuse the result of the first computation, reducing the total number of operations.
- Inlining: Functions that are small and called frequently might be "inlined" — their code is inserted at the place of each call, saving the gas needed for a function call.
- Loop Optimization: The optimizer analyzes loops to reduce unnecessary operations inside loops and, in some cases, unroll them for performance benefits.
- Optimization never changes the functional behaviour of the contract. Solidity's optimizer is designed to maintain the correctness and determinism of the contract.
Storage Overview
- Gas cost associated with storage is one of the most expensive.
1. setting a storage variable `0 to non-zero` → `20,000` gas units
2. setting a storage variable `non-zero to non-zero` → `5,000` gas units
3. setting storage from `non-zero to 0` → refund
4. Pay an additional 2,100 gas units if it's the first time accessing a variable in tx
5. Pay 100 gas units if the variable has already been touched
1
costs20,000
gas because the ethereum network has to index the variable and set it to a non-zero value
2
costs5,000
gas because the network has already indexed it and it only has to update the state of the variable.
3
refunds gas because the default of any variable is0
in ethereum. So nodes don’t have to store this value. To incentivise people to use less storage, the network refunds them.
contract StorageExample {
uint256 private myVar;
function setVarToOne() external {
// Costs 43,344 Gas = 21,000(tx cost) + 20,000(0 -> 1 cost)
// + 2,100 (accessing the storage for the first time in a tx)
// + 244 (cost of doing nothing)
myVar = 1;
}
function setVarToTwo() external { // Costs 26,266 Gas
// Costs 26,266 Gas = 21,000(tx cost) + 5,000(1 -> 2 cost incl cold
// storage access) + 266 (cost of doing nothing)
myVar = 2;
}
function setVarToZero() external { // Costs 21,400
//
myVar = 0;
}
function getAndSetVar() external {
// Costs 43,523 Gas = cost of (cold read + write) == cost of (write)
uint256 _myVar = myVar;
myVar = _myVar + 1;
}
0 -> non-zero
operations cost 21,000 gas because the opcode it requires is the that costs 20,000 gas units.
non-zero to non-zero
operations cost 5,000 gas because the opcodes it requires are that takes 2,100 gas that loads the variable from cold storage and that costs 2,900 gas that resets the value of the variable.
- All storage operations happens in a 32 byte slot. Even if you are using smaller variables like uint8 or uint16, it still costs the same to read and write to storage.
- You can’t save storage gas by using smaller integer sizes. Ethereum treats it all as 32 byte storage slots.
- Setting the variable to the same value will cost you
Costs ~23,500 Gas = 21,000(tx cost) + 2,100 (cold access) + 100 (warm access cost) + cost of doing nothing
contract StorageExample {
uint256 private myVar = 1;
// we are setting the variable to the same value it already has
function setVarToOne() external {
// Costs 23,570 Gas = 21,000(tx cost) + 2,100 (cold access)
// + 100 (warm access cost)
myVar = 1;
}
Gas Cost of Array Storage
- Adding 1 element to an array costs
~66,000
gas. This is because it takes21,000
gas for the tx,
22,100
gas for cold access + writing 0 → non-zero for storing the element
22,100
gas for storing the length of the array
- Adding 2 elements to an array costs
~88,000
gas.21,000
gas for the tx,
22,100
gas for cold access + writing 0 → non-zero for storing the element (0)
22,100
gas for cold access + writing 0 → non-zero for storing the element (1)
22,100
gas for storing the length of the array
- Setting an array that is
[2] to [2, 9]
21,000
gas for the tx,
22,100
gas for cold access + writing 0 → non-zero for storing (9)
2,200
gas for setting the value at slot 0 to the same number
5000
gas for storing the length of the array
contract ArrayExample {
uint256[] private myArray;
function setArray(uint256[] calldata _val) external {
myArray = _val;
}
function getArrayLength() external returns(uint256) {
return myArray.length;
}
function getSlotValue() external returns(uint256 val) {
assembly {
val := sload(myArray.slot)
}
}
}
Refunds on setting a storage variable to 0
- Setting a storage variable to 0 also costs
5,000
gas similar to setting a variable from non-zero to non-zero
- The opcode refunds
4,800
gas when a storage variable is set to 0 (Updated in the London Fork from15,000
to4,800
)
- But this only happens if the total gas cost of the function is more than
24,000
gas. - (updated in London fork from30,000
to24,000
)
- Setting one variable to 0 costs
~21,400
gas+21,000
gas for tx
+5,000
gas for rewriting a non-zero value
+200
- gas is cost of doing nothing
-4,800
gas as a refund
- Setting two variables to 0 costs
~21,600
gas+21,000
gas for tx
+5,000 * 2
gas for rewriting two non-zero value
-4,800 * 2
gas as a refund
- The more variables you set as
0
, the more it will cost because accessing the re-writing the variable is more expensive than the refund you get.
1. Setting to zero can cost between 200 to 5,000 gas, depending on
how much of a refund you're able to get.
2. Deleting an array or setting many values to zero can be surprisingly expensive.
3. Setting a value from non-zero to non-zero is the same as setting it from non-zero to zero if you do
4. For every o operation try spending 24,000 gas elsewhere to get a refund.
5. counting down is more efficient than counting up.
ERC20 Transfers
Sender balance | Receiver Balance | Gas Cost | Notes |
non-zero → zero | zero → non-zero | 46,686 | costly because of 0 → non zero tx and slightly cheaper because of non-zero → 0 gas refund |
non-zero → non-zero | zero → non-zero | 51,474 | expensive because of 0 → !0 tx |
non-zero → zero | non-zero → non-zero | 34,374 | cheaper cuz no 0 → !0 tx |
non-zero → non-zero | non-zero → zero | 29,586 | cheapest cuz !0 → !0 tx and a gas refund |
Events cost extra gas
Storage Cost for files
- Storage on Ethereum is super expensive. DO NOT STORE FILES ON ETHEREUM YOU MORON
- Store the hash of the file on ethereum so it can be verified easily
Structs and Strings
- The cost of a struct is simply the sum of the gas cost used by all it’s variable, whether it is !0 → 0 or cold access or 0 → !0.
contract StructExample {
struct MyStruct {
uint256 a;
address b;
}
MyStruct myStruct;
function setStruct() external {
// ~65,000 = 21,000(tx cost) + 2 * 22,100 (cold access + 0 -> !0)
myStruct = MyStruct({
a: 10,
b: msg.sender
});
}
function setStructV2() external {
// ~29,000 = 21,000(tx cost) + 22,000(setting variable to same cost)
// + 5000 (!0 -> !0 cost)
myStruct = MyStruct({
a: 20,
b: msg.sender
});
}
}
Storing Strings
- If the string is less than 32 bytes, the string will only take one storage slot. And the same math of 0 → !0 stuff applies here too.
- If the string is greater than 32 bytes, it’ll take multiple storage slots. The larger the string, the larger the storage cost.
Variable Packing
- This is when 2 or more variables are stored in a single 32 bit slot.
contract VariablePackingExample {
uint128 private a = 1;
uint128 private b = 2;
function getSlotA() external pure returns(uint256 value) {
assembly {
value := a.slot
}
}
function getSlotB() external pure returns(uint256 value) {
assembly {
value := b.slot
}
}
function loadSlot0() external view returns(bytes32 value) {
assembly {
value := sload(0)
}
}
function getOffsetA() external pure returns(uint256 value) {
assembly {
value := a.offset
}
}
function getOffsetB() external pure returns(uint256 value) {
assembly {
value := b.offset
}
}
}
- In this example both a and b are
uint128
. They are both stored in the same storage slot.
- Calling
getSlotA
andgetSlotB
will return both the variable’s slot as 0.
- That’s because they are both stored in the same slot.
- You get
0x0000000000000000000000000000000200000000000000000000000000000001
when callingloadSlot0
- You can see that both the numbers are stored in the slot
- Solidity also provides the offset that specify where a variable is located inside the storage slot.
- calling
getOffsetA
will return0
because it’s in the 0th position in the storage slot and callinggetOffsetB
will return16
because it’s in the 16th position in the storage slot.
- Most times its more gas efficient to use
uint256
because the EVM has to perform less work packing the variables.
- It makes sense to use smaller data types when both the variables are set in the same transaction.
Array Length
- To loop over a dynamic array, always store the length of the array to memory to save gas.
contract ArrayCache {
uint256 myArray[] = [1,2,3,4,5,6,7,8,9,10];
function getSum() external view returns(uint256 sum) {
// 49,119 gas units
for(uint256 i = 0; i < myArray.length; i++) {
sum += myArray[i];
}
}
function getSumOptimized() external view returns(uint256 sum) {
// 48,124 gas units
uint256 _length = myArray.length;
for(uint256 i = 0; i < _length; i++) {
sum += myArray[i];
}
}
}
- In
getSum()
there is a warm access gas cost associated with accessing the variable everytime in the loop.
- In a dynamic array, the slot0 value of the array contains the length of the array
- In a static array, it just stores the 0th element in slot0
Memory vs Calldata Costs
- Memory is usually more expensive than calldata because it needs to copy the data from the tx into memory.
- But calldata can’t be changed inside a function. It’s cheaper to use a memory if you want to mutate the variable.
contract Memory {
function doNothing(bytes memory _myBytes) external pure {
// 22,011 Gas
}
function doNothing(bytes calldata _myBytes) external pure {
// 21,865 Gas
}
function doNothing(bytes memory _myBytes) external pure returns(bytes memory) {
// 22.419
_myBytes[0] = 0xaa;
return _myBytes;
}
function doNothing(bytes calldata _myBytes) external pure returns(bytes memory) {
// 22,442
bytes memory _myLocalBytes = _myBytes;
_myLocalBytes[0] = 0xaa;
return _myLocalBytes;
}
}
Memory Explosion
- Memory is cheap on Ethereum. But after a certain point, the gas costs for memory increases quadratically and it costs a lot more for memory operations.
- Solidity never clears memory like garbage collection in Python, Javascript or manual memory deallocation in C/C++.
Solidity tricks
FunctionName
- When you call a function, the EVM checks if the function signature matches in ascending order of hex. So if you have a lot of functions, make sure the one that’s called a comes up first in hex. This saves very little gas
LessThan and GreaterThan
- The EVM does not have opcodes to checkLessThanEqual
andGreaterThanEqual
. So it used multiple opcodes to accomplish this.
Revert Early
- An ethereum tx will have to pay gas for all the work it does even if the tx reverts. So make sure to revert early to save gas.
Resources