BlockMeta
BlockMeta runs a fixed list of contract calls at the start of every block. It holds an
ordered list of (target contract, selector, gas limit) entries, and the VM invokes
blockPrologue() once per block, which calls each entry in turn.
This is how SupraEVM performs scheduled work that no user transaction triggers. The
automation registry depends on it: once CoreFacet.monitorCycleEnd() is registered here,
the registry gets a chance each block to notice that an automation cycle has ended.
Genesis deploys BlockMeta with an empty schedule. Until monitorCycleEnd() is registered by a
foundation-wallet action, no automation cycle ends and registered tasks stay PENDING. Devnet has
it registered.
Registration is owner-gated, so BlockMeta is not a general-purpose scheduler for
application contracts. Users read it to understand what runs before their transactions and
to interpret the block-metadata pseudo-transaction in RPC responses. For scheduled
execution owned by an application, use the
automation registry.
BlockMeta is an ERC-1967 proxy over a UUPS implementation. Call the proxy, whose address
is under the contract registry key BlockMetadata; the address under the key
BlockMetadataImpl is the implementation behind it.
Finding it
Read the address under the key BlockMetadata in the
contract registry, the
0x1::evm_config::EvmContractsDetails resource.
Reading the schedule
The examples on this page use these shell variables. RPC is a JSON-RPC endpoint, not a host. The
values here are Devnet’s; its contract addresses are also listed in
Addresses on Devnet:
RPC=https://rpc-devnet.supra.com/rpc/v1/eth/wallet_integration
BLOCK_METADATA=0x2cd6f3c0f0ca46ea1616adf9e396ee99c24559df
AUTOMATION_REGISTRY=0xa75a2cd8e61992526ed2eeaf1e1ab3b9ec472400cast call "$BLOCK_METADATA" "getExecutions()(address[],bytes4[])" --rpc-url "$RPC"
cast call "$BLOCK_METADATA" "blockPrologueGasCap()(uint64)" --rpc-url "$RPC"
cast call "$BLOCK_METADATA" "totalGasAllocated()(uint64)" --rpc-url "$RPC"The gas limit allocated to one entry:
cast call "$BLOCK_METADATA" "getExecutionGasLimit(address,bytes4)(uint64)" \
"$AUTOMATION_REGISTRY" 0x1b87619c --rpc-url "$RPC"How execution works
blockPrologue() iterates the list in order and calls each target with
target.call{gas: gasLimit}(selector). The call carries no arguments: the calldata is the
bare four-byte selector, so a registered function must take no parameters.
A failing entry does not revert the prologue. Each call emits CallSucceeded or
CallFailed, and iteration continues either way. A registered hook that reverts is
therefore silent apart from its event, and the block proceeds.
Each entry is capped by its own gas limit, and the sum of all limits is capped by
blockPrologueGasCap, which genesis sets to 16,777,216. The enforced bound is 63/64 of the cap.
Registering a gas limit that would push the total past that bound reverts with
GasCapExceeded.
Entry packing
updateExecutionOrder replaces the whole list at once and takes entries already packed
into a uint256:
[ target: 160 bits | selector: 32 bits | gasLimit: 64 bits ]
bits 255..96 bits 95..64 bits 63..0Constructing one:
uint256 entry = (uint256(uint160(target)) << 96)
| (uint256(uint32(selector)) << 64)
| uint256(gasLimit);register takes the three components separately and packs them itself. updateExecutionOrder
is the only entry point that requires the packed form, and it exists to change execution
order, which register cannot do because it appends.
Who can call what
| Function | Caller | Effect |
|---|---|---|
blockPrologue() | VM signer | Runs every registered entry. Called once per block. |
register, deregister, deregisterAt, updateExecutionOrder, setBlockPrologueGasCap | Owner | Changes the schedule or the cap |
upgradeToAndCall, transferOwnership, renounceOwnership | Owner | Upgrade and ownership |
getExecutions, getTargetContracts, getSelectors, getExecutionAt, getExecutionIndex, getExecutionGasLimit, blockPrologueGasCap, totalGasAllocated | Anyone | Reads |
The table lists the functions this page documents. owner() is a public read, and initialize
can be called once, which genesis does.
blockPrologue() requires msg.sender to be the VM signer,
0x0000000000000000000000000000000053555000. No account can send a transaction from that
address, so the function is unreachable outside block production. See
The reserved range.
The owner is the foundation wallet, so changing the schedule requires a multisig proposal.
When calls fail
| Error | Condition |
|---|---|
CallerNotVmSigner() | blockPrologue() called by anything other than the VM signer |
InvalidSelector() | A zero selector passed to register or updateExecutionOrder |
InvalidGasLimit() | A zero gas limit passed to register or updateExecutionOrder |
InvalidGasCap() | setBlockPrologueGasCap with zero, or with a cap whose 63/64 bound is below the currently allocated total |
GasCapExceeded() | The new total allocated gas would exceed 63/64 of blockPrologueGasCap |
SelectorAlreadyRegistered() | The (target, selector) pair is already in the list |
SelectorNotRegistered() | deregister, getExecutionIndex or getExecutionGasLimit for a pair not in the list |
InvalidIndex() | deregisterAt or getExecutionAt with an index at or beyond the list length |
InvalidExecutionsLength() | updateExecutionOrder with an empty array |
AddressCannotBeZero() | A zero target address |
AddressCannotBeEOA() | A target address with no code |
OwnableUnauthorizedAccount(address) | An owner-gated function called by another account |
A hook that fails, by reverting or running out of its gas, produces no error here. It emits
CallFailed with the callee’s return data.
Events
| Event | Emitted when | Indexed |
|---|---|---|
SelectorRegistered(address,bytes4,uint64) | An entry is added | targetContract, selector, gasLimit |
SelectorDeregistered(address,bytes4,uint64) | An entry is removed | targetContract, selector |
ExecutionOrderUpdated(uint256[]) | The list is replaced | none |
CallSucceeded(address,bytes4) | A prologue call returned successfully | targetContract, selector |
CallFailed(address,bytes4,bytes) | A prologue call failed | targetContract, selector |
BlockPrologueGasCapUpdated(uint64) | The cap changes | cap |
ExecutionOrderUpdated carries the new packed order in its log data, where it decodes as a
uint256[]. It indexes nothing, so filter for it by the contract address and the event
signature.
CallSucceeded and CallFailed are emitted once per entry per block, so they appear in
every block that has a non-empty schedule. They are attributed to the block-metadata
pseudo-transaction; see
Automated Transactions over RPC.
Interfaces
Solidity
interface IBlockMeta {
error CallerNotVmSigner();
error InvalidIndex();
error InvalidSelector();
error SelectorAlreadyRegistered();
error SelectorNotRegistered();
error InvalidGasLimit();
error InvalidGasCap();
error GasCapExceeded();
error InvalidExecutionsLength();
event SelectorRegistered(address indexed targetContract, bytes4 indexed selector, uint64 indexed gasLimit);
event SelectorDeregistered(address indexed targetContract, bytes4 indexed selector, uint64 gasLimit);
event ExecutionOrderUpdated(uint256[] executionOrder);
event CallFailed(address indexed targetContract, bytes4 indexed selector, bytes returndata);
event CallSucceeded(address indexed targetContract, bytes4 indexed selector);
event BlockPrologueGasCapUpdated(uint64 indexed cap);
function register(address _targetContract, bytes4 _selector, uint64 _gasLimit) external;
function deregister(address _targetContract, bytes4 _selector) external;
function deregisterAt(uint256 _index) external;
function updateExecutionOrder(uint256[] calldata _executions) external;
function setBlockPrologueGasCap(uint64 _cap) external;
function getExecutions() external view returns (address[] memory targets, bytes4[] memory selectors);
function getTargetContracts() external view returns (address[] memory targetContracts);
function getSelectors(address _targetContract) external view returns (bytes4[] memory selectors);
function getExecutionAt(uint256 _index) external view returns (address target, bytes4 selector);
function getExecutionIndex(address _targetContract, bytes4 _selector) external view returns (uint256 index);
function getExecutionGasLimit(address _targetContract, bytes4 _selector) external view returns (uint64 gasLimit);
function blockPrologueGasCap() external view returns (uint64 cap);
function totalGasAllocated() external view returns (uint64 totalGas);
}Signatures and selectors
| Signature | Selector | Mutability |
|---|---|---|
getExecutions() | 0x71ad7ee9 | view |
getTargetContracts() | 0xd14ba0e5 | view |
getSelectors(address) | 0x330c64ae | view |
getExecutionAt(uint256) | 0xa74ee939 | view |
getExecutionIndex(address,bytes4) | 0x13d81956 | view |
getExecutionGasLimit(address,bytes4) | 0x1c7d2fc0 | view |
blockPrologueGasCap() | 0xbcf041d5 | view |
totalGasAllocated() | 0x4e2b3180 | view |
register(address,bytes4,uint64) | 0xc8fc310b | nonpayable |
deregister(address,bytes4) | 0x8c482631 | nonpayable |
deregisterAt(uint256) | 0x3af145f1 | nonpayable |
updateExecutionOrder(uint256[]) | 0x2fa0c422 | nonpayable |
setBlockPrologueGasCap(uint64) | 0x91924353 | nonpayable |
blockPrologue() | 0x7ded091b | nonpayable |
JSON ABI
Generate it with forge inspect IBlockMeta abi --json; see
Interfaces and ABIs. The fragments needed
to read the schedule:
[
{
"type": "function",
"name": "getExecutions",
"inputs": [],
"outputs": [
{ "name": "targets", "type": "address[]" },
{ "name": "selectors", "type": "bytes4[]" }
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getExecutionGasLimit",
"inputs": [
{ "name": "_targetContract", "type": "address" },
{ "name": "_selector", "type": "bytes4" }
],
"outputs": [{ "name": "gasLimit", "type": "uint64" }],
"stateMutability": "view"
}
]Related
- System Contracts — the chapter index and the ownership model.
- AutomationRegistry — whose
monitorCycleEnd()is registered here after genesis, and whose task capacity is bounded by the gas limit allocated to that entry. - Automated Transactions over RPC — how prologue execution appears as a block-metadata pseudo-transaction.