Predeploys & Precompiles
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 chains0x4300...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]);