Abstract
This EIP introduces a block-scoped reference counter, maintained by the execution client outside of world state, that records how many message calls have been made into each enrolled account during the current block. An account enrolls itself by calling a system registry contract, and can leave only with at least a full window's notice. After activation, every message call whose target is an enrolled account is charged, in addition to all existing costs, a surcharge of
surcharge(k) = min(G_REF * k * k, G_REF_MAX) for k > K_FREE
surcharge(k) = 0 for k <= K_FREEwhere k is the ordinal of that call among all calls into that account in the block, counted across every transaction and every caller. A second counter, per originator and per window of blocks, prices the same sender returning to the same account; a call pays the larger of the two. The surcharge is accounted separately from other gas. Nothing of it is burned and none of it reaches the block producer. At the end of each transaction its value in wei is split in two: one half is credited to a canonical no-owner sink that can only stake, the other half to a beneficiary the enrolled account named at enrollment, which must itself be a stake vault. All of it ends up as validator stake.
The intent is to make repetition of references to a consenting contract within a block superlinearly expensive while leaving single references unchanged. The first K_FREE references to an account in a block pay nothing, so a transfer or a swap in a quiet block costs what it costs today. A crew filling from a dozen wallets in one block, a route that crosses the same token many times, or a wallet that returns to it dozens of times in a day pays quadratically in gas that the actor cannot recover.
Motivation
The problem this prices
Sequencer-ordered chains with private submission are immune to the mempool sandwich. A 2026 study of six chains over three years (arXiv:2609.28115) finds zero sandwiches against protected order flow on Arbitrum and Monad. The same chains are not immune to a second class of extraction that needs no mempool at all. On Robinhood Chain on 2026-09-26 a token was farmed within thirty-two minutes of its launch by:
- fifty-nine permissionless pools initialized on the token, most with fee tiers between 70% and 98%, so that any route through them loses most of its input to fees and reports it as slippage;
- a price ladder walked to seventeen times the reference price on a sibling pool with no trades, by initializing pool after pool at rising quotes with no tokens deposited;
- liquidity added and removed inside the same block around each fill;
- a fixed toll taken on every fill, a machine constant rather than a market outcome;
- one helper contract at the same address on eight chains, and one operator that touched 532 tokens in under a month with a net position per token of approximately zero.
The victim of that machine is not one trader; it is everyone routed through the surface the machine built, and it runs against every launch on the chain regardless of who launched it. Encrypted mempools, private relays, batch auctions and threshold decryption do not address it, because the machine is not reacting to any transaction. It is reacting to the existence of the token.
None of this is an argument about memecoins. A token launch is a market; the technology is neutral and this EIP does not judge what is launched. What it prices is a practice: running the same extraction machine against every launch on a chain, whoever launched it, because the chain makes repetition free. This EIP does not price every step of that machine. Initializing a pool never calls the token, so the first two items above are invisible to a counter of calls into the token and are out of scope (see What this does not price). What it prices is the part that pays the machine, the fills and the return visits, each of which is the same primitive: reference the same token again.
Why price it in the protocol
The token can price only part of it, and at a cost. A token can count its own transfers, and one deployed form does (see Token-level form). But a counter in contract storage rolls back when the call reverts, so probing is free; it costs every caller a storage write; and the token can charge only in itself, which breaks every venue that checks exact balances. Neither the token nor this EIP sees pool initialization, which makes no call into the token, or a sequence that a flash-accounting venue (the Uniswap v4 pattern) nets into one settlement.
One AMM cannot price it. An AMM that charges liquidity operations the same as swaps and escalates repeated references defends only its own pools. The machine initializes its own pools, on the same singleton or on a fresh deployment of it, with a hook of its own choosing.
Ordering cannot price it. Fair ordering, batch auctions and single-sequencer FCFS constrain the position of a transaction relative to others. Repeated fills and return visits are not order-sensitive; they are count-sensitive.
The only layer that observes every reference to an account, across all callers and all transactions in a block, is the execution client. It already keeps per-transaction access sets for EIP-2929. This EIP asks it to keep a per-block counter for accounts that ask for one.
Why gas
The surcharge could have been specified as a fee in the referenced token. It is specified as gas for three reasons. A fee in the token is captured by whoever controls the venue, which is the party being priced. Gas is paid to the protocol and cannot be recaptured by the actor. And gas is the only unit in which the client can charge a message call without knowing anything about the callee's semantics. Because it is gas, it is ETH, and ETH can be staked: the surcharge is not burned, it is turned into validator stake that nobody can withdraw.
Why self-enrollment
A universal surcharge would price organic volume on every popular contract and would be a hard fork of the gas schedule for all existing code. Enrollment restricts the rule to accounts that have chosen it, makes the choice visible on chain, and leaves every unenrolled contract's gas exactly as it was. Leaving takes effect only after at least one full window, announced on chain when it is requested, so that a contract cannot enroll for a launch and quietly sell the surface back.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Constants
| Name | Value | Description |
|---|---|---|
FORK_BLOCK | TBD | first block at which this EIP is active |
REGISTRY_ADDRESS | 0x000000000000000000000000000000000000E5CA | placeholder; assigned at deployment |
SINK_ADDRESS | 0x000000000000000000000000000000000000E5CB | placeholder; the canonical StakeVault whose withdrawal address is itself |
STAKE_VAULT_CODEHASH | TBD | keccak256 of the canonical StakeVault runtime code |
DEPOSIT_CONTRACT | 0x00000000219ab540356cBB839Cbe05303d7705Fa | beacon chain deposit contract |
G_REF | 2000 | base surcharge, in gas |
G_REF_MAX | 5000000 | surcharge cap per call, in gas |
K_FREE | 2 | references per block per enrolled account that carry no fast surcharge |
W_SLOW | 50400 | length, in blocks, of the slow ratchet's window (a week at 12 s) |
G_REF_SLOW | 400 | base surcharge of the slow ratchet, in gas |
G_REF_SLOW_MAX | 5000000 | slow surcharge cap per call, in gas |
K_FREE_SLOW | 16 | references per window per originator per enrolled account that carry no slow surcharge |
BOND | 1 ether | operator bond per validator in a stake vault, held inside the validator |
TOP_UP | 31 ether | what a stake vault adds to a proven validator |
MAX_PROOF_AGE | 3600 | seconds a validator proof stays acceptable |
Chains other than Ethereum mainnet MAY adopt different values. For a single-sequencer rollup with sub-second blocks the RECOMMENDED values are G_REF = 20000, G_REF_MAX = 30000000, K_FREE = 2, W_SLOW = 2419200 (a week at 250 ms), K_FREE_SLOW = 16.
Block sequence
Everywhere in this specification, "block" means a block of the chain executing the transaction. refs is initialized and discarded on that chain's block boundaries, and windows are computed from that chain's block height. A chain whose NUMBER opcode reports its parent chain's height MUST key both counters on its own block sequence and MUST NOT key them on NUMBER. An implementation that keys on NUMBER on such a chain fails silently: it produces a well-formed schedule at the wrong ordinal, because every child block that shares a parent block shares one counter.
Definitions
- Enrolled account. An account
Afor whichREGISTRY.isEnrolled(A)returnedtruein the registry snapshot taken at the start of the current block (see Registry snapshot). - Reference. A message call whose target is an enrolled account, as enumerated under Counted operations.
- Reference ordinal
k. For a reference to accountA, the valuerefs[A]after incrementing, whererefsis the block-scoped counter defined below. The first reference toAin a block hask = 1. - Fast surcharge.
min(G_REF * k * k, G_REF_MAX)fork > K_FREE, else0. It is charged to whoever makes the reference. - Window. Block numbers
nwith the same value ofn / W_SLOWbelong to the same window. - Originator. The
ORIGINof the transaction making the reference. - Slow ordinal
m. For a reference to accountAby originatoro, the valuewrefs[(A, o)]after incrementing, wherewrefsis the window-scoped counter defined below. - Slow surcharge.
min(G_REF_SLOW * m * m, G_REF_SLOW_MAX)form > K_FREE_SLOW, else0. - Surcharge. The larger of the fast and the slow surcharge for the reference.
Block-scoped counter
At the beginning of the execution of every block with number >= FORK_BLOCK, the client MUST initialize an empty map
refs: address -> uint64The map MUST persist across all transactions of the block, MUST be discarded when the block's execution completes, and MUST NOT be included in the state root or receipts root. Where EIP-7928 is active its values are recorded in the block access list (see Block-level access lists); no other structure commits to them. Its contents are fully determined by the ordered execution of the block's transactions, so all conforming clients compute identical values.
The map is not rolled back on transaction failure or on frame revert (see Reverts).
Window-scoped counter
The client MUST additionally maintain a map
wrefs: (address, address) -> uint64keyed by enrolled account and originator, that counts references across every block of the current window. At the first block of a new window the map MUST be discarded and re-initialized empty. Like refs it is not part of state, is recorded only in the block access list where EIP-7928 is active, is fully determined by ordered execution, and is not rolled back on failure or revert.
The two counters price two things. refs is global and prices repetition within a block, whoever it comes from, so splitting a bundle across senders changes nothing. wrefs follows one originator across blocks and prices the same wallet coming back over hours and days. A reference pays the larger of the two surcharges, never both.
These shapes come from replays, not a prior. Two days of one extraction operator's activity on Robinhood Chain (65 launches, 45,728 transfers, 198 serial or heavy wallets against 2,911 others) showed the machine returning to a token 6 to 37 times over about twelve hours while other wallets touched it 3 to 8 times over about seven minutes; the slow ratchet is sized to that. In that sample the machine's same-block repetition came from one wallet at a time, and an earlier draft therefore charged the fast surcharge only to an originator already repeating in the block. On a later launch carrying the token-level form, a crew buying from thirteen wallets in the same block paid nothing under that gate. With the gate removed, over that token's first two hours (409 trades, 250 ms blocks) the crew paid 19 basis points of its volume and every other wallet 0.6. The gate is not in this specification (see Bystanders in busy blocks).
Registry snapshot
At the beginning of the execution of every block with number >= FORK_BLOCK, before executing the block's first transaction, the client MUST read the enrollment set from the registry contract's storage as it stands at the end of the previous block, and use that set for every reference decision in the block. An account A is in the set for block N if its beneficiary slot is non-zero and its exit slot is either zero or greater than N.
Enrollment during block N therefore takes effect at block N + 1, and an exit takes effect at the block recorded by scheduleExit, which is at least W_SLOW blocks later. This is required so that a contract cannot toggle its own cost mid-block and so that clients can consult a fixed set for the whole block.
Clients MAY implement the snapshot lazily (reading the registry's storage slots for an address on first reference and caching them for the block) provided the values observed are the end-of-previous-block values. A scheduleExit made earlier in the current block cannot change the current block's answer, since its exit block is at least W_SLOW blocks away; an enroll made earlier in the current block, including a re-enrollment after an exit, MUST be ignored until the next block.
Registry contract
At FORK_BLOCK the client MUST set the code of REGISTRY_ADDRESS to the canonical registry runtime code (see Reference Implementation). The registry has the following external interface:
interface IReferenceRegistry {
event Enrolled(address indexed account, address indexed beneficiary);
event ExitScheduled(address indexed account, uint256 exitBlock);
/// Enroll msg.sender with SINK_ADDRESS as beneficiary.
function enroll() external;
/// Enroll msg.sender with a chosen beneficiary.
/// Reverts unless beneficiary.codehash == STAKE_VAULT_CODEHASH.
function enroll(address beneficiary) external;
/// Schedule msg.sender's exit for the first block of the window after next.
/// Cannot be cancelled.
function scheduleExit() external;
function isEnrolled(address account) external view returns (bool);
function beneficiaryOf(address account) external view returns (address);
/// The block at which the account stops being enrolled, or 0 if no exit is scheduled.
function exitBlockOf(address account) external view returns (uint256);
}Storage layout, which clients read directly for the snapshot:
- slot
keccak256(abi.encode(account, uint256(0)))holds the beneficiary address foraccount, or zero if it has never enrolled; - slot
keccak256(abi.encode(account, uint256(1)))holds the block at whichaccount's enrollment ends, or zero if no exit is scheduled.
Rules:
enrollandenroll(address)MUST revert ifmsg.senderis enrolled at the current block, including while an exit is scheduled and has not yet taken effect, and MUST revert while the current block is below the exit block plusW_SLOW. After that,enrollwrites the new beneficiary and clears the exit slot.enroll(address)MUST revert ifEXTCODEHASH(beneficiary) != STAKE_VAULT_CODEHASH.- Enrollment and exit are by the account itself. There is no function that enrolls a third party, and no function that removes another account's enrollment.
scheduleExitMUST revert unlessmsg.senderis enrolled and has no exit scheduled. It sets the exit slot to(n / W_SLOW + 2) * W_SLOW, wherenis the current block, and emitsExitScheduled. The exit therefore always falls on a window boundary, at leastW_SLOWand fewer than2 * W_SLOWblocks after the request, and a scheduled exit cannot be cancelled or brought forward.- The registry is a plain contract with no owner and no upgrade path. Its address is reserved and its code MUST NOT be replaced by any later mechanism except a subsequent EIP.
- Enrolling
REGISTRY_ADDRESS,SINK_ADDRESS, any precompile, or the system contracts of EIP-4788 and similar EIPs is not possible because those accounts never callenroll; clients MUST NOT special-case them. - "The current block" and
nmean the chain's own block height, per Block sequence. On a chain whoseNUMBERreports the parent chain's height, the canonical registry code for that chain MUST read its own block height from the chain's native source (on Arbitrum-family chains,ArbSys.arbBlockNumber()).
Counted operations
A reference occurs when a message call is made whose target account is enrolled and has non-empty code, in exactly these cases:
- The top-level transaction, when
tx.tois enrolled. This applies to legacy, EIP-2930, EIP-1559, EIP-4844 and EIP-7702 transaction types. Contract creation transactions (tx.to == null) never count. CALLandCALLCODEopcodes, when the target address is enrolled.
The following are never references, regardless of enrollment:
STATICCALL. A read cannot change the state of the account it reads, so it cannot be a leg of anything this EIP prices (see ExcludingSTATICCALL). It does not increment either counter.DELEGATECALL. The code of the target runs in the caller's storage context. The account whose state is affected is the caller, which is counted when it is itself called.CREATEandCREATE2. A freshly created account cannot be enrolled in the block of its creation (snapshot rule), so the question does not arise; in later blocks creation is not a call.SELFDESTRUCTbeneficiaries, value transfers to enrolled accounts throughSELFDESTRUCT, and block reward or withdrawal credits.- Calls to an enrolled account that has empty code at the time of the call. An enrolled account whose code was removed cannot be referenced in the counted sense; if code is later placed there through
CREATE2at the same address, enrollment persists and counting resumes. - Calls to precompiled contracts.
- The system calls a client makes to system contracts at the start or end of block processing (for example the EIP-4788 beacon root update), which are not EVM message calls from a transaction.
A call that is counted is counted whether or not it succeeds, whether or not the target frame executes any code, and whether or not value is transferred. The count is taken at the moment the call is issued, before the call's own gas is deducted and before the target frame begins.
Gas accounting
For a counted call with reference ordinal k and slow ordinal m:
fast = min(G_REF * k * k, G_REF_MAX) if k > K_FREE else 0
slow = min(G_REF_SLOW * m * m, G_REF_SLOW_MAX) if m > K_FREE_SLOW else 0
surcharge = max(fast, slow)The surcharge MUST be charged to the calling frame, in addition to every other cost of the call as specified by the Yellow Paper, EIP-150, EIP-2929 and later EIPs, and BEFORE the 63/64 gas forwarding computation of EIP-150 is applied. That is, the surcharge reduces the gas available to be forwarded, exactly as the cold-access cost of EIP-2929 does.
Order of charges for a CALL-family opcode into an enrolled target:
- static opcode cost;
- EIP-2929 cold or warm access cost for the target;
- the surcharge for the reference;
- value transfer cost and new-account cost, if applicable;
- memory expansion;
- compute the forwardable gas per EIP-150 from what remains.
For the top-level transaction, the surcharge for tx.to is added to the intrinsic gas of the transaction. A transaction whose gas limit cannot cover intrinsic gas including the surcharge is invalid and MUST NOT be included in a block. Because the surcharge depends on the block-scoped counter, transaction validity for inclusion is evaluated at the transaction's position in the block, the same way balance sufficiency is.
If the calling frame does not have enough gas for the surcharge, the calling frame fails with an out-of-gas exception. The reference has still been counted.
The surcharge participates in the ordinary gas mechanics of the frame: it is part of gas used, it is never refunded, and the EIP-3529 refund cap applies to the transaction's total gas used including surcharges.
Surcharge tracking and distribution
Each transaction carries an accumulator surchargeGas, initialized to zero, to which every surcharge charged in any frame of the transaction is added. Surcharges charged in frames that later revert remain in the accumulator: the gas was consumed and the reference was counted.
At the end of the transaction, after gas refunds are applied and the sender is charged for gas used, the client MUST compute
S = surchargeGas * effectiveGasPrice
sinkHalf = S / 2 (integer division)
beneHalf = S - sinkHalfand:
- treat the
surchargeGasportion of gas used as carrying neither base fee nor priority fee under EIP-1559 accounting: the coinbase receivespriorityFeePerGas * (gasUsed - surchargeGas), the base fee burn is computed ongasUsed - surchargeGas, and the surcharge's full valueSis distributed as follows. No part of the surcharge is burned and no part reaches the coinbase. - credit
sinkHalfwei toSINK_ADDRESS. - credit
beneHalfwei to the beneficiary recorded in the registry snapshot for the enrolled account referenced, if the transaction contains references to exactly one enrolled account; otherwise credit each referenced account's beneficiary in proportion to the surcharge gas attributable to that account. Clients MUST therefore track surcharge gas per referenced account within the transaction. Rounding remainders go toSINK_ADDRESS. For an account that enrolled withenroll(), the beneficiary isSINK_ADDRESSand the whole ofSgoes to the sink.
A beneficiary credit is a balance increase with no message call, identical in mechanism to a withdrawal credit or a coinbase payment. It does not invoke the beneficiary's code and cannot fail. Since every permitted beneficiary is a StakeVault, every wei of surcharge ends as validator stake.
On a chain without a beacon deposit path (a rollup), the same split applies and the vaults bridge to their counterparts on the parent chain, per Stake vaults and the sink.
Stake vaults and the sink
The canonical StakeVault is a contract with a single constructor parameter withdrawalAddress, held in storage and never written again, so that every instance has the same runtime code and STAKE_VAULT_CODEHASH identifies them all. Its interface:
interface IStakeVault {
struct ValidatorFields {
bytes32 withdrawalCredentials;
uint64 effectiveBalance;
bool slashed;
uint64 activationEligibilityEpoch;
uint64 activationEpoch;
uint64 exitEpoch;
uint64 withdrawableEpoch;
}
/// A validator proven against the parent beacon block root of the block at `timestamp`
struct ValidatorProof {
uint64 timestamp;
uint40 validatorIndex;
ValidatorFields fields;
bytes32[] branch;
}
event Registered(bytes pubkey, address indexed operator);
event Staked(bytes pubkey, address indexed operator);
event Ejected(bytes pubkey, address indexed by);
event Exited(bytes pubkey, address indexed operator, uint256 bondReturned);
function withdrawalAddress() external view returns (address);
function register(bytes calldata pubkey) external;
function stake(bytes calldata pubkey, ValidatorProof calldata proof) external;
function eject(bytes calldata pubkey, ValidatorProof calldata proof) external payable;
function exit(bytes calldata pubkey, ValidatorProof calldata proof) external;
}A vault never creates a validator. The deposit contract does not check signatures, and the consensus layer ignores the withdrawal credentials of a deposit to a public key it already knows. A vault that made the first deposit for a key could therefore be front-run by a deposit for the same key with other credentials, after which the vault's 32 ETH would fund a validator that pays somebody else; and it could lose 32 ETH outright to a signature that does not verify. So the operator makes the first deposit, and the vault tops up only what it can prove.
Rules:
- An operator calls
register(pubkey)and then deposits at least 1 ETH forpubkeytoDEPOSIT_CONTRACTthemselves, with withdrawal credentials0x01 || 0x00 * 11 || withdrawalAddress. That deposit is the operator's bond. Whoever registered a key is its operator of record; a key can be registered once. stake(pubkey, proof)MAY be called by anyone. It MUST revert unlesspubkeyis registered and not yet staked, andproofshows a validator with that public key in the beacon state whose withdrawal credentials are0x01or0x02followed by eleven zero bytes andwithdrawalAddress, which is not slashed and has no exit epoch. If the credentials are0x01, the validator's effective balance MUST NOT exceed 1 ETH: a balance above 32 ETH on such a validator is swept to the withdrawal address, and a top-up of a funded one would turn stake into balance.stakeMUST revert unlessaddress(this).balance >= bondsOwed + BOND + TOP_UP. It then addsBONDtobondsOwedand deposits exactlyTOP_UPtoDEPOSIT_CONTRACTforpubkey, computing the deposit data root itself. The caller supplies no credentials, no signature and no root.- A proof is a Merkle branch from the validator to the beacon block root returned by the EIP-4788 contract for
proof.timestamp, which MUST NOT be older thanMAX_PROOF_AGE. The path is the validator's index invalidators, the list's length mix-in, field 11 of a state tree of depth 6, and field 3 of the block header: 50 siblings. A fork that takes the beacon state past 64 fields changes the depth, and with itSTAKE_VAULT_CODEHASH. exit(pubkey, proof)MAY be called by anyone. It MUST revert unlessproofshows a withdrawable epoch that has passed. It subtractsBONDfrombondsOwedand paysBONDto the operator of record, unless the validator was slashed, in which case the bond stays in the vault.eject(pubkey, proof)MAY be called by anyone, on a vault whosewithdrawalAddressis itself, for an active validator whose effective balance has fallen below 32 ETH. It forwardsmsg.valueto the EIP-7002 contract as the fee of a full exit request.- The vault has no other function that transfers ETH. It has no owner.
SINK_ADDRESSis theStakeVaultinstance whosewithdrawalAddressisSINK_ADDRESSitself. Every reward and every exited balance returns to it and can only be staked again.- The operator runs the validator. Consensus-layer rewards accrue to the withdrawal address, not to the operator. The operator's compensation is whatever the execution layer pays the fee recipient the operator sets, which this EIP does not constrain.
On a chain without a beacon deposit path (a rollup), the vault holds what it receives until it is bridged to the vault with the same withdrawalAddress on the parent chain, where it is staked as above. The bridging contract is outside this EIP.
An account MAY name any StakeVault instance as its beneficiary at enrollment, including one whose withdrawalAddress is the account's own treasury. That treasury receives consensus rewards as they are swept, and the principal only when a validator exits, which the exit queue delays. The share is the enroller's, as stake; the sink's share is nobody's.
Reverts and exceptional halts
The counter is monotone within a block. It MUST NOT be decremented or restored on:
REVERTin the called frame or any ancestor;- an exceptional halt in the called frame or any ancestor;
- a failed top-level transaction (out of gas, revert, invalid opcode);
- a call that fails before the target frame starts, for example because the caller cannot afford value transfer or the call depth limit is reached. In the depth-limit case the reference is still counted, since the client has already resolved the target.
The surchargeGas accumulator is likewise monotone within a transaction.
Block-level access lists
Where EIP-7928 is active, the surcharge on a transaction depends on references made by the transactions before it in the block, and on references its originator made in earlier blocks of the window. Neither is state, so neither would otherwise appear in the block access list, and a client could execute a transaction in parallel only after executing everything before it. The counters MUST therefore be recorded in the block access list.
AccountChanges gains a seventh element, reference_changes, after code_changes:
# ReferenceChange: [block_access_index, refs_after, window_after]
# for one transaction that referenced this account; the originator is that transaction's sender
ReferenceChange = [BlockAccessIndex, uint64, uint64]
# WindowCarry: [originator, window_before]
# wrefs[(account, originator)] at the start of the block
WindowCarry = [Address, uint64]
ReferenceChanges = [List[ReferenceChange], List[WindowCarry]]
AccountChanges = [
Address,
List[SlotChanges], # storage_changes
List[StorageKey], # storage_reads
List[BalanceChange], # balance_changes
List[NonceChange], # nonce_changes
List[CodeChange], # code_changes
ReferenceChanges # reference_changes
]Rules:
- For every account
Aenrolled in the block's snapshot and every transactionithat made at least one reference toA,A'sreference_changesMUST contain exactly oneReferenceChangeat indexi, holdingrefs[A]andwrefs[(A, o)]after transactioni, whereois the transaction's sender. - For every originator
othat made a reference toAin the block and for whichwrefs[(A, o)]was non-zero at the start of the block,A'sreference_changesMUST contain exactly oneWindowCarrywith that value. In the first block of a window this list is empty. - Every other account carries
[[], []].ReferenceChangeentries are ordered by index,WindowCarryentries lexicographically by originator. - Each
ReferenceChangeand eachWindowCarrycounts as one item towardbal_items. EveryReferenceChangealready pays for at least one cold account access toAwithin its transaction (2600 gas, or 2400 through an access list), so the limit binds no earlier than it does for the access itself. - A block whose
reference_changesdo not match execution is invalid, as with any other block access list mismatch.
With these entries a client can price transaction i without executing the transactions before it: refs[A] before i is refs_after at the largest index below i, or zero; wrefs before i is window_after at the largest index below i whose sender is o, else the WindowCarry for o, else zero. A node that joins in the middle of a window can execute the next block from that block's access list alone, and can rebuild the full wrefs map, which it needs to build blocks, by taking the last window_after for each pair from the access lists of the window's earlier blocks, without executing them. Clients that build or validate blocks MUST therefore keep either the wrefs map or the reference_changes of the current window, and a validating client MUST check every WindowCarry against that map: a carry is the one entry a block's own execution cannot confirm, and an inflated carry raises the slow surcharge of the originator it names. On Ethereum mainnet EIP-7928 already retains access lists for the weak subjectivity period, which is longer than W_SLOW; a chain that sets W_SLOW longer than its access list retention MUST retain them for W_SLOW blocks.
On a chain where EIP-7928 is not active, the counters remain client memory as specified above.
JSON-RPC
eth_estimateGasandeth_callMUST execute against the pending block's counter as it stands after all transactions preceding the estimated one in the pending block, or, when no pending block is being built, against an empty counter. Implementations SHOULD document which.- Clients SHOULD expose the surcharge separately in
debug_traceTransactionoutput as a per-call fieldreferenceSurcharge. - A new optional method
eth_getReferenceCount(address, blockTag)MAY be provided, returning the counter value for the address in the specified block (zero for unenrolled or unreferenced accounts).
Activation
This EIP activates at FORK_BLOCK. At activation the client MUST:
- set the code of
REGISTRY_ADDRESSto the canonical registry code with empty storage; - set the code of
SINK_ADDRESSto the canonicalStakeVaultcode withwithdrawalAddress == SINK_ADDRESS; - begin maintaining the counter and registry snapshot from the first block at or after
FORK_BLOCK.
Nothing can be enrolled before FORK_BLOCK, so the first block after activation runs with an empty enrollment set and the gas schedule is unchanged until an account enrolls.
Rationale
Block scope rather than transaction scope
Transaction scope, which could be implemented today in application code with transient storage, is defeated by splitting a machine across a bundle of transactions in one block. On sequencer chains that is how same-block add and remove is actually executed. Block scope prices the bundle as one thing. Transaction-scoped escalation remains available to applications and composes with this EIP.
Two ratchets
An earlier draft had one global per-block counter, and a later one paired it with a per-originator counter over a few minutes. It prices a bundle correctly and cannot be evaded with fresh wallets, but it cannot see the same wallet coming back in the next block, and on chains where gas is nearly free it can be griefed cheaply (see Security Considerations). A counter keyed by originator alone cannot be griefed and sees the returning wallet, but a machine that splits its legs across two originators in one block pays nothing. Keeping both, and charging the larger, closes each one's hole with the other: the fast global ratchet catches the split bundle, the slow originator ratchet catches the return visit. The slow ratchet is sized to the ninetieth percentile of ordinary use, sixteen references per wallet per account per week, so a person who trades a token a dozen times over a week never pays it, while a machine that returns thirty times does. K_FREE = 2 on the fast ratchet spares a single swap in a quiet block, which is two references on a v4 route. Nothing spares a bystander in a busy one (see Bystanders in busy blocks).
Global rather than per sender
A counter keyed by tx.origin, by the calling contract, or by any sender-side identity is defeated by fresh wallets and fresh helper contracts at no cost. A global counter has an honest cost: an enrolled contract pays for its own organic volume once it exceeds K_FREE references per block. Two things bound that cost. Enrollment is a choice, made by the account itself, with consequences that are stated below and that cannot be shed in less than a window. And the surcharge is the only cost that scales: at mainnet constants the 10th reference in a block costs 200,000 gas extra, the 30th costs 1,800,000, and the cap binds from the 50th. A contract that expects hundreds of organic references per block should not enroll; a token in its launch week, whose organic activity is tens of references per block and whose adversary's activity is hundreds, should.
Quadratic rather than linear or exponential
A linear surcharge is a flat per-reference tax that a machine with positive expected value per reference absorbs and passes to its victims. Quadratic growth means the marginal cost of the machine's next reference grows with the machine's own size, so the largest surfaces (many pools, many rungs) are priced out first while a router that hops a token through two or three venues pays a few multiples of G_REF. Exponential growth was considered and rejected because it would make even a three-hop route prohibitive at any base constant large enough to matter for the machine.
Counting calls rather than storage writes
Storage writes to an enrolled account occur inside frames executing that account's code, so counting them would require per-account accounting inside the interpreter loop. Counting calls happens once per frame boundary, where EIP-2929 accounting already occurs, and captures every way an external actor can reference an account.
Counting reverted references
A reverted call leaves no state behind, but it is not free and it is not meaningless. It reads the state it was sent against (a price, a reserve, whether a pool exists, whether a launch is live) and its revert is the decision not to act on what it read. That is how the machine this EIP prices works: it sends many attempts into the same account in a block, each guarded by a condition, and reverts every one except the one that wins. Within a transaction it does the same thing one frame down, trying references in subframes and reverting the losers. If the counter rolled back with the frame, every losing attempt would be free and only the winner would be counted, so the surcharge would fall on one reference per block no matter how many were sent. The repetition this EIP prices would be invisible exactly where it is heaviest.
The counter is also not EVM state. No opcode reads it, and a frame that reverts leaves nothing an EVM program can observe; the only effect is on the gas of later references. That is the same kind of effect a reverted transaction already has: its gas is used and paid, its nonce is consumed, and its gas counts toward the block's fullness and so toward the next block's base fee under EIP-1559. EIP-2929's access sets do roll back on revert, and the difference is deliberate. Warmth describes what a frame has loaded, which a revert discards. The counter describes what has been attempted, which a revert cannot undo.
What a single operation costs depends on the venue
The unit that is counted is the reference, not the user's intent. A swap that reaches the enrolled account once is one reference; the same swap through a venue that reaches it four times is four. "A single swap pays nothing" therefore holds when the venue's route makes at most K_FREE references and nothing else has referenced the account in the block. It does not hold for every venue or every block. A replay of the token-level form over one token's full history found the largest payers to be the pool manager and ordinary routers, not dedicated extraction contracts. That is the mechanism pricing repetition wherever it occurs, and it is the reason K_FREE is 2 and not 1. A venue that needs more references per operation than the free allowance passes a cost to its users that a venue with a tighter route does not.
The cap
G_REF_MAX binds from the 50th reference in a block at mainnet constants. A contract that legitimately needs more than about fifty references per block is priced out, and that is intended: such a contract should not enroll. The slow ratchet does not carry that case. It is keyed by originator and exists to price the returning wallet, not to relieve the global counter.
Excluding STATICCALL
An earlier draft counted reads. A replay settled it. Every transaction that moved any of 24 tokens on Robinhood Chain was traced, 8,019 transactions from 2,734 originators, and each call into the token was recorded by type. Of 62,168 calls, 57% were STATICCALL, and 98% of those were balanceOf: routers and pools checking balances before and after a transfer. Priced at the rollup constants, counting reads charged 80% of ordinary wallets on their first transaction on a token. Exempting reads brought that to 31% and cut the surcharge on ordinary wallets elevenfold, against eightfold for wallets matching the machine's shape. Reads carry no extraction, since a static frame cannot change the state it reads, and they are the larger part of what a bystander's venue does on the bystander's behalf.
The figures in this section were measured under the earlier draft's rule, which charged the fast surcharge only from an originator's own third reference in a block. Without that gate more ordinary wallets are charged, and the call-level replay has not been rerun; the comparison between counting and exempting reads does not depend on it.
With reads exempt, the replay leaves 31% of ordinary wallets charged on their first transaction on a token, and the constants are kept as they are. Those wallets are not people who approve and then swap: that is two transactions in two blocks, the fast counter resets between them, and the pair costs nothing. Of the approvals seen inside ordinary wallets' transactions, 95% were issued by a router or aggregator in the middle of the swap, not by the wallet. What is charged is a route that makes three or more state-changing calls into the token for one operation. That cost belongs to the venue that chose the route, and a venue with a tighter route does not pass it on. Raising K_FREE to fit the loosest common route would spare those venues and the machine alike: at K_FREE = 8 the share of machine-shaped transactions charged falls from 66% to 16%. Exempting approvals by selector was also considered and rejected for the protocol form, because the client cannot know what a selector means and any contract may name an extractive function approve; STATICCALL is exempt because the client itself guarantees a static frame changes nothing.
Excluding DELEGATECALL
Counting DELEGATECALL targets would count on the implementation contract shared by every proxy that uses it, so one enrolled implementation would make every one of its proxies expensive collectively. The account whose storage a machine manipulates is the proxy; the proxy is what enrolls and what is counted.
The registry snapshot
Without the snapshot rule, a contract could enroll itself in the middle of a block after its own references were free and before a competitor's, or a client would have to re-read registry storage on every call. Taking the end-of-previous-block set once per block gives every client a fixed input and makes enrollment a visible, one-block-delayed event.
Staked, not burned, and not paid to the producer
Burning the surcharge would benefit every holder of ETH pro rata, including the actor being priced. Paying it to the block producer would give builders a reason to induce references. Staking it does neither: the ETH stays in the system as security budget, it belongs to nobody, and it cannot be withdrawn. Half goes to a sink with no owner, no governance and no exit, so the practice being priced funds the network's validators forever. The other half goes to a stake vault of the enroller's choosing, so an issuer that enrolls has skin in the same game and receives its share only as consensus rewards over time. No foundation, no research organization, no general partner is in the path.
Stake vaults as the only permitted beneficiary
If an enroller could name an arbitrary beneficiary, enrollment would become a way to charge callers a private toll in ETH, which is the practice this EIP exists to price. Requiring the beneficiary to be a stake vault means the enroller's half is locked into validator duty and reaches the enroller only as consensus rewards and eventual exits, at the pace of the beacon chain's withdrawal queue. The code-hash check is cheap, needs no registry of vaults, and lets anyone deploy a vault with the withdrawal address of their choice.
Opting in and opting out
Enrollment is a choice with consequences on both sides, and they are stated here so nobody enrolls, or declines to, by accident.
A token that enrolls gets this and only this: the second and later calls into it within a block cost the caller superlinear gas that the caller cannot recapture. Repeated fills, separately settled adds and removes, and multi-venue routing through the token all pay. Pool initialization, and a sequence a venue nets into one settlement, do not (see What this does not price). So does the token's own organic volume once it exceeds K_FREE references in a block, which is the price of the choice and the reason the constants are per chain. Enrollment does not stop ordering attacks: a sandwich is two references, not two hundred, and is priced elsewhere. Leaving takes at least a full window and is announced when it is requested, so a token cannot switch the surcharge off for a block, a bundle or a buyer. And the issuer never receives the surcharge as spendable ETH: half is staked in the no-owner sink and half in a stake vault the issuer named at enrollment, or all of it in the sink if the issuer named none. Enrolling cannot be turned into the issuer's own toll.
A token that does not enroll changes nothing for itself. Every call costs what it costs today, every machine that walks launches keeps walking it, and every router prices it the same as before the fork. The chain does not force the choice and does not need to: wallets, explorers and launchpads can read isEnrolled and show it. A launchpad that leaves its tokens unenrolled is stating, in a way any interface can display, that it is fine with its users being order flow for the machine. That is a legitimate position; this EIP only makes it visible.
For extractors nothing is banned. A bot that touches hundreds of tokens a month is unaffected on every one that did not enroll, and on an enrolled token it pays a price that grows with the size of its own surface. Extraction at the scale of one arbitrage is still cheap; extraction as a practice, the same machine on every launch, is what becomes expensive.
For the chain the surcharge is gas, so it becomes validator stake, never a payment to an intermediary and never producer revenue. Block builders can reorder enrolled-token transactions to shift who pays a surcharge, as they can reorder to shift EIP-2929 warmth today.
What this does not price
A reference is a call into the enrolled account. When a token enrolls, three things the motivating machine does are therefore outside this EIP:
- Pool initialization. Initializing a pool on a singleton venue calls the venue, not the token, and moves no balance. Pool spam, and a price ladder built by initialization alone, are invisible to the token and to this counter. Pricing them needs the venue.
- Netted sequences. A flash-accounting venue settles any sequence of adds, swaps and removes inside one unlock with at most one net transfer per token, so the sequence is one or two references however long it is.
- Anything that needs only a few calls. A sandwich is two references. A searcher that simulates off chain and lands one transaction makes one.
What remains is what has to be on chain to be worth anything: each fill, each separately settled liquidity move, each return visit. The surcharge is a price on those, in proportion to how much of a block one account's traffic takes. It is not a deterrent and is not sized to be one.
Leaving with notice rather than never
An earlier draft made enrollment irrevocable, so that a token could not enroll for its launch and then sell the surface back. That shut the door on a contract whose circumstances change (a token that outgrows the free allowance, a venue that migrates) and gave the contract no way to undo a decision the protocol lets it make. What irrevocability actually protected was narrower: a caller should never find the surcharge gone for the one block, bundle or counterparty where removing it was worth something to the issuer.
scheduleExit keeps that. The exit is public from the moment it is requested, takes effect only at the start of the window after next, so never sooner than W_SLOW blocks, and cannot be cancelled or moved. It always lands on a window boundary, where wrefs is discarded anyway, so no originator's slow count is cut off part way. A contract that has left cannot enroll again until a full window after its exit, so it cannot schedule an exit and re-enroll inside the exit block to open a one-block hole a window in advance: any gap it opens lasts more than a window and is announced a window ahead. What a contract still cannot do is decide per call or per caller whether the surcharge applies: that discretion would let it waive the surcharge for its own bundles and charge everyone else, which is the toll this EIP is written to prevent.
Counters in the access list, not in state
The cross-transaction dependency that block-scoped storage proposals create for EIP-7928 exists here too: the surcharge on transaction i depends on what came before it. Putting the counters in state would make them visible to the access list for free, but refs would then be written and cleared for every referenced account in every block, and wrefs, keyed by originator, would grow state with every new wallet and could not be cleared at the end of a window without iterating it. Recording them in the access list gives a parallel executor and a node joining mid-window everything they need, commits to them through block_access_list_hash, and leaves state exactly as it was. The cost is a few bytes per referencing transaction and three bytes per other account.
Why not extend EIP-2929 directly
EIP-2929's sets are per transaction, are reset at every transaction boundary, and price the first access higher than later ones. This EIP's map is per block and prices later accesses higher than the first. Reusing the same data structure would conflate two signals with opposite shapes. The implementation is nevertheless the same kind of object, consulted at the same point in the call path.
Why a system contract and not an account flag
A per-account flag in the state trie needs an account format change and a migration. A system contract whose storage the client reads once per block needs neither, has precedent in EIP-4788, and keeps the enrollment set queryable by ordinary contracts through isEnrolled.
Backwards Compatibility
No account is enrolled at activation, so no existing transaction, contract or tool changes behavior at the fork. Changes appear only for accounts that enroll afterwards, and only for callers of those accounts:
- Gas cost of a call into an enrolled account becomes dependent on block context. Tooling that assumes a call's gas is a function of state alone (fixed-gas relayers, some bundle simulators, some meta-transaction forwarders) will under-estimate for enrolled targets and MUST consult
isEnrolledor the pending counter. - EIP-1559 fee accounting changes for transactions that include surcharges: both the base fee burn and the coinbase's priority fee are computed on gas used net of surcharge gas, and the surcharge's value follows the split above. Transactions with no surcharges are accounted exactly as before.
- Contracts that enroll and are also
DELEGATECALLimplementations are not counted through that path, as specified. - The intrinsic gas of a transaction whose
tx.tois enrolled varies with position in the block. Transaction pools SHOULD treat the maximum possible surcharge (G_REF_MAX) as the requirement for admission and let block builders evaluate the actual value at inclusion.
This EIP requires a hard fork.
Test Cases
Notation: A is an enrolled account with non-empty code; U is an unenrolled account; P is a proxy that DELEGATECALLs into A; costs are stated as gas above the pre-fork cost of the same operation. Mainnet constants.
- Unenrolled is unchanged. 100
CALLs intoUin one block, across any number of transactions: surcharge0on every call. - Enrollment delay.
Acallsenroll()in blockN. In blockN, 5 calls intoAafter the enrollment transaction: surcharge0on every call. In blockN+1, the same 5 calls: surcharges0, 0, 18000, 32000, 50000. - Cross-transaction counting. Block with transactions 1, 2 and 3 each calling
Aonce, from three different senders: transactions 1 and 2 pay0, transaction 3 pays18000. - Reverted call counts. Transactions 1 and 2 each call
Aand revert. Transaction 3 callsA: pays18000. ThesurchargeGasof transactions 1 and 2 is0(ordinals 1 and 2); their gas used includes no surcharge. - Reverted frame keeps surcharge. One transaction:
CALL Atwice (free), then a subcall that doesCALL A(ordinal 3,18000) and reverts. The transaction'ssurchargeGasis18000and its gas used includes it. - Cap. 60 calls into
Ain one block: the surcharge for ordinal 50 ismin(2000 * 2500, 5000000) = 5000000; for ordinal 51 it is5000000. DELEGATECALLexcluded.CALL P, wherePis unenrolled andPDELEGATECALLsA: surcharge0. IfPis enrolled:CALL Pis counted onP, and theDELEGATECALLintoAis not counted onA.STATICCALLnot counted. 100STATICCALLs intoAin one transaction: surcharge0on every one, andrefs[A]andwrefsare unchanged afterwards.- Top-level target counted. Transaction with
tx.to == A, after two prior calls intoAin the block: intrinsic gas is increased by18000; a gas limit that covers the pre-fork intrinsic gas but not the increase makes the transaction invalid for inclusion at that position. - Out-of-gas on surcharge. A frame with 5,000 gas remaining performs
CALL Aat ordinal 3 (surcharge18000): the frame halts with out-of-gas; the counter forAis now 3. - Empty code.
Ais enrolled but has no code: calls intoAare not counted and pay no surcharge. - Distribution, single account. Transaction with
surchargeGas = 50000(ordinals 3 and 4), effective gas price100 gwei:S = 5,000,000 gwei;2,500,000 gweicredited toSINK_ADDRESS;2,500,000 gweicredited tobeneficiaryOf(A); base fee burn and coinbase priority fee both computed ongasUsed - 50000. IfAenrolled withenroll(),SINK_ADDRESSreceives the full5,000,000 gwei. - Distribution, two accounts. Transaction references enrolled
A(surcharge18000) and enrolledB(surcharge32000):SINK_ADDRESSreceivesS / 2; of the other halfA's beneficiary receives18/50andB's receives32/50; the wei remainder goes toSINK_ADDRESS. - Registry rejects bad beneficiary.
enroll(U)whereU.codehash != STAKE_VAULT_CODEHASHreverts;enroll(V)whereVis aStakeVaultsucceeds andbeneficiaryOf(msg.sender) == V. - Registry rejects re-enrollment while enrolled. Second
enroll()from the same account reverts, and so doesenroll()afterscheduleExit()untilW_SLOWblocks after the exit block. - Vault stakes only to the deposit contract.
V.stake(...)withmsg.value != BONDreverts; with a vault balance below32 ether + totalBondsreverts; a successful call emits a deposit with credentials0x01 || 0 * 11 || V.withdrawalAddress(). eth_estimateGas. After 9 references toAin the pending block, estimating a transaction that callsAonce returns the base estimate plus200000.- Leaving takes a window.
AcallsscheduleExit()in blockN = 3 * W_SLOW + 10:exitBlockOf(A) == 5 * W_SLOW. In block5 * W_SLOW - 1, 5 calls intoAfrom one originator pay0, 0, 18000, 32000, 50000. In block5 * W_SLOW, the same 5 calls pay0andrefs[A]stays0. A secondscheduleExit()before the exit reverts. - Re-enrollment after exit.
Aexited at blockE.enroll(V)fromAreverts in blockEand in blockE + W_SLOW - 1. In blockE + W_SLOWit succeeds: calls intoAin that block are not counted; from blockE + W_SLOW + 1they are,beneficiaryOf(A) == VandexitBlockOf(A) == 0. - Access list entries. With EIP-7928 active, a block whose transactions 1 and 3 each call
Aonce from the same sendero, which made 4 references toAin earlier blocks of the window:A'sreference_changesis[[[1, 1, 5], [3, 2, 6]], [[o, 4]]]. Every account without references carries[[], []]. - Slow ratchet. Originator
ohas made 16 references toAin earlier blocks of the window. Its next reference, the first toAin its block: slow ordinal 17, surcharge400 * 17 * 17 = 115600. The same call from an originator with no earlier references in the window:0. - Larger of the two. The same originator
omakes that reference as the 10th toAin its block: fast200000, slow115600, surcharge200000.
Reference Implementation
Registry
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.26;
contract ReferenceRegistry {
bytes32 constant STAKE_VAULT_CODEHASH = 0x0; // set at deployment
address constant SINK = 0x000000000000000000000000000000000000e5CB;
uint256 constant W_SLOW = 50400;
mapping(address => address) private _beneficiary; // slot 0
mapping(address => uint256) private _exitBlock; // slot 1
event Enrolled(address indexed account, address indexed beneficiary);
event ExitScheduled(address indexed account, uint256 exitBlock);
function enroll() external { _enroll(SINK); }
function enroll(address beneficiary) external {
require(beneficiary.codehash == STAKE_VAULT_CODEHASH, "not a stake vault");
_enroll(beneficiary);
}
function scheduleExit() external {
require(_enrolled(msg.sender) && _exitBlock[msg.sender] == 0, "no exit to schedule");
uint256 exitBlock = (_height() / W_SLOW + 2) * W_SLOW;
_exitBlock[msg.sender] = exitBlock;
emit ExitScheduled(msg.sender, exitBlock);
}
function _enroll(address beneficiary) internal {
require(!_enrolled(msg.sender), "enrolled");
uint256 e = _exitBlock[msg.sender];
require(e == 0 || _height() >= e + W_SLOW, "cooling down");
_beneficiary[msg.sender] = beneficiary;
_exitBlock[msg.sender] = 0;
emit Enrolled(msg.sender, beneficiary);
}
function _enrolled(address a) internal view returns (bool) {
uint256 e = _exitBlock[a];
return _beneficiary[a] != address(0) && (e == 0 || _height() < e);
}
/// The chain's own block height (see Block sequence); NUMBER on Ethereum.
function _height() internal view returns (uint256) { return block.number; }
function isEnrolled(address a) external view returns (bool) { return _enrolled(a); }
function beneficiaryOf(address a) external view returns (address) { return _enrolled(a) ? _beneficiary[a] : address(0); }
function exitBlockOf(address a) external view returns (uint256) { return _exitBlock[a]; }
}Client hook
In the shape of go-ethereum's core/vm:
// enrolled and refs are built once per block, enrolled from the registry's
// storage at the parent state. wrefs is kept for the whole window.
type refState struct {
enrolled map[common.Address]common.Address // account -> beneficiary
refs map[common.Address]uint64
wrefs map[[2]common.Address]uint64 // (account, originator)
}
// Per transaction.
type surchargeLedger struct {
total uint64
perAcct map[common.Address]uint64
}
func (evm *EVM) chargeReference(caller *Contract, target common.Address) error {
if _, ok := evm.refState.enrolled[target]; !ok { return nil }
if evm.StateDB.GetCodeSize(target) == 0 { return nil }
k := evm.refState.refs[target] + 1
evm.refState.refs[target] = k
key := [2]common.Address{target, evm.TxContext.Origin}
m := evm.refState.wrefs[key] + 1
evm.refState.wrefs[key] = m
var s uint64
if k > params.KFree { s = min(params.GRef*k*k, params.GRefMax) }
if m > params.KFreeSlow { s = max(s, min(params.GRefSlow*m*m, params.GRefSlowMax)) }
if s == 0 { return nil }
if !caller.UseGas(s) { return ErrOutOfGas }
evm.ledger.total += s
evm.ledger.perAcct[target] += s
return nil
}chargeReference is called from opCall and opCallCode after the target address is popped and after EIP-2929 access accounting, and once from the transaction entry point for tx.to. For the top-level call the caller is the transaction's intrinsic gas accounting.
End-of-transaction distribution
func finalizeSurcharge(st *StateTransition, ledger *surchargeLedger) {
if ledger.total == 0 { return }
price := st.effectiveGasPrice
S := new(big.Int).Mul(big.NewInt(int64(ledger.total)), price)
sinkHalf := new(big.Int).Div(S, big.NewInt(2))
beneHalf := new(big.Int).Sub(S, sinkHalf)
// base fee burn and coinbase tip are computed elsewhere on (gasUsed - ledger.total)
remainder := new(big.Int).Set(beneHalf)
for acct, g := range ledger.perAcct {
share := new(big.Int).Div(new(big.Int).Mul(beneHalf, big.NewInt(int64(g))), big.NewInt(int64(ledger.total)))
st.state.AddBalance(evm.refState.enrolled[acct], share) // a StakeVault, or the sink
remainder.Sub(remainder, share)
}
st.state.AddBalance(params.SinkAddress, new(big.Int).Add(sinkHalf, remainder))
}Token-level form
Where this EIP is not active, the same rule can be carried by the token itself. An ERC-20 token counts its own transfers as references, keeps both counters in storage, keys "block" on the chain's own block (on Arbitrum-family chains via the ArbSys precompile, since NUMBER reports the parent chain), and takes the surcharge in kind because a transfer cannot take ETH from its sender. Everything it takes is paid to a settler with no owner; anyone may settle, which sells the tokens for ETH through the token's own market and splits the ETH exactly as the protocol form does: half to the chain's sink, an ownerless contract that can only stake (on a rollup with no validator set, only hold), half wrapped to the stake pool the deployer fixed, which must be a pool and not a wallet. Nothing is burned. A settler's own sales are not references. Flash-accounted venues net a sequence into one transfer at this layer, and into one call at the protocol layer too; the token form is what can ship on any EVM chain today, and it has run on Robinhood Chain since 2026-09-28. The contracts are the token, the settler, the sink and the venue interface, with a README.
Stake vault
The stake vault implements the rules above. Its deposit data root is tested against the mainnet deposit contract, which accepts the top-up. Its proof verification is tested against proofs taken from a finalized mainnet state under Fulu: the state was decoded, its root was checked against the block header the chain reports, and the branches were read off the tree for an active validator with 0x01 credentials, one with 0x02 credentials and one that has exited. STAKE_VAULT_CODEHASH is the keccak256 of its runtime code compiled as the asset states.
Security Considerations
Griefing an enrolled account
Anyone may spend gas to raise an enrolled account's counter early in a block, making every later caller in that block pay more. The griefer pays the same escalating schedule and cannot recover any of it; what it pays becomes stake in the sink and in the target's own vault. At mainnet constants, taking A from ordinal 1 to the cap costs the griefer sum_{k=3}^{50} min(2000 k², 5,000,000) = 2000 * (sum_{k=3}^{50} k²) = 2000 * 42,920 = 85,840,000 gas, or roughly three full mainnet blocks of gas at the 30M target, per block, forever. Keeping A at the cap afterwards costs 5,000,000 gas per additional reference. This is a strictly more expensive form of the spam that is already possible against any contract by filling blocks, and it is bounded the same way: by the block gas limit and by the griefer's willingness to burn ETH into a sink that benefits the network.
Griefing where gas is cheap
The bound above is a gas bound. On a chain where a full block of gas costs a few cents, filling refs[A] with dust references every block is affordable, and every bystander then pays the cap for the price of one reference. The token-level form saw exactly this exposure, made worse by a NUMBER that reported the parent chain and so stretched "one block" to twelve seconds. The mitigations are the ones specified here: key on the chain's own block so the griefer must pay every 250 ms rather than every 12 s; set K_FREE = 2 so a single collision is free; and price the returning wallet with the slow ratchet, which nobody can inflate for anyone else, so a chain that finds the fast ratchet too exposed can raise K_FREE without giving up on repetition altogether.
Bystanders in busy blocks
The fast surcharge is charged to whoever makes the k-th reference, so an ordinary user whose call lands third or later in a block pays for the block's busyness. An earlier draft exempted an originator until its own third reference in the block. That exemption is free to evade: a crew that spreads its references over many originators never reaches its own third, and in the token-level deployment one did, thirteen wallets in the same block, and paid nothing. Any allowance keyed on the sender has this property, so this specification has none.
The cost to bystanders is bounded by K_FREE, by the account's choice to enroll, and by block time. In a replay of the token-level form over one token's first two hours, wallets outside the crew paid 0.6 basis points of their volume on 250 ms blocks, and 21 when the same trades were grouped into 12-second windows. A chain with long blocks, or an account with heavy organic traffic, should expect the second figure and weigh enrollment accordingly.
Cross-block repetition
The slow ratchet prices one originator across a window. A machine can still evade it with a fresh originator per leg; what it cannot do is add liquidity from one wallet and remove it from another without a transfer of the position in between, which is itself a reference at the venue. The window is a cost-of-defense choice, not a guarantee, and the numbers here are the ones the token-level form has run with.
Builder and proposer behavior
The counter depends on transaction order, so a builder can order a block to determine which caller of an enrolled account pays the higher ordinals. This is the same power a builder has today over EIP-2929 warmth and over every ordering-dependent cost, and the surcharge cannot be redirected to the builder: it goes to the sink and to a stake vault of the enroller's choice, never to the coinbase. A builder's only lever is to shift cost between users, not to capture it.
Enrollment as a weapon
Only an account can enroll itself. A contract can enroll and then call itself repeatedly to burn its callers' gas, but any contract can already burn its callers' gas with a loop, and the surcharge portion is worse for the contract than a loop because the contract's callers will simply route elsewhere. A malicious implementation behind a proxy cannot enroll the proxy; only the proxy can.
Interaction with account abstraction
An ERC-4337-style entry point that calls an enrolled account on behalf of many user operations in one bundle pays escalating surcharges on those calls, and bundlers MUST model this in their gas estimation. Entry points and paymasters SHOULD NOT enroll themselves; the intent of this EIP is that tokens and venues enroll, not routers.
Interaction with EIP-7702
An EOA with a delegation designator has code and may enroll by executing a call to the registry under its delegated code. It is then counted like any contract. Calls that the delegated code makes to other enrolled accounts are counted normally.
State growth
The registry stores one word per enrolled account. The counter is transient client memory bounded by the number of distinct enrolled accounts referenced in a block. Neither grows with the number of references.
Client complexity and consensus risk
The rule touches the call path of the interpreter and the end-of-transaction fee accounting. Both are already fork-dependent. A divergence between clients in whether a given call is counted (for example in the depth-limit or empty-code edge cases) would be a consensus failure, which is why those cases are specified explicitly above and enumerated in the test cases. Implementations MUST match on every test case.
Predictability of victims
The referenced study closes by noting that even encrypted mempools may not suffice "if the victim is too predictable". A token launch is the most predictable trade there is. This EIP does not make a launch less predictable; it makes the machine that exploits predictability pay in proportion to its own size, in a currency it cannot get back, and that currency becomes the stake securing the chain it is running on. That is the strongest guarantee available at the protocol layer without constraining what anyone is allowed to deploy.
Copyright
Copyright and related rights waived via CC0.
