--- eip: 8140 title: Token Gas Payments for EIP-8130 description: Enable gas payments using fungible tokens for EIP-8130 AA transactions via the fee_token field author: Chris Hunter (@chunter-cb) discussions-to: https://ethereum-magicians.org/t/eip-8130-account-abstraction-by-account-configurations/25952 status: Draft type: Standards Track category: Core created: 2025-10-14 requires: 8130 --- ## Abstract This proposal extends [EIP-8130](./eip-8130.md) to enable gas payments using ERC-20 tokens. It defines the token payment format for EIP-8130's `fee_token` field, a Token Payment Registry for allowlisting tokens, and an optional native payer system contract for chain-operated gas abstraction with oracle-based pricing. ## Motivation EIP-8130 provides account abstraction with ETH-based gas sponsorship. However, many users hold tokens (USDC, USDT, etc.) but no ETH, creating friction for onboarding and transaction submission. Token gas payments enable: - **Gasless UX**: Users transact with tokens they already hold - **Stablecoin payments**: Predictable transaction costs in USD-denominated tokens - **Competitive market**: Permissioned payers compete on exchange rates off-chain - **Chain flexibility**: Native payer option for chain-operated gas abstraction with oracle-based pricing - **Zero token changes**: Works with existing ERC-20 tokens using standard transfer logic—no upgrades or storage migrations required This proposal uses EIP-8130's `fee_token` field to add token payment capability without modifying the base transaction type. ## Specification ### Constants | Name | Value | Comment | |------|-------|---------| | `TOKEN_PAYMENT_REGISTRY` | TBD | Token Payment Registry system contract address | | `TOKEN_TRANSFER_COST` | 13000 | Token escrow/refund gas (see breakdown below) | | `DEACTIVATION_DELAY` | TBD | Blocks before token deactivation takes effect | | `PAUSE_COST` | TBD | ETH required to invoke emergency pause | | `NATIVE_PAYER` | TBD | (Optional) Native gas AMM payer system contract address | ### Fee Token Format Token payments use the EIP-8130 `fee_token` field with packed byte encoding: | Length | Format | Description | |--------|--------|-------------| | 0 bytes | `0x` (empty) | ETH payment only | | 52 bytes | `token (20) \|\| max_exchange_rate (32)` | Token gas payment | ``` fee_token = token_address || max_exchange_rate = address (20 bytes) || uint256 (32 bytes) = 52 bytes total ``` | Field | Type | Size | Description | |-------|------|------|-------------| | `token_address` | address | 20 bytes | ERC-20 token contract address | | `max_exchange_rate` | uint256 | 32 bytes | Maximum tokens per ETH the sender will accept, in token's smallest unit (e.g., 2000 USDC/ETH = `2000 × 10^6`) | The `max_exchange_rate` is denominated in the token's native decimals per ETH, making it oracle-decimal-agnostic. The sender does NOT commit to a specific payer in `fee_token` — payer commitment uses EIP-8130's extended payer modes (see [Payer Modes with Token Payments](#payer-modes-with-token-payments)). When `fee_token` is non-empty, the transaction uses token-based gas payment instead of ETH. ### Token Payment Registry Tokens must be registered in the Token Payment Registry system contract at `TOKEN_PAYMENT_REGISTRY` before use for gas payment. Similar to EIP-8130's account configuration precompile, this is a protocol-level system contract with persistent storage. The registry stores token metadata required for protocol-level balance reads and transfers. The registry defines a single protocol-level interface that all wallets and payers integrate with. Its governance model for write access (token registration, pause, blocklist) is implementation-defined — chain-operated, permissionless, or hybrid — and can be extended by the deployer. The protocol reads from the registry identically regardless of governance model. #### Token Configuration Storage All storage keys use byte concatenation (`||` denotes concatenation of raw bytes, addresses as 20 bytes, strings as UTF-8): ``` Base slot: keccak256(token_address || TOKEN_PAYMENT_REGISTRY || "config") - base_slot + 0: balance_slot_index (uint256) - base_slot + 1: packed config (uint256) - byte 0: token_decimals (uint8) - byte 1: active (bool) - byte 2: paused (bool) - bytes 3-31: reserved - base_slot + 2: deactivation_block (uint256) — 0 if no pending deactivation ``` | Field | Description | |-------|-------------| | `balance_slot_index` | Storage slot index for token's balance mapping. User balance at: `keccak256(user_address \|\| balance_slot_index)` in the token contract | | `token_decimals` | Token decimal places (e.g., 6 for USDC, 18 for most tokens) | | `active` | Whether token is currently accepted for gas payments | | `paused` | Whether token is globally paused (emergency, instant) | | `deactivation_block` | Block number at which token becomes inactive. Set by `requestDeactivation`, takes effect after `DEACTIVATION_DELAY` blocks | #### Blocklist Blocklist state is managed directly in the TOKEN_PAYMENT_REGISTRY rather than read from individual token contracts. This decouples the protocol from token storage internals and provides a single, predictable location for compliance checks: ``` Blocklist slot: keccak256(token_address || account_address || TOKEN_PAYMENT_REGISTRY || "blocklist") Value: bool (true = blocked) ``` Token issuers or designated admins call `setBlocked(token, account, blocked)` on the registry to maintain the blocklist. For tokens like USDC, an admin or keeper bot monitors the token's native `Blacklisted` events and mirrors them to the registry. The number of blocklisted addresses is typically small (hundreds for major stablecoins — OFAC sanctions, stolen funds, etc.), making this approach practical. #### Deactivation and Emergency Pause Token removal and emergency pause use separate mechanisms: **Deactivation** (planned removal, timelocked): `requestDeactivation(token)` sets `deactivation_block = current_block + DEACTIVATION_DELAY`. The token remains active until that block, giving pending transactions time to clear. `cancelDeactivation(token)` resets the countdown. Activation via `activateToken(token)` takes effect immediately — enabling a token does not invalidate transactions. **Emergency pause** (instant, costs ETH): `setTokenPaused(token, true)` takes effect immediately and requires `msg.value >= PAUSE_COST`. The ETH cost discourages frivolous or spam pauses while preserving the ability to respond to genuine emergencies (e.g., a token exploit). Unpausing is free and immediate. ### Payer Modes with Token Payments This extension leverages EIP-8130's payer marker in the sender signature hash to add token payment capability. EIP-8130 defines the marker as `0x80` (self-pay) or `0x00` (any sponsor). This EIP extends the marker to accept a 20-byte payer address, enabling the sender to commit to a specific payer without modifying `fee_token`: | Sender Signs (marker) | `payer_auth` (wire) | Gas Payer | Token Recipient | Description | |------------------------|---------------------|-----------|-----------------|-------------| | `0x80` (empty) | MUST be empty | `from` (ETH only) | — | Self-pay; `fee_token` MUST be empty | | `0x00` | 65-byte K1 signature | ecrecover(sig) | Payer | Any permissioned payer; payer signs tx | | `payer_address` (20 bytes) | 65-byte sig (must match) | Committed payer | Payer | Specific permissioned payer | | `NATIVE_PAYER` (20 bytes) | MUST be empty | `NATIVE_PAYER` | `NATIVE_PAYER` | Native payer (chain-operated) | The RLP encoding naturally disambiguates: 0 bytes = self-pay, 1 byte (`0x00`) = any sponsor, 20 bytes = specific payer address. **Permissioned payers** sign each transaction, explicitly agreeing to the `max_exchange_rate`. They can use either `0x00` (any willing signer) or a 20-byte address (specific signer). The `NATIVE_PAYER` address is used directly as the marker for native payer mode. No permissionless (unsigned) third-party payers are supported — see [Security Considerations](#security-considerations) for rationale. **Updated sender signature hash** (extends [EIP-8130 Signature Payload](./eip-8130.md#signature-payload)): ``` // Self-pay (from pays ETH): keccak256(AA_TX_TYPE || rlp([..., fee_token, 0x80])) // Any permissioned sponsor: keccak256(AA_TX_TYPE || rlp([..., fee_token, 0x00])) // Specific payer (permissioned or native): keccak256(AA_TX_TYPE || rlp([..., fee_token, payer_address])) ``` ### Native Payer The optional `NATIVE_PAYER` system contract provides chain-operated gas abstraction with oracle-based pricing. This is the only mechanism for unsigned (non-permissioned) token gas payments. #### Storage ``` Per-token oracle config: Token slot: keccak256(token_address || NATIVE_PAYER || "oracle") - token_slot + 0: oracle_address (address) - token_slot + 1: oracle_slot (bytes32) - token_slot + 2: oracle_decimals (uint8) ``` Chain operators configure accepted tokens and their oracle feeds via the `NATIVE_PAYER` contract. Only tokens registered in the Token Payment Registry AND configured in the native payer can be used with native payer mode. #### Oracle-Based Pricing The native payer reads exchange rates from chain-operator-configured oracles. The oracle value represents **tokens per ETH**, scaled by `10^oracle_decimals`. The protocol normalizes the oracle value to the token's native units per ETH for comparison with `max_exchange_rate`: ``` oracle_value = SLOAD(native_payer.oracle_address, native_payer.oracle_slot) exchange_rate = oracle_value * 10^token_decimals / 10^oracle_decimals token_cost = ceil(gas_cost_wei * exchange_rate / 10^18) ``` Both `exchange_rate` and `max_exchange_rate` are in the same units: token's smallest unit per ETH. Validation requires `exchange_rate <= max_exchange_rate`. **Example**: USDC payment for 0.001 ETH gas cost (1e15 wei), ETH at $2000: - `oracle_value` = 2000 × 10^8 (Chainlink 8-decimal standard) - `exchange_rate` = 2000e8 × 10^6 / 10^8 = 2000e6 (= 2000 USDC per ETH in USDC units) - `max_exchange_rate` = 2100e6 (sender accepts up to 2100 USDC/ETH) - `token_cost` = 1e15 × 2000e6 / 1e18 = 2e6 = 2 USDC If oracle returns 0, `token_cost` is 0 (chain operator assumes this risk). #### Oracle Maintenance Oracle maintenance is the chain operator's responsibility. Common approaches: | Oracle Type | Description | |-------------|-------------| | **Chainlink feeds** | Decentralized price updates via established infrastructure | | **Internal feed** | Chain operator runs an updater service | | **DEX-integrated** | Oracle contract queries configured on-chain liquidity pool | The oracle rate is read once and locked at the start of each AA transaction (see [Exchange Rate Resolution](#exchange-rate-resolution)). Stale prices are the chain operator's risk. #### Behavior - Uses the Token Payment Registry for token allowlisting and metadata - Chain operators define supported tokens and oracle feeds - Referenced via the sender's payer marker: sender signs `NATIVE_PAYER` address as the 20-byte marker, `payer_auth` is empty - Chain operators may implement custom pricing logic (AMMs, fixed rates, or custom curves) within the native payer contract ### Token Transfer Flow Token transfers use a two-sided escrow/refund model. The exchange rate is resolved and locked once at the start of transaction execution — it does not change during the transaction. #### Exchange Rate Resolution The exchange rate is determined by payer type and locked at the start of AA transaction execution: | Payer Type | Exchange Rate Source | Meaning of `max_exchange_rate` | |------------|---------------------|-------------------------------| | **Permissioned** (65-byte sig) | `max_exchange_rate` directly | Mutually agreed rate (both parties signed over it) | | **Native** (`NATIVE_PAYER`) | Chain-operator oracle | Cap — oracle rate MUST NOT exceed `max_exchange_rate` | `max_exchange_rate` serves the same role for token pricing that `max_fee_per_gas` serves for ETH gas pricing — it is the sender's maximum willingness-to-pay. For native payer transactions, nodes SHOULD reject transactions where `max_exchange_rate < current_oracle_rate`, just as nodes reject transactions where `max_fee_per_gas < base_fee`. Wallets SHOULD set `max_exchange_rate` with a buffer above the current oracle rate (e.g., 10%) to avoid premature invalidation during normal price movement. #### Pre-Execution Escrow 1. Parse `fee_token` to extract `token_address` and `max_exchange_rate` 2. Resolve exchange rate (locked for the duration of this tx): - Permissioned payer: `exchange_rate = max_exchange_rate` (mutually agreed) - Native payer: `exchange_rate = oracle_rate`. If `oracle_rate > max_exchange_rate`, transaction fails 3. Verify token is not paused (registry `paused` flag) 4. Verify sender is not blocklisted (registry blocklist) 5. Compute `max_token_cost = ceil(gas_limit × max_fee_per_gas × exchange_rate / 10^18)` 6. **Token escrow**: Deduct `max_token_cost` from sender's token balance (sender tokens on hold) 7. **ETH escrow**: Deduct `gas_limit × max_fee_per_gas` from payer's ETH balance (payer ETH on hold, per EIP-8130) #### Post-Execution Settlement After EVM execution completes (regardless of revert): 1. Compute `actual_gas_cost = gas_used × effective_fee_per_gas` 2. Compute `actual_token_cost = ceil(actual_gas_cost × exchange_rate / 10^18)` 3. **Token settlement**: Transfer `actual_token_cost` from escrow to payer; refund `max_token_cost - actual_token_cost` back to sender 4. **ETH settlement**: Refund `(gas_limit × max_fee_per_gas) - actual_gas_cost` back to payer (per EIP-8130) All token operations occur outside EVM execution via direct storage manipulation. ### Intrinsic Gas When `fee_token` is non-empty, add to EIP-8130 intrinsic gas: ``` intrinsic_gas = EIP8130_intrinsic_gas + TOKEN_TRANSFER_COST ``` `TOKEN_TRANSFER_COST` (~13,000 gas) covers the protocol's direct storage operations for the escrow/refund flow: | Operation | Gas | Notes | |-----------|-----|-------| | Sender balance read (cold SLOAD) | 2,100 | From token contract storage | | Sender balance write (warm SSTORE) | 2,900 | Non-zero → non-zero | | Payer balance read (cold SLOAD) | 2,100 | From token contract storage | | Payer balance write (warm SSTORE) | 2,900 | Non-zero → non-zero | | Oracle read (cold SLOAD) | 2,100 | From oracle contract storage (native payer only; 0 for permissioned) | | Blocklist + pause check | ~200 | Registry storage (warm — same contract as token config reads) | | Post-execution refund (2× warm SSTORE) | ~800 | Both balance slots already warm | | **Total** | **~13,000** | | ### Validation Flow Updates Extend EIP-8130 mempool acceptance: 1. (EIP-8130 steps 1-6: signature, nonce, etc.) 2. If `fee_token` is non-empty (52 bytes): - Parse `fee_token`: extract `token_address`, `max_exchange_rate` - Verify token is registered and active in Token Payment Registry - Verify token is not paused (registry `paused` flag) - Verify sender is not blocklisted (registry blocklist) - Resolve payer from sender's signed marker and `payer_auth`: - Marker = empty (`0x80`): **REJECT** (self-pay not valid with token payments) - Marker = `0x00`: recover payer via ecrecover from `payer_auth` (any permissioned payer) - Marker = 20-byte address with 65-byte `payer_auth`: specific permissioned payer (ecrecover must match marker) - Marker = `NATIVE_PAYER`: `payer_auth` MUST be empty; verify native payer has oracle config for token - For native payer: read oracle exchange rate; **REJECT** if `max_exchange_rate < current_oracle_rate` - For permissioned payer: use `max_exchange_rate` directly (mutually agreed rate) - Compute `max_token_cost = ceil(gas_limit × max_fee_per_gas × exchange_rate / 10^18)` - Verify sender token balance ≥ `max_token_cost` 3. (EIP-8130 remaining steps: nonce, balance, expiry) Nodes SHOULD treat `max_exchange_rate` the same way they treat `max_fee_per_gas`: reject transactions where the cap is below the current rate. For native payer transactions, this means `max_exchange_rate >= current_oracle_rate`. Transactions that were valid but become invalid due to oracle rate increases are evicted from the mempool, the same as transactions evicted when `base_fee` rises above `max_fee_per_gas`. ### Block Execution Updates Extend EIP-8130 block execution: 1. **Exchange rate lock** (if `fee_token` non-empty): Read and lock exchange rate for this tx 2. **Token escrow** (if `fee_token` non-empty): Deduct `max_token_cost` from sender 3. **ETH escrow**: Deduct `gas_limit × max_fee_per_gas` from payer (per EIP-8130) 4. Process authorization_list (EIP-7702) 5. Account initialization (if applicable) 6. Apply key changes (if applicable) 7. Deliver calldata to sender via self-call 8. **Token settlement** (if `fee_token` non-empty): Transfer `actual_token_cost` to payer, refund excess to sender 9. **ETH settlement**: Refund unused ETH gas to payer (per EIP-8130) Steps 1–6 and 8–9 are protocol-level direct state operations (no EVM execution). Step 7 is the single EVM execution frame. Token escrow (step 2) ensures the sender cannot move tokens during execution; settlement (step 8) transfers the actual cost. If step 7 reverts, token and ETH settlement still complete (consistent with EIP-8130's revert semantics). ### Transaction Context Extension Extend EIP-8130's `IAccountConfig` with: ```solidity // Transaction context (only valid during AA transaction execution) function getGasPaymentInfo() external view returns (address token, uint256 amount); ``` Returns the token address and amount transferred for gas payment. Returns `(address(0), 0)` for ETH-only payments. ## Rationale ### Why `max_exchange_rate` Instead of `max_amount`? The sender specifies a maximum exchange rate (tokens per ETH) rather than an absolute token amount: 1. **Scales naturally**: The actual token cost scales with gas used — no over-escrow beyond what the gas limit requires 2. **Rate-centric**: Users think in terms of "what rate am I paying?" not "what absolute amount might be deducted" 3. **Oracle-comparable**: Directly comparable to the oracle's exchange rate without additional conversions 4. **Simpler escrow**: `max_token_cost = gas_limit × max_fee_per_gas × max_exchange_rate / 10^18` — deterministic from tx fields 5. **`max_fee_per_gas` analogy**: `max_exchange_rate` is to the oracle rate what `max_fee_per_gas` is to `base_fee` — a sender-specified ceiling that determines validity. Wallets set both with headroom above the current value; transactions become invalid when the underlying rate exceeds the cap. This is a familiar pattern for mempool nodes and wallet implementations. ### Why Payer Commitment via Signature Marker? EIP-8130 already includes a payer marker in the sender's signature hash (`0x80` for self-pay, `0x00` for any sponsor). Extending this to accept a 20-byte payer address lets the sender commit to a specific payer without modifying `fee_token`. This prevents **payer attachment attacks** where an attacker replaces the intended payer with one offering worse rates. The sender's `max_exchange_rate` provides a hard cap, but the payer marker provides the primary protection by binding the sender's signature to a specific payer. ### Why No Permissionless Third-Party Payers? Oracle-based pricing without a per-transaction signature creates a mass invalidation attack surface. A permissionless payer registers an oracle, and the mempool fills with transactions referencing that oracle. If the oracle price spikes (maliciously or organically), all pending transactions where `oracle_rate > max_exchange_rate` become invalid simultaneously. Worse, an attacker can register multiple Sybil payer identities pointing to the same oracle, circumventing per-payer pending limits and amplifying the blast radius. Oracle-based pricing is instead scoped to the **native payer** — a chain-operated system contract where the chain operator controls the oracle feed. This provides a single trusted operator with aligned incentives, no Sybil risk, and a simple invalidation model. Competitive pricing is achieved through **permissioned payers** who sign each transaction, agreeing to a specific `max_exchange_rate` — no oracle dependency in the mempool. ### Why Direct Storage Reads for Balances? Reading token balances directly from storage slots rather than calling ERC-20 methods: 1. **No token upgrades**: Existing tokens with standard `mapping(address => uint256)` balance storage work without modification—just register the balance slot index 2. **No storage migrations**: Tokens don't need to move data or conform to new interfaces 3. **Atomic execution**: Balance reads and transfers happen at protocol level, before and after EVM execution (escrow/refund model) Blocklist and pause state are managed in the TOKEN_PAYMENT_REGISTRY rather than read from token contracts. This decouples the protocol from token storage internals—each token's blocklist implementation is different (high-bit flags, separate mappings, struct fields), but the registry provides a single, predictable interface. Designated admins mirror token issuer blocklist events to the registry. ## Backwards Compatibility This proposal extends EIP-8130. Nodes not supporting this EIP will reject transactions with non-empty `fee_token` as specified in EIP-8130. ## Reference Implementation ### ITokenPaymentRegistry ```solidity interface ITokenPaymentRegistry { event TokenRegistered(address indexed token, uint256 balanceSlotIndex, uint8 tokenDecimals); event TokenActivated(address indexed token); event DeactivationRequested(address indexed token, uint256 effectiveBlock); event DeactivationCancelled(address indexed token); event TokenPaused(address indexed token, bool paused); event BlocklistUpdated(address indexed token, address indexed account, bool blocked); event BlocklistAdminUpdated(address indexed token, address indexed admin, bool authorized); // Token registration — idempotent, calling on an already-registered token updates its config function registerToken(address token, uint256 balanceSlotIndex, uint8 tokenDecimals) external; // Activation (immediate) and deactivation (timelocked by DEACTIVATION_DELAY) function activateToken(address token) external; function requestDeactivation(address token) external; function cancelDeactivation(address token) external; // Emergency pause — pausing requires msg.value >= PAUSE_COST function setTokenPaused(address token, bool paused) external payable; // Blocklist management (token admin or designated blocklist admin) function setBlocklistAdmin(address token, address admin, bool authorized) external; function setBlocked(address token, address account, bool blocked) external; // View functions — isTokenActive returns false if paused or past deactivation_block function isTokenActive(address token) external view returns (bool); function isBlocked(address token, address account) external view returns (bool); function getTokenConfig(address token) external view returns ( uint256 balanceSlotIndex, uint8 tokenDecimals, bool active, bool paused, uint256 deactivationBlock ); } ``` ### INativePayer ```solidity interface INativePayer { event OracleConfigured(address indexed token, address oracleAddress, bytes32 oracleSlot, uint8 oracleDecimals); event OracleRemoved(address indexed token); // Oracle configuration (chain operator only) function configureOracle(address token, address oracleAddress, bytes32 oracleSlot, uint8 oracleDecimals) external; function removeOracle(address token) external; // View functions function getOracleConfig(address token) external view returns (address oracleAddress, bytes32 oracleSlot, uint8 oracleDecimals); function getExchangeRate(address token) external view returns (uint256); } ``` ## Security Considerations **Token Spend Protection**: Token gas payments occur before wallet code executes, bypassing wallet-level spend limits. Mitigations: - **`max_exchange_rate` cap**: Hard limit on the exchange rate per transaction; combined with `max_fee_per_gas` and `gas_limit`, this bounds maximum token spend - **`requiresSponsor` policy** (EIP-8130): Accounts can enable the `requiresSponsor` flag in their gas payment policy, which disables ALL self-paid gas (ETH and tokens). When enabled, transactions must have a sponsor signature in `payer_auth`. This completely eliminates the gas spend attack vector and is strongly recommended for accounts using EVM-layer multisig. - **Blocklist checks**: Protocol reads the registry-managed blocklist before escrow, ensuring blocklisted accounts cannot spend tokens on gas **Escrow/Griefing Prevention**: The two-step escrow model (deduct `max_token_cost` pre-execution, refund excess post-execution) prevents the sender from transferring tokens away during EVM execution. Without escrow, the sender could drain their token balance in `calldata` execution, causing the post-execution token deduction to fail while the payer still pays ETH gas. **Oracle Risks**: Oracle-based pricing is restricted to the chain-operated native payer. The oracle rate is read once and locked at the start of AA transaction execution. If another transaction in the same block updates the oracle, the new rate is used — but the sender's `max_exchange_rate` cap protects against spikes. Stale oracle prices are the chain operator's risk. **Payer Security**: Permissioned payers sign each transaction, explicitly agreeing to the `max_exchange_rate`. When the sender uses `0x00` (any sponsor), only permissioned payers who sign can attach. The native payer requires the sender to sign `NATIVE_PAYER` as the 20-byte marker. In all cases, `max_exchange_rate` provides a hard cap on token cost, preventing payer attachment attacks with worse rates. **Mass Invalidation Prevention**: Multiple layers protect against mass mempool invalidation: - **No permissionless third-party payers**: Oracle-based payers where anyone can register create a Sybil-amplified invalidation vector. Oracle pricing is restricted to the chain-operated native payer. - **Timelocked deactivation**: `requestDeactivation` starts a `DEACTIVATION_DELAY` countdown, giving pending transactions time to clear. Mempool nodes can stop accepting new transactions for a token with a pending deactivation. - **Paid emergency pause**: `setTokenPaused` requires a non-zero ETH payment (`PAUSE_COST`), making frivolous or spam pauses economically costly. Emergency pauses may still invalidate pending transactions, but the ETH cost bounds abuse. - **Permissioned payer rate lock**: Permissioned payers sign each transaction with an agreed `max_exchange_rate`, fixing the rate at signature time with no oracle dependency in the mempool. **Balance Manipulation**: Protocol reads token balances directly from storage slots. Tokens with non-standard balance storage (rebasing, fee-on-transfer) may behave unexpectedly. Only register well-understood tokens. **Balance Slot Changes**: If a registered token upgrades and changes its balance storage slot, the registry configuration becomes stale. The admin can call `registerToken` again to update the config atomically (no deactivation gap). However, tokens used for gas payment should have immutable balance storage layouts — frequent slot changes indicate an unstable token that should not be registered. **Blocklist Sync**: The registry-managed blocklist may lag behind a token issuer's native blocklist by a few blocks. In the worst case, a newly-blacklisted address could spend a small amount of tokens on gas before the registry is updated. This is an acceptable tradeoff for the simplicity of not coupling to each token's internal storage layout. Designated admins should monitor token blocklist events and mirror updates promptly. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).