EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8324: Priority Update Registry

A shared registry whose updates can only write its own storage, so any EVM block producer can order them ahead of the reads

Authors
Created2026-06-02
Discussion Linkhttps://ethereum-magicians.org/t/erc-8324-priority-update-registry-pur/28921
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1852PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationPart A: On-chain registry (normative)DefinitionsInterfaceWrite-scoping (the core guarantee)AuthorisationWritingReadingStorage layoutEIP-712 domainPart B: Block-producer behaviour (expected, not enforceable)RationaleOpen questions for discussionBackwards CompatibilityReference ImplementationSecurity ConsiderationsBlock producer is trusted for ordering and freshnessReactivity via the ERC-1271 pathFailing safe in the absence of an update is the target's responsibilityNo native replay protectionAuthorisation is the target's responsibilityCopyright

Abstract

This ERC specifies the Priority Update Registry (PUR): a single, shared contract that holds small pieces of state owned by other smart contracts. An owning contract authorises one or more off-chain updaters to write its state; on-chain, each contract reads back only its own state. The state is meant to be short-lived and fresh: a value tied to a single block (for example, the current price for a trading pair) that updaters refresh as often as needed while the next block is being built.

What makes this safe to build on is that a transaction sent to the registry can change only the registry's own storage and nothing else. A block producer (an L1 builder or L2 sequencer) can therefore see, from its destination address alone, that the transaction can affect nothing but the registry, and can place the update at the front of the block without trusting its sender or simulating the transaction. We call this property write-scoping. The primary motivating use case is prioritised-update systems such as propAMMs (actively-managed AMMs that refresh their on-chain quote each block).

The standard is in two parts: (A) the on-chain registry (interface, storage layout, authorisation, and the write-scoping guarantee), enforced by the EVM; and (B) the block-producer ordering behaviour that makes updates useful, which the EVM cannot enforce. PUR does not provide that ordering itself; whether an update lands first rests on (B) and on how the reading contract is written. PUR's contribution is narrower: it removes the need for block producers to trust updaters, and provides a common standard for collecting and analysing update data.

Motivation

Actively managed AMMs (propAMMs) exist to escape loss-versus-rebalancing (LVR), the biggest cost of a traditional AMM: when the off-chain market moves, arbitrageurs trade against the pool's stale quote before it can update. A propAMM refreshes its quote each block, but the refresh only helps if the block producer (builder/sequencer) places it ahead of the transactions that would exploit the stale quote. This requires block producers to distinguish transactions that warrant special treatment from ordinary ones. In a permissionless setting, that is only scalable when the producer can easily understand the effect of an ostensible update transaction without trusting its sender. This predictability is what PUR provides.

In practice a DeFi protocol is expected to use the registry in four steps:

  1. Authorise one or more updaters with addUpdater.
  2. For a given block, an updater publishes the latest off-chain data into one of the contract's lanes via updateState (e.g. the current price for a pair).
  3. The block producer places that update ahead of any transaction that reads the lane (Part B).
  4. When a user transaction calls the contract, the contract reads the fresh value with getState and acts on it (prices the swap, and so on).

Routing every update through one registry also creates a common record: authorisation and updates live on-chain in a uniform format, so updater behaviour can be tracked across all contracts at once instead of reverse-engineered per maker.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT", and "MAY" are to be interpreted as in RFC 2119 and RFC 8174.

This specification is split into two parts:

  • Part A — On-chain registry specifies the contract this ERC standardises. It is normative and EVM-enforced.
  • Part B — Block-producer behaviour is expected behaviour that cannot be enforced by the EVM or consensus. It defines what it means for a block producer to honour PUR updates; no producer can be forced to do so.

Part A: On-chain registry (normative)

This ERC specifies a contract, intended to be deployed once for chain-wide use and shared by every target and producer, like an account-abstraction EntryPoint or Permit2. A shared singleton is the point: it gives producers one address to recognise and one fixed storage layout to read, and it makes updates from unrelated targets batchable.

Because the contract itself is what this ERC standardises, Part A describes its behaviour in the indicative ("reverts", "enforces") and reserves the RFC-2119 keywords (MUST/SHOULD) for the targets and integrators that use it (below and in Security Considerations). Block-producer behaviour (Part B) is expected rather than mandated, and is referenced here only as a guide.

