Amsterdam hardfork support
Hardhat provides experimental support for the upcoming Amsterdam execution-layer hardfork. It lets you start testing your contracts against Amsterdam’s new behavior before it activates on Ethereum Mainnet.
Amsterdam is not enabled by default: osaka remains Hardhat’s default and latest stable hardfork. You have to opt in explicitly, see enabling Amsterdam.
We are implementing Amsterdam’s EIPs incrementally, and will add more as the hardfork is finalized in the run-up to launch.
Supported features
Section titled “Supported features”The following Amsterdam EIPs are currently supported:
- EIP-2780: Resource-based intrinsic transaction gas
- EIP-7708: ETH transfers emit logs
- EIP-7778: Block Gas Accounting without Refunds
- EIP-7843: SLOTNUM opcode
- EIP-7928: Block-Level Access Lists
- EIP-7976: Increase Calldata Floor Cost
- EIP-7981: Increase Access List Cost
- EIP-7997: Deterministic Factory Contract
- EIP-8024: Backward Compatible SWAPN, DUPN, EXCHANGE
- EIP-8037: State Creation Gas Cost Increase
- EIP-8246: Remove SELFDESTRUCT Burn
Enabling Amsterdam
Section titled “Enabling Amsterdam”Set hardfork to "amsterdam" on a simulated network with the l1 or generic chain type:
import { defineConfig } from "hardhat/config";
export default defineConfig({ networks: { edrAmsterdam: { type: "edr-simulated", chainType: "l1", hardfork: "amsterdam", }, },});You can then use the new network as the default in scripts and tests with the --network flag:
npx hardhat run scripts/example.ts --network edrAmsterdampnpm hardhat run scripts/example.ts --network edrAmsterdamyarn hardhat run scripts/example.ts --network edrAmsterdamOr you can name it explicitly when creating a network:
import { network } from "hardhat";
const connection = await network.create({ network: "edrAmsterdam" });EIP-2780: Resource-based intrinsic transaction gas
Section titled “EIP-2780: Resource-based intrinsic transaction gas”EIP-2780 replaces the flat 21,000 gas that every transaction pays up front with a charge per resource it uses. From Amsterdam onwards, a transaction pays a base cost of 12,000 gas, plus extra charges for accessing its recipient and for transferring value. For instance, a plain ETH transfer to an existing account still costs 21,000 gas in total, however a transfer to yourself costs only the base cost.
In practice, this may affect test suites that hardcode 21,000 as the cost of a transaction, for example when asserting on a receipt’s gasUsed.
EIP-7708: ETH transfers emit logs
Section titled “EIP-7708: ETH transfers emit logs”Under EIP-7708, ETH transfers emit a log with the same shape as an ERC-20 Transfer event:
- address: the system address
0xfffffffffffffffffffffffffffffffffffffffe - topics[0]:
keccak256("Transfer(address,address,uint256)") - topics[1]: the sender
- topics[2]: the recipient
- data: the amount in wei
This means plain ETH transfers — which previously produced no logs — now emit one. The following is a minimal script that sends 1 ETH and prints the resulting log using viem:
import { network } from "hardhat";
import { decodeEventLog, formatEther, getAddress, parseAbiItem, toEventSelector,} from "viem";
// EIP-7708: value-transferring operations emit a log from this system addressconst LOG_EMITTING_SYSTEM_ADDRESS = "0xfffffffffffffffffffffffffffffffffffffffe";
// topics[0] is keccak256("Transfer(address,address,uint256)")const transferEvent = parseAbiItem( "event Transfer(address indexed from, address indexed to, uint256 value)",);const TRANSFER_TOPIC = toEventSelector(transferEvent);
const { viem } = await network.create({ network: "edrAmsterdam" });
const [sender, recipient] = await viem.getWalletClients();const publicClient = await viem.getPublicClient();
const hash = await sender.sendTransaction({ to: recipient.account.address, value: 10n ** 18n, // 1 ETH});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
const transferLogs = receipt.logs.filter( (log) => getAddress(log.address) === getAddress(LOG_EMITTING_SYSTEM_ADDRESS) && log.topics[0] === TRANSFER_TOPIC,);
console.log("ETH transfer logs emitted by the system address:\n");for (const log of transferLogs) { const { args } = decodeEventLog({ abi: [transferEvent], data: log.data, topics: log.topics, });
console.log({ emittedBy: getAddress(log.address), from: args.from, to: args.to, value: `${formatEther(args.value)} ETH`, });}Running it prints a log emitted by the system address, with the from and to addresses derived from its topics and the transferred amount from its data:
npx hardhat run scripts/transfer-logs.tspnpm hardhat run scripts/transfer-logs.tsyarn hardhat run scripts/transfer-logs.tsEIP-7778: Block Gas Accounting without Refunds
Section titled “EIP-7778: Block Gas Accounting without Refunds”Under EIP-7778, gas refunds no longer reduce the gas counted towards the block gas limit. From Amsterdam onwards, a block’s gasUsed reflects the gross gas spent by its transactions, without subtracting refunds. This only affects block-level accounting; per-transaction costs are unchanged.
In practice, this may affect test suites that assert on a block’s gasUsed, which can now be higher than on earlier hardforks if there are transactions that trigger refunds.
EIP-8037 further changes how a block’s gasUsed is computed.
EIP-7843: SLOTNUM opcode
Section titled “EIP-7843: SLOTNUM opcode”EIP-7843 exposes the consensus-layer slot number to the execution layer, so contracts can read it without hardcoding slot-length calculations or checking it against a beacon root. Amsterdam blocks carry a new slotNumber header field, and the new SLOTNUM (0x4b) opcode returns the executing block’s slot number. In practice, most test suites won’t need the slot number.
Hardhat’s simulated network has no consensus layer, so it simulates the slot number. On a new chain it starts at 0, and it increments by one for every mined block, including the blocks that hardhat_mine fast-forwards through. When you fork, the forked block keeps its original slot number, and blocks mined on top of it continue from there.
The simulated value is deterministic, but it doesn’t advance the way a real network’s does. On Ethereum, slots advance at a fixed interval regardless of whether a validator proposes a block. Because some slots are empty, consecutive blocks often have non-consecutive slot numbers. Don’t rely on one slot per block outside of a simulated network.
slotNumber isn’t part of the standard block response. To read it, send a raw JSON-RPC request:
import { network } from "hardhat";
const { provider } = await network.create({ network: "edrAmsterdam" });
for (let i = 0; i < 3; i++) { await provider.request({ method: "hardhat_mine", params: [] });
const block = (await provider.request({ method: "eth_getBlockByNumber", params: ["latest", false], })) as { number: string; slotNumber: string };
console.log( `block ${BigInt(block.number)} has slot number ${BigInt(block.slotNumber)}`, );}Running it prints the slot number of each newly mined block:
npx hardhat run scripts/slot-number.tspnpm hardhat run scripts/slot-number.tsyarn hardhat run scripts/slot-number.tsEIP-7928: Block-Level Access Lists
Section titled “EIP-7928: Block-Level Access Lists”EIP-7928 introduces block-level access lists, which record every account and storage slot accessed while executing a block. Amsterdam blocks carry a new blockAccessListHash header field: the keccak256 hash of the RLP-encoded block-level access list.
Hardhat’s simulated network populates this field so that it is present on Amsterdam blocks, but its value is a placeholder and does not match the value a real network would produce. Within a chain the placeholder blockAccessListHash is unique per block, and a block with no state changes uses the empty-list hash keccak256(rlp([])) as the EIP specifies. Don’t assert on specific blockAccessListHash values.
EIP-7976: Increase Calldata Floor Cost
Section titled “EIP-7976: Increase Calldata Floor Cost”EIP-7976 raises the calldata gas floor: from Amsterdam onwards, every byte counts as 64 gas towards the calldata floor.
Transactions that carry a lot of data but which do little computation will cost more. A test suite that asserts on their current gas usage may fail. Prefer letting Hardhat estimate the gas over using a hardcoded value, which may now be too low.
EIP-7981: Increase Access List Cost
Section titled “EIP-7981: Increase Access List Cost”EIP-7981 raises the cost of access lists: from Amsterdam onwards, every byte of address and storage key data in an access list costs an extra 64 gas.
Transactions that carry a nonempty access list will cost more. A test suite that asserts on their current gas usage may fail. Prefer letting Hardhat estimate the gas over using a hardcoded value, which may now be too low.
EIP-7997: Deterministic Factory Contract
Section titled “EIP-7997: Deterministic Factory Contract”EIP-7997 adds a CREATE2 factory at 0x4e59b44847b379578588920cA78FbF26c0B4956C, this allows deterministic deployments at identical addresses across EVM chains. Deployment with CREATE2 means a contract’s address depends only on the factory, a salt, and the contract’s creation bytecode. This allows a contract to deploy to the same address on every chain that has the factory.
To deploy through the factory, send a transaction whose data is a 32-byte salt followed by the creation bytecode. The factory returns the new contract’s address, so calling it first tells you where the contract will be deployed. The following script deploys the Counter contract:
import hre, { network } from "hardhat";
import { concat } from "viem";
// EIP-7997: the deterministic factory addressconst FACTORY_ADDRESS = "0x4e59b44847b379578588920cA78FbF26c0B4956C";
const { viem } = await network.create({ network: "edrAmsterdam" });
const [deployer] = await viem.getWalletClients();const publicClient = await viem.getPublicClient();
const { bytecode } = await hre.artifacts.readArtifact("Counter");
// Any 32-byte value works as the saltconst salt = "0x0000000000000000000000000000000000000000000000000000000000000000";
// The factory expects the salt followed by the creation bytecodeconst data = concat([salt, bytecode]);
// Calling the factory first returns the address it will deploy toconst { data: address } = await publicClient.call({ to: FACTORY_ADDRESS, data,});
await deployer.sendTransaction({ to: FACTORY_ADDRESS, data });
console.log(`Counter deployed at ${address}`);Running it prints the address the factory deployed Counter to:
npx hardhat run scripts/deterministic-deployment.tspnpm hardhat run scripts/deterministic-deployment.tsyarn hardhat run scripts/deterministic-deployment.tsEIP-8024: Backward Compatible SWAPN, DUPN, EXCHANGE
Section titled “EIP-8024: Backward Compatible SWAPN, DUPN, EXCHANGE”EIP-8024 adds the DUPN, SWAPN and EXCHANGE opcodes, which reach stack items deeper than DUP16 and SWAP16 can.
Existing contracts and tests should not be affected. The benefit comes once Solidity compilers use these opcodes, which can make the code they generate use gas more efficiently.
EIP-8037: State Creation Gas Cost Increase
Section titled “EIP-8037: State Creation Gas Cost Increase”EIP-8037 raises the cost of operations that grow Ethereum’s state, and meters it separately from everything else. From Amsterdam onwards, gas has two dimensions:
- State gas: charged for creating state, such as new accounts, writes to empty storage slots, and deployed code
- Execution gas: charged for everything else
A transaction pays for both, so its receipt’s gasUsed is the total, just like before. Blocks count the two dimensions separately, and a block’s gasUsed reports whichever is larger. The next block’s base fee follows that value.
In practice, transactions that create state cost more on Amsterdam than on earlier hardforks, and a block’s gasUsed can be lower than the sum of its transactions’ gasUsed. Test suites that assert on gas costs, or on a block’s gasUsed, may need updating.
The following script increments a freshly deployed Counter, and prints the gas used by the transaction and by its block:
import { network } from "hardhat";
const { viem } = await network.create({ network: "edrAmsterdam" });
const publicClient = await viem.getPublicClient();const counter = await viem.deployContract("Counter");
// The first increment writes to an empty storage slot, which creates stateconst hash = await counter.write.inc();const receipt = await publicClient.waitForTransactionReceipt({ hash });const block = await publicClient.getBlock({ blockNumber: receipt.blockNumber });
console.log(`transaction gasUsed: ${receipt.gasUsed}`);console.log(`block gasUsed: ${block.gasUsed}`);Most of the increment’s cost is state gas, so the block reports just that, while the transaction’s gasUsed is the total of both dimensions:
npx hardhat run scripts/state-gas.tspnpm hardhat run scripts/state-gas.tsyarn hardhat run scripts/state-gas.tsTransaction gas limits
Section titled “Transaction gas limits”The EIP-7825 transaction gas cap, which you can change with the transactionGasCap network option, applies only to execution gas from Amsterdam onwards. A transaction’s gas can go above it to pay for state gas, up to a hard limit of 4_294_967_295 (232 − 1), which still applies when you disable the cap with transactionGasCap: false. The block gas limit also applies, so raise blockGasLimit to send more than 60_000_000 gas.
EIP-8246: Remove SELFDESTRUCT Burn
Section titled “EIP-8246: Remove SELFDESTRUCT Burn”EIP-8246 removes the remaining cases where SELFDESTRUCT burns ETH. Accounts marked for selfdestruction still have their code, storage, and nonce cleared at transaction finalization, but any remaining balance is preserved. In practice, most test suites will not be affected.