Skip to Content
Supra Layer 1SupraEVMSystem ContractsBlockMeta

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=0xa75a2cd8e61992526ed2eeaf1e1ab3b9ec472400
cast 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..0

Constructing 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

FunctionCallerEffect
blockPrologue()VM signerRuns every registered entry. Called once per block.
register, deregister, deregisterAt, updateExecutionOrder, setBlockPrologueGasCapOwnerChanges the schedule or the cap
upgradeToAndCall, transferOwnership, renounceOwnershipOwnerUpgrade and ownership
getExecutions, getTargetContracts, getSelectors, getExecutionAt, getExecutionIndex, getExecutionGasLimit, blockPrologueGasCap, totalGasAllocatedAnyoneReads

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

ErrorCondition
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

EventEmitted whenIndexed
SelectorRegistered(address,bytes4,uint64)An entry is addedtargetContract, selector, gasLimit
SelectorDeregistered(address,bytes4,uint64)An entry is removedtargetContract, selector
ExecutionOrderUpdated(uint256[])The list is replacednone
CallSucceeded(address,bytes4)A prologue call returned successfullytargetContract, selector
CallFailed(address,bytes4,bytes)A prologue call failedtargetContract, selector
BlockPrologueGasCapUpdated(uint64)The cap changescap

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

SignatureSelectorMutability
getExecutions()0x71ad7ee9view
getTargetContracts()0xd14ba0e5view
getSelectors(address)0x330c64aeview
getExecutionAt(uint256)0xa74ee939view
getExecutionIndex(address,bytes4)0x13d81956view
getExecutionGasLimit(address,bytes4)0x1c7d2fc0view
blockPrologueGasCap()0xbcf041d5view
totalGasAllocated()0x4e2b3180view
register(address,bytes4,uint64)0xc8fc310bnonpayable
deregister(address,bytes4)0x8c482631nonpayable
deregisterAt(uint256)0x3af145f1nonpayable
updateExecutionOrder(uint256[])0x2fa0c422nonpayable
setBlockPrologueGasCap(uint64)0x91924353nonpayable
blockPrologue()0x7ded091bnonpayable

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" } ]
  • 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.
Last updated on