Definitions

  • Target: a contract that reads PUR state. A target reads its own current update during execution.
  • Updater: an address a target has authorised to publish updates on its behalf.
  • Lane: an independent update channel scoped to a target, identified by laneIndex. Different lanes land separately and carry their own timestamp.
  • Priority update: a per-block write to a lane, consisting of a 27-byte base value plus up to 254 additional 32-byte slots, carrying an updateTimestamp.
  • Block producer: any actor with ordering authority over an EVM block, such as an L1 block builder, an L2 sequencer, or equivalent.

Interface

The contract exposes:

interface IPriorityUpdateRegistry {
    // --- Updater management ---
    /// @notice Authorize `updater` to write state on behalf of msg.sender.
    function addUpdater(address updater) external;
    /// @notice Revoke `updater`'s authorization for msg.sender.
    function removeUpdater(address updater) external;
    /// @notice Whether `updater` may write state for `target`.
    function isUpdater(address target, address updater) external view returns (bool);

    // --- Writing updates ---
    /// @notice Direct write; msg.sender MUST be an authorized updater for `target`.
    function updateState(
        address target,
        uint256 laneIndex,
        uint32 updateTimestamp,
        uint256[] calldata slots
    ) external;

    /// @notice Relay a batch of signed updates. Anyone MAY relay.
    function batchUpdateStateWithSignature(SignedUpdate[] calldata updates) external;

    // --- Reading updates ---
    /// @notice Read the calling target's own current update for `laneIndex`.
    ///         Reverts if the stored timestamp is outside [minTimestamp, maxTimestamp].
    function getState(uint256 laneIndex, uint32 minTimestamp, uint32 maxTimestamp)
        external view returns (uint32 updateTimestamp, uint256[] memory slots);

    // --- EIP-712 ---
    function DOMAIN_SEPARATOR() external view returns (bytes32);
    function UPDATE_TYPEHASH() external view returns (bytes32);

    event UpdaterAdded(address indexed target, address indexed updater);
    event UpdaterRemoved(address indexed target, address indexed updater);
}

struct SignedUpdate {
    address target;
    address signer;     // NOT part of the signed payload
    uint256 laneIndex;
    uint32  updateTimestamp;
    uint256[] slots;
    bytes   signature;
}

Two immutable parameters are fixed at construction:

  • MAX_UPDATE_AGE: maximum seconds by which updateTimestamp MAY lag block.timestamp on a write.
  • MAX_UPDATE_LEAD_TIME: maximum seconds by which updateTimestamp MAY exceed block.timestamp on a write.

Write-scoping (the core guarantee)

No externally callable function on the contract performs a state-changing external interaction: there is no value-bearing CALL, DELEGATECALL, CREATE / CREATE2, or SELFDESTRUCT anywhere in its code. The only external interactions it makes are read-only: the ecrecover precompile and the ERC-1271 isValidSignature staticcall.

It follows that any transaction whose to is the registry mutates only the registry's own storage, and within it only the lane and updater slots reachable through the functions above. A block producer can bound the complete state effect of a priority-update transaction from its destination address alone.

Write-scoping does not imply commutativity (the property that an update has the same effect no matter where in the block it is placed). The ERC-1271 path (below) executes updater-controlled code via staticcall: it cannot write, but it can read arbitrary external state (oracles, other pools) and cause the update to revert conditionally. A priority update can therefore be reactive: whether it lands can depend on the rest of the block or chain state. The standard does not attempt to prevent this; see Security Considerations.

Authorisation

Each target manages its own updater set; authorisations are scoped to msg.sender. addUpdater / removeUpdater emit the corresponding event and are idempotent. The registry imposes no global ACL: a target authorises exactly the addresses it chooses.

Writing

All write paths enforce:

  • slots.length in [1, 255]; else revert (EmptySlots / TooManySlots).
  • slots[0] fits in 27 bytes (top 5 bytes zero); else revert (Slot0Exceeds27Bytes). The reserved top 5 bytes hold updateTimestamp (4) and the slot count (1).
  • block.timestamp - MAX_UPDATE_AGE <= updateTimestamp <= block.timestamp + MAX_UPDATE_LEAD_TIME (inclusive); else revert (InvalidUpdateTimestamp).
  • updateTimestamp >= the lane's currently stored timestamp; else revert (StaleUpdate). Equal-or-newer overwrites.

updateState reverts (NotAuthorized) unless isUpdater[target][msg.sender].

batchUpdateStateWithSignature validates each SignedUpdate over the EIP-712 digest of (target, laneIndex, updateTimestamp, slots) and reverts the whole call on the first failure. The signer field is supplied by the relayer and is not part of the signed payload (see Rationale):

  • signer == target: the signature is verified against target via ERC-1271, accepted only if isValidSignature returns 0x1626ba7e. No prior registration is needed: the target authorises by signing.
  • signer != target: accepted only if ecrecover(digest, signature) equals signer and isUpdater[target][signer] is true.

