<h1>📚Calldata Gas Optimization📚</h1>
Calldata Gas Optimization is a rarely known paradigm, as the resource is quite limited. This could be because, in general cases, the amount of gas which could be saved is often in-significant. However, this accounts for a great proportion of total gas saved if it is corectly used in a certain context (e.g. in some L2 chains whose gas are calculated differently from ethereum L1 chain)
This article is mainly aimed at the competent smart contract developer as a guide to grokkking how the calldata optimization works in pracice. In other words, this guide could be a good starting point if you want to know how to balance tradeoffs and make a gas optimization decisions design.
We will explain the concept at the high level, and provide some implication as examples. Then, we will discuss the chain-specific techniques when they are tailored to different L2s.
# Prerequisites
The reader should be familiar with Solidity and Ethereum Virtual Environment (EVM).
Please check out [Rareskills' Solidity Bootcamp](https://www.rareskills.io/solidity-bootcamp) to learn more.
The reader should at least know some simple gas optimization techniques.
This is a good starting point to learn about these [techniques](https://www.rareskills.io/post/gas-optimization).
The reader should know what ABI encoding/decoding.
This [videos](https://youtu.be/upVloLUw5Z0?si=Dc7IKc5sffOMbvHI) is a good starting point to learn.
# Authorship
This article was written by Rati Montreewat ( [Linkedin](https://www.linkedin.com/in/rati-montreewat/), [Twitter](https://twitter.com/RATi_MOn) ), a blockchain engineer and an author of [Solid Grinder](https://github.com/Ratimon/solid-grinder).
# How it works
It is trivial that the gas fee is calulated using this formula: `Gas Limit * Gas Price per unit`, but it is not trivial when looking how `Gas limit` is calculated.
Certainly, the answer can be found in the **Ethereum Yellow white paper**:


Let break it down and look into only parts that are relevant to calldata bytes that are sent with the transaction:
1. **Gtxdatazero** - 4 gas for every zero byte of calldata
2. **Gtxdatanonzero** - 16 gas for every non-zero byte of calldata.
Noteworthly, these opeations are one of the cheapest Ethereum operations, so the calldata optimization is usually marked as unworthy, due to possible unneccesary complexity and un-readablillity.
> **Note**💡
Regarding the biggest opeation cost, you can check out Rareskills 's `9 Biggest Gas Guzzlers in Solidity on Ethereum` [here](https://www.rareskills.io/post/9-biggest-gas-guzzlers-in-solidity-on-ethereum).
Nonetheless, this is not true when it comes to gas calculation on L2s. L2 transactions' gas consumed will not be the same as L1 during contract execution, because calldata is the most expensive operations. The reason is that the L2 chain outsources the transation execution outside the L1 and submit the batch of those compressed L2 transaction back to L1.
Mathematically, the total layer2 transaction' gas is defined as :
$\
\text{Total Transaction Gas }_{\text{Layer2}} = \text{Execution Fee}_{\text{Layer2}} + \text{Data / Security Fee}_{\text{Layer1}}$
This figure is quite impactful when implementing on dApp deployed on L2 where users pay their significant portion of L1 data/security cost of batch submission. The L1 gas could possibly be from 90% to 99% of the total gas cost (L1 + L2 gas). It is noted that these figures heavily depend on the congestion on L1.
Why is this is important? Suppose that your logics in dapp contain loop, the dApp deployed on L2 could have more room regarding gas limit when executing such logics. This will revamp dApp design space and push forward the edge of scalability and feasibillity even more.
# Different Rules for Different L2 Chains
Although it is true that most of the gas spent on L2s comes from data/security part, the same set of smart contracts on different L2s could produce different gas optimization results. This is because different L2 chains (like Arbitrum/ Optimism/ Starknet and etc) use different rules and formulas.
Moreover, these rules will have been evolving, as the client & ethereum ecosystem matures overtime. By way of illustration, EIP4844 (aka [Proto-Danksharding](https://www.eip4844.com/) ) will make gas L2 data/security component even cheaper and the L2 execution part more significant, resulting in possible changes in how L2 execution fee will be calculated in order to reflect appropriate incentive & economic model.
I have gathered some of how different L2s' transaction gas are calculated:
### Arbitrum
$\
\text{Total Gas Fee}_{\text{Layer2}} = \text{Execution Fee}_{\text{Layer2}} + \text{Data Fee}_{\text{Layer1}}$
where $\ \text{Execution Fee}_{\text{Layer2}}$ is :
$\
\text{Execution Fee}_{\text{Layer2}} = \text{Gas Price}_{\text{Layer2}} \times \text{ Gas Used}_{\text{Layer2}}$
$\
text{Gas Price}_{\text{Layer2}} = \max( \text{Base Fee}_{\text{Layer2}} + \text{Priority Fee}_{\text{Layer2}} , \text{Gas Price Floor} )$
$\
\text{where} \quad \text{Gas Price Floor} =
\begin{cases}
0.1 \, \text{Gwei}, & \text{on Arbitrum One} \\
0.01 \, \text{Gwei}, & \text{on Nova}
\end{cases}$
and $\ \text{Data Fee}_{\text{Layer1}} $ is :
$\
\text{Data Fee}_{\text{Layer1}} = \text{Gas Price}_{\text{Layer1}} \times \text{brotli-zero-algorithm}_{\text{txdata}}(txdata) \times 16$
$\
\text{where } \text{brotli-zero-algorithm}_{\text{txdata}}(txdata)$ is used to reward users for posting compressible transactions.
## Optimism
Here's the (simple) math:
$\
\text{Total Gas Fee}_{\text{Layer2}} = \text{Execution Fee}_{\text{Layer2}} + \text{Data Fee}_{\text{Layer1}}$
where $\
\text{Execution Fee}_{\text{Layer2}}$ is :
$\
\text{Execution Fee}_{\text{Layer2}} = \text{Gas Price}_{\text{Layer2}} \times \text{ Gas Used}_{\text{Layer2}}$
$\
\text{Gas Price}_{\text{Layer2}} = \text{Base Fee}_{\text{Layer2}} + \text{Priority Fee}_{\text{Layer2}}$
and $\text{Data Fee}_{\text{Layer1}}$ is :
$\
\text{Data Fee}_{\text{Layer1}} = \text{Gas Price}_{\text{Layer1}} \times \left( \text{Tx Data Gas + Fixed Overhead}\right) \times \text{Dynamic Overhead}$
where $\ \text{Tx Data Gas}$ is :
$\
\text{Tx Data Gas} = \text{count-zero-bytes}_{\text{txdata}}(txdata) \times 4 + \text{count-non-zero-bytes}_{\text{txdata}}(txdata) \times 16$
# Methods of Optimizing Calldata
The key factor to determine the amount of gas required for the calldata component is the calldata size, and this is specified by the **ABI encoding** rule. In particular, the **ABI** ,according to [Solidity's Official Documentation](https://docs.soliditylang.org/en/latest/abi-spec.html), stands for ** Application Binary Interface**. It is the standard way to interact with contracts in the EVM, both from outside the blockchain and for contract-to-contract interaction. Data is encoded according to its type.
This can be illustrated by following commands.
First, let intall **cast**, a toolkit to interact with EVM, and we use **Foundryup** as a toolchain installer:
```sh
curl -L https://foundry.paradigm.xyz | bash
```
```sh
foundryup
```
Then we use the following **cast** command shows how solidity encodes the function with arguments:
```sh
cast calldata "addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)" 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD 1200000000000000000000 2500000000000000000000 1000000000000000000000 2000000000000000000000 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD 100
```
The result has the total bytes count of 520 hexa = 520/2 = 260 bytes:
```sh
0xe8e33700000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000000000000000000000410d586a20a4c000000000000000000000000000000000000000000000000000878678326eac90000000000000000000000000000000000000000000000000003635c9adc5dea0000000000000000000000000000000000000000000000000006c6b935b8bbd400000000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000000000000000000000000000000000000064
```
As you can see, the first 4 bytes of the calldata are the first four bytes of the **Keccak256** hash of the function signature **(addLiquidity(address,..))** . After the function selector, the following chunks of 32 bytes are the function arguments. If the argument is shorter than 32 bytes, it is, by default, “left padded” with extra zeroes to fit inside the 32 byte.
To illustrate, the chunks of calldata can be spilted as following:
- 0xe8e33700 as **function selector**
- 000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead as **address** of 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD
- 000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead as **address** of 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD
- 0000000000000000000000000000000000000000000000410d586a20a4c00000 as **uint256** of 1200000000000000000000
- 0000000000000000000000000000000000000000000000878678326eac900000 as **uint256** of 2500000000000000000000
- 00000000000000000000000000000000000000000000003635c9adc5dea00000 as **uint256** of 1000000000000000000000
- 00000000000000000000000000000000000000000000006c6b935b8bbd400000 as **uint256** of 2000000000000000000000
- 000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead as **address** of 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD
- 0000000000000000000000000000000000000000000000000000000000000064 as **uint256** of 64
There are a bunch of techniques to reduce total bytes of calldata, without losing the information. The concept is to try to encode calldata in a compact way in order to use as little bytes of calldata as possible. Then, the encoded data is later decoded into usable format.
On L2, we ,when doing gas optimization, do not concern about the gas costs of computation/execution , as the resulting benefit of gas cost when encoding saved clearly outweights the gas cost spent. This is because data/security gas costs is significantly greater than computation/execution gas costs.
However, most of tricks mentioned here are application-specific. The amount of bytes saved heavily depend on specific business logic in the smart contract.
## Bypassing more-often-used used bytes
If we know the exact value of either function signature or the function parameter, we can hard-code them as constant and ther will be use them later when required. This means that the calldata sent when calling contract could be saved.
We can design the set of smart contract using factory pattern such that the factory deploys a unique contract with every combination of methods and parameters. Some gas optimization examples are provided as following:
1. **function signature**
We can save 4 bytes of calldata by using only `fallback` function in the contract:
```ts
fallback() external payable {
// business logic
}
```
2. **function parameter**
We can save 32 bytes of calldata by removing one parameter from the function
For instance, the **address** of `ERC20` contract can be hardcoded as constant and could removed from the function. This can possibly save total of 20 non-zero bytes (same as **address** size ) and 12 zero bytes ( padded bytes to full 32 bytes).
```ts
address public constant USDC = <address>;
function TEST() external {
// business logic using USDC
}
```
If your are curious and would like to explore more about the implementation in practice you can checkout the following projects with interesting designs:
- [Scopelift's calldata-optimized router](https://scopelift.co/blog/calldata-optimizooooors)
- [op-kompressor's ERC4337](https://github.com/clabby/op-kompressor)
## Address Table
Another smart contract(aka **AddressTable**) can be deployed to store the mapping between addresses and indexes. This allows that the address can be registered to the contract, then the index is generated. Then, the generated id can then be used to look up the registered address during the compressed call data decoding process.
This design is adopted and implemented by **Arbitrum**. The interface is as following:
```ts
interface ArbAddressTable {
/**
* @notice Check whether an address exists in the address table
* @param addr address to check for presence in table
* @return true if address is in table
*/
function addressExists(address addr) external view returns (bool);
/**
* @notice compress an address and return the result
* @param addr address to compress
* @return compressed address bytes
*/
function compress(address addr) external returns (bytes memory);
/**
* @notice read a compressed address from a bytes buffer
* @param buf bytes buffer containing an address
* @param offset offset of target address
* @return resulting address and updated offset into the buffer (revert if buffer is too short)
*/
function decompress(bytes calldata buf, uint256 offset)
external
view
returns (address, uint256);
/**
* @param addr address to lookup
* @return index of an address in the address table (revert if address isn't in the table)
*/
function lookup(address addr) external view returns (uint256);
/**
* @param index index to lookup address
* @return address at a given index in address table (revert if index is beyond end of table)
*/
function lookupIndex(uint256 index) external view returns (address);
/**
* @notice Register an address in the address table
* @param addr address to register
* @return index of the address (existing index, or newly created index if not already registered)
*/
function register(address addr) external returns (uint256);
/**
* @return size of address table (= first unused index)
*/
function size() external view returns (uint256);
}
```
However, the implemantation is a precompile contract which is written in **Go**. You can check **OffchainLabs**'s [git repository](https://github.com/OffchainLabs/nitro/blob/v2.0.14/precompiles/ArbAddressTable.go) here. It is intended to be a single universal address table where anyone can register and use it.
If you want to see another implementation written in **Solidity**, together with its application. This **Solid Grinder**'s [git repository](https://github.com/Ratimon/solid-grinder) contains modified version of [UniswapV2](https://github.com/Ratimon/solid-grinder/blob/main/contracts/examples/uniswapv2/UniswapV2Router02_Optimized.sol), which adopt its own [address table](https://github.com/Ratimon/solid-grinder/blob/main/contracts/AddressTable.sol).
## Data Serialization
**Data Serialization** works by serializing and deserializing parameters into the correct type with adequate data size.
For example, if we choose to reduce the calldata by sending the time period as arguments with type of **uint40** (5 bytes) instead of **uint256**, the calldata should be sliced at the correct offset and the result (after zero bytes removed) can be correctly used in the next steps.
Let look at the implementation by **Solid Grinder** again [here](https://github.com/Ratimon/solid-grinder/blob/main/contracts/examples/uniswapv2/decoder/UniswapV2Router02_Decoder.g.sol). This contract is a good starting point:

This decoder function is application-specific to **Uniswapv2** and it is genrated with **Solid Grinder** 's CLI by looking at original unoptimized function. In this case, it is **UniswapV2Router02**. Basically, you can experiment and follow the detailed steps here at [Quick Start](https://github.com/Ratimon/solid-grinder/tree/main#quickstart).
# Tradeoffs
The clearest tradoffs of above calldata gas optimization tricks are readabillity and complexity. For example,
Adding encode and decode logics into smart contract and explicitly removing function's parameters will not only confuse users who directly interact with the contract via **Etherscan**, but will also make harder job for the developer who want to build on-top of your modified smart contract, reducing composabillity which is the unique stregth of permissionless world.
# Final thought
Aforementioned, calldata gas optimization is a new topic, but will be becoming more relativly relevant as layer 2/rollup technology are becoming more mainstream. Moveover, there is still no clear standard and practice. This article just provides and proposes possible design decisions & approaches. There is more room to reinvent this paradigm.
# Reference
https://docs.arbitrum.io/arbos/l1-pricing#l1-fee-collection
https://docs.arbitrum.io/stylus/reference/opcode-hostio-pricing#opcode-costs
https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/address-table
https://community.optimism.io/docs/developers/build/transaction-fees/
https://scopelift.co/blog/calldata-optimizooooors
https://github.com/clabby/op-kompressor
https://github.com/Ratimon/solid-grinder