Access lists first appeared on my radar while inspecting various transactions on samczsun’s fantastic Ethereum Transaction Viewer website. I was reviewing other MEV bot activity and wondered why a particular bot was paying less gas for WETH transfers.
I was paying 8062 gas for a WETH transfer, and the other bot was paying 6062 gas. A 2000 gas reduction is a big deal, and it appeared that all of the bot’s transfers were similarly discounted.
Since a transfer is an action performed by an external contract, I immediately thought that some sort of call trickery was taking place. But after several hours of searching I had found nothing and gave up for a time.
I reviewed the mysterious discounted-WETH transaction later on Etherscan and noticed a new tab labeled “Access List”.
I clicked on it and noticed immediately that the WETH address (which you should know by heart by now) appeared there.
Could this be the reason for the discounted WETH transfer?
And just what the hell is an access list anyway?
Searching briefly lead me to a formal proposal called EIP-2930 which introduced a new transaction type (0x1) to support the inclusion of access lists. The EIP further describes what an access list does:
The
accessList
specifies a list of addresses and storage keys; these addresses and storage keys are added into theaccessed_addresses
andaccessed_storage_keys
global sets (introduced in EIP-2929). A gas cost is charged, though at a discount relative to the cost of accessing outside the list.
Further they state:
Mitigates contract breakage risks introduced by EIP-2929, as transactions could pre-specify and pre-pay for the accounts and storage slots that the transaction plans to access; as a result, in the actual execution, the SLOAD and EXT* opcodes would only cost 100 gas: low enough that it would not only prevent breakage due to that EIP but also “unstuck” any contracts that became stuck due to EIP 1884.
Simplifying, this means that a transaction with an access list will be charged extra gas for that inclusion, charged a reduced gas price for all addresses/storage keys listed, and charged an extra gas price for storage access at any addresses/storage keys not listed.
So the choice whether to include an access list boils down to how many addresses and storage slots your transaction will use, and how large the discount for accessing them is relative to the extra payment for including that list.
Building An Access List
As expected, building an access list can be done several ways. There’s a hard way that teaches us a lot, and an easy way that saves us time later. We’ll do both here.
The Hard Way
Let’s demonstrate how a node sees a transaction by launching a local fork with ganache in verbose mode, sending a transaction, and seeing what addresses and storage are accessed.
Launch Ganache with the -v option to place it in verbose mode:
(.venv) [devil@dev bots]$ ganache-cli --chain.vmErrorsOnRPCResponse true --wallet.totalAccounts 10 --hardfork istanbul --fork.url http://localhost:8545 --miner.blockGasLimit 12000000 --wallet.mnemonic brownie --server.port 6969 --chain.chainId 1 -v
ganache v7.6.0 (@ganache/cli: 0.7.0, @ganache/core: 0.7.0)
Starting RPC server
[...]
Now connect to it via Brownie (which will connect to the active fork if one is listening on the expected port):
(.venv) [devil@dev bots]$ brownie console --network mainnet-fork
Brownie v1.19.2 - Python development framework for Ethereum
BotsProject is the active project.
Attached to local RPC client listening at '127.0.0.1:6969'...
Brownie environment is ready.
>>> weth = Contract.from_explorer(
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
)
>>> weth.deposit({'from':accounts[0],'value':50*10**18})
Transaction sent: 0xe8abce708b93d9e32b09150d0f064222e8ba33e5c56371d0fe6f745e5cc8d3fe
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 6
WETH9.deposit confirmed Block: 16439454 Gas used: 43738 (0.36%)
<Transaction '0xe8abce708b93d9e32b09150d0f064222e8ba33e5c56371d0fe6f745e5cc8d3fe'>
Flipping back to the ganache logs, we see the following:
Saved snapshot #1
> eth_accounts: []
> eth_blockNumber: []
> eth_getCode: [
> "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
> "latest"
> ]
> eth_getStorageAt: [
> "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
> "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
> "latest"
> ]
> eth_getStorageAt: [
> "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
> "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7",
> "latest"
> ]
> eth_getCode: [
> "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
> "latest"
> ]
> eth_getBlockByNumber: [
> "latest",
> false
> ]
> eth_getTransactionCount: [
> "0x66aB6D9362d4F35596279692F0251Db635165871",
> "latest"
> ]
> eth_gasPrice: []
> eth_getBlockByNumber: [
> "latest",
> false
> ]
> eth_chainId: []
> eth_sendTransaction: [
> {
> "from": "0x66aB6D9362d4F35596279692F0251Db635165871",
> "value": "0x2b5e3af16b1880000",
> "nonce": "0x6",
> "gas": "0xb71b00",
> "data": "0xd0e30db0",
> "to": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
> "gasPrice": "0x0"
> }
> ]
Transaction: 0xe8abce708b93d9e32b09150d0f064222e8ba33e5c56371d0fe6f745e5cc8d3fe
Gas usage: 43738
Block number: 16439454
Block time: Wed Jan 18 2023 23:47:13 GMT-0800 (Pacific Standard Time)
There are a few lines above that show the accessed addresses:
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 (WETH)
And these storage locations:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7
If we wanted to format an access list per EIP-2930, it would look like this:
[
[
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
[
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
]
]
]
Where each element in the access list is a pair with the first item matching the address, and the second item a list of storage locations accessed.
Web3py accepts a slightly different format when added to a transaction dictionary:
'accessList': [
{
'address':'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
'storageKeys':
[
'0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc',
'0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7'
]
}
]
Unfortunately Brownie does not have a mechanism to add an access list to a transaction, so I cannot proceed further and provide detailed logs. It did serve to illustrate what storage access looks like at the EVM and how to format the access list, but is otherwise unpractical.
The Easy Way
Luckily for us, geth provides a built-in RPC method called eth_createAccessList that will allow us to generate an access list from a transaction dictionary.
I have provided a script below which will send an HTTP request to your real Ethereum node (you are running one, right?) that generates an access list and gas estimate.