Predeployed and precompiled contracts, or simply predeploys and precompiles, are a special class of contracts at predetermined addresses that come with Blast out-of-the-box without ever having been explicitly deployed by an EOA.

These contracts implement core functionalities such as the L2 side of bi-directional message passing between Blast and Ethereum, as well as the bridging logic built on top of it. They are also unique in that Blast’s node can interface directly with them to initiate transactions in response to messages received from Ethereum mainnet.

In this article, we will identify the differences between predeploys and precompiles, examine how precompiles can cause problems during local development and testing, and provide examples of workarounds for these issues.

Predeploys and precompiles are not unique to Blast. Most of Blast’s precompiles are common across all EVM chains, and most of its predeploys are common to all OP Stack chains.

Predeploys

Predeploys are smart contracts with bytecode deployed to predetermined addresses at genesis. You can identify a predeploy by its common addressing pattern:

  • 0x4200...0000 - Predeploys shared with other OP Stack chains
  • 0x4300...0000 - Predeploys unique to Blast

You can find a complete listing of Blast’s predeploys here.

Precompiles

Precompiles are accessed at predetermined addresses but are implemented directly within the execution client. As a result, there is no bytecode deployed to precompile addresses.

For example, when calls are made to Blast’s Yield contract at 0x0000...0100, Blast’s execution client (Blast Geth) uses an internal Go implementation for execution rather than any bytecode deployed at that address.

Local Development

The Problem

When running scripts or tests on a forked execution environment, the chain state—including the bytecode of existing contracts—is copied into the testing environment. Interacting with predeploys when forking Blast is generally not an issue since their bytecode is copied over like any other contract. However, because precompiles lack bytecode at their addresses, any attempts to interact with them will revert.

New Blast builders often encounter this issue during fork tests or when deploying contracts that configure or claim native ETH yield, as these operations interact with Blast’s Yield precompile.

Ran 1 test for test/Blast.t.sol:MyBlastTest
[FAIL. Reason: setup failed: EvmError: Revert] setUp() (gas: 0)
Traces:
  [45933] MyBlastTest::setUp()
    ├─ [13601] → new <unknown>@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    │   ├─ [10701] 0x4300000000000000000000000000000000000002::configureClaimableYield()
    │   │   ├─ [5690] 0xc0D3C0d3c0d3C0d3c0D3c0D3C0D3C0D3C3d30002::configureClaimableYield() [delegatecall]
    │   │   │   ├─ [0] 0x0000000000000000000000000000000000000100::configure(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, 2)
    │   │   │   │   └─ ← [Stop] 
    │   │   │   └─ ← [Revert] EvmError: Revert
    │   │   └─ ← [Revert] EvmError: Revert
    │   └─ ← [Revert] 0 bytes of code
    └─ ← [Revert] EvmError: Revert

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.35s (0.00ns CPU time)

Workaround

The current workaround for this issue is to deploy a mock version of the Yield predeploy to 0x0000000000000000000000000000000000000100 within the local environment. This YieldMock contract doesn’t need to fully implement the Yield precompile; it simply needs to satisfy the IYield interface so that calls to it do not revert.

The following is an example of a minimal YieldMock contract that satisfies the IYield interface.

interface IYield {
    function configure(address contractAddress, uint8 flags) external returns (uint256);
    function claim(address contractAddress, address recipientOfYield, uint256 desiredAmount) external returns (uint256);
    function getClaimableAmount(address contractAddress) external view returns (uint256);
    function getConfiguration(address contractAddress) external view returns (uint8);
}

contract YieldMock is IYield {
    
    mapping(address => uint8) public getConfiguration;

    function configure(address contractAddress, uint8 flags) external returns (uint256) {        
        return 0;
    }

    function claim(address, address, uint256) external pure returns (uint256) {
        return 0;
    }

    function getClaimableAmount(address) external pure returns (uint256) {
        return 0;
    }
}

Foundry

Using Foundry, you can deploy bytecode to an arbitrary address using the vm.etch cheatcode.

address constant YIELD_CONTRACT = 0x0000000000000000000000000000000000000100;

// 1. Deploy an instance of `YieldMock` as you would any other contract.
YieldMock yieldMock = new YieldMock();

// 2. Cache the bytecode that was just deployed.
bytes memory code = address(yieldMock).code;

// 3. Use `vm.etch` to set the bytecode at `YIELD_CONTRACT`
vm.etch(YIELD_CONTRACT, code);

This method works for both testing and running scripts against Blast. This issue occurs when using forge script, because Foundry attempts local and forked simulations before broadcasting any transactions.

The --skip-simulation flag can be used to opt out of forked simulations, but local simulation cannot be skipped.

When using vm.etch with forge script, ensure that you are not calling vm.etch in any code that will be broadcasted.

Hardhat

Builders using Hardhat can use the same approach with the Hardhat node’s hardhat_setCode RPC method. Just like vm.etch, this method allows us to set the bytecode at an arbitrary address.

const YIELD_CONTRACT = "0x0000000000000000000000000000000000000100";

// 1. Deploy an instance of `YieldMock` as you would any other contract.
const YieldMock = await hre.ethers.getContractFactory("YieldMock");
const yieldMock = await YieldMock.deploy();

// 2. Cache the bytecode that was just deployed.
const code = await hre.ethers.provider.getCode(await yieldMock.getAddress())

// 3. Use `hardhat_setCode` to set the bytecode at `YIELD_CONTRACT`
// YIELD_CONTRACT = 0x0000000000000000000000000000000000000100      
await hre.ethers.provider.send("hardhat_setCode", [YIELD_CONTRACT, code]);