Reading

getState(laneIndex, minTimestamp, maxTimestamp) returns the update for (msg.sender, laneIndex) and reverts (StaleUpdate) if the stored timestamp is outside [minTimestamp, maxTimestamp] (inclusive). A never-written lane has stored timestamp 0. The returned slot count equals the count written. Reads are scoped to msg.sender. Targets SHOULD choose their freshness window per read (the write-side bounds are not a substitute) and MUST NOT depend on reading another target's lane.

Storage layout

The fixed layout is part of the standard: it lets a producer locate, verify, and insert updates without executing arbitrary code.

Updater authorisation (nested mapping at slot 0):

slot  = keccak256(abi.encode(updater, keccak256(abi.encode(target, 0))))
value = 1 if authorized else 0

Lane state (one contiguous range per (target, laneIndex)):

base    = keccak256(abi.encode(target, laneIndex))
slot[i] = base + i

Base word packs three fields:

[ updateTimestamp (32b) | numSlots (8b) | slot0 value (216b) ]
  bits 255..224           bits 223..216    bits 215..0

Slots 1..k store raw uint256 values.

EIP-712 domain

UPDATE_TYPEHASH = keccak256("UpdateState(address target,uint256 laneIndex,uint32 updateTimestamp,uint256[] slots)"), domain name "PrioUpdateRegistry", version "1".

Part B: Block-producer behaviour (expected, not enforceable)

Nothing in Part A forces a block producer to do anything. The ordering below cannot be enforced by the EVM or by consensus; a block producer is free to ignore priority updates entirely, to include a stale-but-valid one, or to choose when during construction to incorporate an update. The registry's role is to make this behaviour easy to provide and safe to accept (write-scoping is what lets a producer accept an update without trusting the updater), rather than to mandate it. The behaviour below is described for any EVM block producer (L1 builder, L2 sequencer, or equivalent); the standard assumes only EVM execution and an actor with ordering authority over a block, and different chains and sequencer designs may legitimately handle these ordering rights differently.

A block producer that chooses to honour PUR updates is expected to:

  1. Recognise updates by destination, and order them ahead of reads. Treat any transaction whose to is the registry as a priority update; the single registry address is the recognition rule, so no per-transaction tracing is needed. For each target read in the block, place the most recent valid update received for that target ahead of any transaction that reads that target's state.
  2. Drop unused updates. Not land a priority update in a block in which no transaction reads the updated state.

Rationale

  • One shared, write-scoped contract. Predictable effect without simulation, fixed cost, conflict-free composability/batching, bounded abuse surface, and a property a producer can verify from the destination address rather than per-updater trust. This is why a standard beats each app rolling its own.
  • signer excluded from the signed payload. It is claimed by the relayer and then checked: ECDSA recovery must match it, or it equals target and ERC-1271 decides. Hashing it would add nothing, since both paths already bind the signature to a concrete authority.
  • Lanes. A target often updates several independent quotes (e.g. per pair). Independent lanes land/expire/batch separately, so one stale lane does not force a revert on another.
  • Caller-chosen freshness window on reads. Staleness tolerance is application-specific; pushing the window to the reader keeps the registry policy-free while letting each target enforce its own bound.
  • Packed base word. Packing updateTimestamp, numSlots, and a 27-byte slot0 into one word makes the common single-slot update a single SSTORE and keeps the producer's write cost fixed and small.
  • Accepting reactivity rather than banning it. Forbidding the ERC-1271 path would buy commutativity but remove smart-wallet / multi-party authorisation and the conditional-revert behaviour some makers rely on. v0 keeps the flexible path and documents the limitation.

Open questions for discussion

These are raised for discussion on the linked thread and are not settled in v0:

  1. Commutativity for targets that want it. The updateState and ECDSA-signed batch paths are pure writes and are therefore already commutative; only the ERC-1271 (signer == target) path can be reactive. The simplest route to full commutativity is to restrict a lane to direct / ECDSA-signed writes. Should the standard formalise a commutative tier (e.g. a per-lane flag marking it ECDSA-only), or leave it to application choice?
  2. Telling simulators which updates a read transaction expects. A transaction that reads a lane may revert (or take a fallback branch) under simulation against pre-update state. Today a missing or stale update surfaces as a coarse StaleUpdate() revert from getState. Should the standard mandate a richer error surface (e.g. StaleUpdate(target, laneIndex)) so simulators, RPCs and aggregators can learn which update a transaction is waiting on, or leave it to integrators?
  3. A richer-than-1271 update interface. Two needs point past the current ERC-1271 path: multi-signer composition (several parties signing quotes into one lane) and partial writes (overwriting only part of a lane within a block). Is a purpose-built validation/update interface (an extended ERC-1271-style call with an explicit auxiliary payload and sub-slot overwrites) worth standardising?

