# Amsterdam hardfork support

Description: Reference documentation for Hardhat's experimental support for the upcoming Amsterdam hardfork

Note: This document was authored using MDX

  Source: https://github.com/NomicFoundation/hardhat-website/tree/main/src/content/docs/docs/reference/amsterdam-support.mdx

  Components used in this page:
    - <Run cmd="..."/>: Runs a command in the terminal with npm/pnpm/yarn.
    - :::caution: A warning callout block. Supports custom title `:::caution[Title]` and icon `:::caution{icon="name"}` syntax.

import Run from "@hh/Run.astro";

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.

:::caution

Amsterdam support is experimental and incomplete. The hardfork is still being specified and implemented. The EIPs it includes may change in backward-incompatible ways before launch.

:::

Amsterdam is not enabled by default: `osaka` remains Hardhat's default and latest stable hardfork. You have to opt in explicitly, see [enabling Amsterdam](#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

The following Amsterdam EIPs are currently supported:

- [EIP-2780: Resource-based intrinsic transaction gas](#eip-2780-resource-based-intrinsic-transaction-gas)
- [EIP-7708: ETH transfers emit logs](#eip-7708-eth-transfers-emit-logs)
- [EIP-7778: Block Gas Accounting without Refunds](#eip-7778-block-gas-accounting-without-refunds)
- [EIP-7843: SLOTNUM opcode](#eip-7843-slotnum-opcode)
- [EIP-7928: Block-Level Access Lists](#eip-7928-block-level-access-lists)
- [EIP-7954: Increase Maximum Contract Size](#eip-7954-increase-maximum-contract-size)
- [EIP-7976: Increase Calldata Floor Cost](#eip-7976-increase-calldata-floor-cost)
- [EIP-7981: Increase Access List Cost](#eip-7981-increase-access-list-cost)
- [EIP-7997: Deterministic Factory Contract](#eip-7997-deterministic-factory-contract)
- [EIP-8024: Backward Compatible SWAPN, DUPN, EXCHANGE](#eip-8024-backward-compatible-swapn-dupn-exchange)
- [EIP-8037: State Creation Gas Cost Increase](#eip-8037-state-creation-gas-cost-increase)
- [EIP-8246: Remove SELFDESTRUCT Burn](#eip-8246-remove-selfdestruct-burn)

## Enabling Amsterdam

Set `hardfork` to `"amsterdam"` on a simulated network with the `l1` or `generic` chain type:

```ts
// hardhat.config.ts
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:

<Run command="hardhat run scripts/example.ts --network edrAmsterdam" />

Or you can name it explicitly when creating a network:

```ts
import { network } from "hardhat";

const connection = await network.create({ network: "edrAmsterdam" });
```

## EIP-2780: Resource-based intrinsic transaction gas

[EIP-2780](https://eips.ethereum.org/EIPS/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

Under [EIP-7708](https://eips.ethereum.org/EIPS/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:

```ts
// scripts/transfer-logs.ts
import { network } from "hardhat";

import {
  decodeEventLog,
  formatEther,
  getAddress,
  parseAbiItem,
  toEventSelector,
} from "viem";

// EIP-7708: value-transferring operations emit a log from this system address
const 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:

<Run command="hardhat run scripts/transfer-logs.ts" />

## EIP-7778: Block Gas Accounting without Refunds

Under [EIP-7778](https://eips.ethereum.org/EIPS/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](#eip-8037-state-creation-gas-cost-increase) further changes how a block's `gasUsed` is computed.

## EIP-7843: SLOTNUM opcode

[EIP-7843](https://eips.ethereum.org/EIPS/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:

```ts
// scripts/slot-number.ts
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:

<Run command="hardhat run scripts/slot-number.ts" />

## EIP-7928: Block-Level Access Lists

[EIP-7928](https://eips.ethereum.org/EIPS/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-7954: Increase Maximum Contract Size

[EIP-7954](https://eips.ethereum.org/EIPS/eip-7954) raises the maximum size of deployed contract code from 24 KiB to 64 KiB, and the maximum size of initcode from 48 KiB to 128 KiB. The new limits apply to deployment transactions and to contracts created with `CREATE` and `CREATE2`.

From Amsterdam onwards, you can deploy larger contracts without enabling `allowUnlimitedContractSize`.

## EIP-7976: Increase Calldata Floor Cost

[EIP-7976](https://eips.ethereum.org/EIPS/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

[EIP-7981](https://eips.ethereum.org/EIPS/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

[EIP-7997](https://eips.ethereum.org/EIPS/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:

```ts
// scripts/deterministic-deployment.ts
import hre, { network } from "hardhat";

import { concat } from "viem";

// EIP-7997: the deterministic factory address
const 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 salt
const salt =
  "0x0000000000000000000000000000000000000000000000000000000000000000";

// The factory expects the salt followed by the creation bytecode
const data = concat([salt, bytecode]);

// Calling the factory first returns the address it will deploy to
const { 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:

<Run command="hardhat run scripts/deterministic-deployment.ts" />

## EIP-8024: Backward Compatible SWAPN, DUPN, EXCHANGE

[EIP-8024](https://eips.ethereum.org/EIPS/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

[EIP-8037](https://eips.ethereum.org/EIPS/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:

```ts
// scripts/state-gas.ts
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 state
const 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:

<Run command="hardhat run scripts/state-gas.ts" />

### Transaction gas limits

The [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825) transaction gas cap, which you can change with the `transactionGasCap` [network option](/docs/reference/configuration#simulated-network-options), 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` (2<sup>32</sup> − 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

[EIP-8246](https://eips.ethereum.org/EIPS/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.