Backwards Compatibility

No backwards-compatibility issues: a new contract plus a new producer ingress, no consensus changes. Existing bespoke top-of-block mechanisms keep working; this standard is opt-in and aims to converge fragmented formats over time. A target adopting PUR must accept that its updates are only honoured by producers implementing Part B; under a non-honouring producer a PUR-reading function behaves as if no update landed, and SHOULD be written to fail safe (see Security Considerations).

Reference Implementation

The PrioUpdateRegistry source is the normative definition of the contract this ERC specifies. The full repository, with tests and gas benchmarks, is at https://github.com/flashbots/priority-update-registry.

The canonical instance is deployed on Ethereum mainnet with MAX_UPDATE_AGE = 0, MAX_UPDATE_LEAD_TIME = 0 at 0xDa7AfEeD021EAFC1c1Af9C362dE477DaD0396B81. ExamplePropAmm.sol shows a propAMM reading per-pair pricing from a lane each block.

The registry has been live on Ethereum mainnet since May 2026 and is in production use: several actively-managed AMMs post priority updates through it, and multiple independent block builders recognise those updates and order them ahead of the transactions that read the affected pools.

Indicative gas (steady-state overwrite, k = additional slots, n = batch size):

MethodFormula
Direct updateState21000 + 9712 + k × 5212
Batched (EOA path)21000 + 916 + n × (17366 + k × 5235)
getState (warm)1524 + k × 269

Batching breaks even at ~2 updates and saves ~37% at 10.

Security Considerations

Block producer is trusted for ordering and freshness

The registry cannot distinguish on-chain between the freshest update and a stale-but-valid one. A buggy or adversarial producer can insert an older (within-window) update, delay when it incorporates an update (affecting how stale the reference is), or run parallel build candidates and pick the one most favourable to its own flow. The validity window bounds the worst case but does not remove producer discretion. Mitigations live outside this ERC (e.g. distributing construction across operators in TEEs, limiting dependence on updates); targets SHOULD size their read window conservatively.

Reactivity via the ERC-1271 path

Write-scoping guarantees an update writes only the registry. It does not guarantee the update is non-reactive: here, reactive means the update can behave differently depending on the rest of the block, landing or reverting based on other transactions or state, rather than being a fixed write that always has the same effect. The signer == target path runs the target's isValidSignature via staticcall, which can read arbitrary external state and revert conditionally. This is a deliberate v0 trade-off (it preserves smart-wallet and multi-party authorisation), but it means a producer's "insert top-of-block" can still be conditioned by the updater. Targets and integrators relying on commutativity MUST NOT assume it holds for the 1271 path.

Failing safe in the absence of an update is the target's responsibility

The registry does not mandate any behaviour from targets, including any form of fail-safe. A target is free to ignore the "no fresh update" case, but one that does so (reading a lane without constraining behaviour when the update is stale, reverted, or never written) re-opens the loss-versus-rebalancing (LVR) window the primitive exists to close. Targets that care SHOULD handle it, e.g. revert the dependent function (a "state lock", so a swap cannot execute against stale state), or fall back to a deliberately wide quote. State locks are a recommended pattern rather than a requirement of the standard.

No native replay protection

The contract provides no native replay protection. A SignedUpdate is not single-use: while updateTimestamp is >= the lane's stored timestamp and still inside the write window, any party may re-relay it. A replay reproduces the same state (so it cannot corrupt state) but can consume gas/write-quota attributable to the signer. Replay protection, where wanted, is left to the target/updater to implement, e.g. tight MAX_UPDATE_AGE / timestamp windows, a nonce or single-use marker carried in the slots, or revoking the signer via removeUpdater (which immediately stops further replays, for non-target signers).

Authorisation is the target's responsibility

The registry only checks isUpdater / ECDSA / ERC-1271. Compromise of an authorised updater key lets the attacker write any structurally valid value to that target's lanes, which the ordering then applies top-of-block. Targets SHOULD bound single-update damage (quote sanity bounds, multi-party / threshold updaters, emergency locks).

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth