Asset-level spending limits and recovery: should rate limiting live in the asset rather than the account?

Most key-compromise defenses today (smart wallets, ERC-7579 modules, social recovery, EIP-7702 delegation) enforce policy in the account’s code. ERC-8403 continues this: spend limits and unlock times are authorities committed under the account’s root.

I’d like to probe a different placement: enforcing rate limits at the asset: the token contract for ERC-20s, and eventually the protocol itself for native ETH.

Motivation. Account-level policy only binds while the account’s code is in the transfer path. Two gaps:

  1. Allowances: a contract holding an existing transferFrom approval moves tokens without re-entering the wallet’s validation logic, so a wallet-level daily limit does not bind.
  2. Delegation replacement: under EIP-7702, the compromised EOA key can re-delegate and strip the policy that was supposed to contain it.

An asset-level check binds every balance-reducing path (transfer, transferFrom, burns) regardless of which application initiated it, and survives delegation changes.

Sketch (details negotiable, that is the point of the thread):

  • Optional per-account spending limit (rolling window) enforced where balances change.
  • Security-weakening changes (raising or removing a limit, replacing a recovery address) take effect after a delay of about 14 days; security-strengthening changes are immediate.
  • An irreversible declareCompromised() state that blocks ordinary outflows, including spends via existing allowances.
  • A pre-registered recovery address (a “savior”) that gains authority only in the compromised state and can sweep remaining funds. It is just an address, so it can be a contract verifying post-quantum signatures, which puts the expensive key on the rarely used path and the cheap everyday key behind the rate limit. New-savior registration is itself delayed, so an attacker cannot register then sweep.
  • For ERC-20s this needs no protocol change: either a shared registry contract holding savior and compromised state that participating tokens consult, or per-token state. Native ETH enforcement would need something like a system contract consulted on balance decrease, which is a much larger ask, and maybe never justified.

Native ETH: system contract

A system contract in the EIP-7002 pattern at a fixed address. Configuration is an ordinary signed transaction from the account; the protocol consults the contract whenever ETH leaves the account:

interface IAccountSecurity {
    // decrease: immediate; increase or removal: 14-day delay
    function setSpendingLimit(uint256 amountPerPeriod, uint64 period) external;

    // irreversible by the primary key
    function declareCompromised() external;

    // activation delayed 14 days; a pending savior never activates if
    // the account is declared compromised first
    function setCompromisedSavior(address savior) external;

    // callable by the registered savior, only in compromised state
    function saviorSweep(address account, address destination, uint256 amount) external;
}

Example calls below use readable shorthand in data, not real ABI encoding. Configuring a limit:

{
  "jsonrpc": "2.0",
  "method": "eth_sendTransaction",
  "params": [{
    "from": "0xYourAccount",
    "to": "0xAccountSecuritySystemContract",
    "data": "setSpendingLimit(1 ETH, 24 hours)"
  }],
  "id": 1
}

Registering a savior (any address: an EOA, a multisig, or a contract verifying post-quantum signatures):

{
  "jsonrpc": "2.0",
  "method": "eth_sendTransaction",
  "params": [{
    "from": "0xYourAccount",
    "to": "0xAccountSecuritySystemContract",
    "data": "setCompromisedSavior(0xSaviorAccount)"
  }],
  "id": 1
}

Declaring compromise (blocks ordinary outflows immediately, irreversible by the primary key):

{
  "jsonrpc": "2.0",
  "method": "eth_sendTransaction",
  "params": [{
    "from": "0xYourAccount",
    "to": "0xAccountSecuritySystemContract",
    "data": "declareCompromised()"
  }],
  "id": 1
}

Recovery sweep, signed by the savior rather than the primary key, and not restricted by the normal spending limit:

{
  "jsonrpc": "2.0",
  "method": "eth_sendTransaction",
  "params": [{
    "from": "0xSaviorAccount",
    "to": "0xAccountSecuritySystemContract",
    "data": "saviorSweep(0xYourAccount, 0xSafeAccount, 9 ETH)"
  }],
  "id": 1
}

ERC-20: token-level enforcement

No protocol change needed. The token enforces the limit at every balance decrease (transfer, transferFrom, burns), so an existing allowance cannot bypass it:

interface IERC20SecurityExtended is IERC20 {
    function setSpendingLimit(uint256 amount, uint64 period) external;
    function saviorTransfer(address account, address destination, uint256 amount) external returns (bool);
}

Savior and compromised state can live per-token, or be read from a shared audited registry so a user registers once per chain:

interface IAccountSecurityRegistry {
    function setCompromisedSavior(address savior) external;
    function declareCompromised() external;
    function saviorOf(address account) external view returns (address);
    function isCompromised(address account) external view returns (bool);
}

Open questions I would value input on:

  1. Is there prior work on asset-side (rather than account-side) rate limiting I should know about? I found guardian and module designs (Argent, ERC-7579) and timelocks, but they all sit account-side.
  2. Does the allowance bypass argument hold up, or are there wallet-side mitigations that make asset-side enforcement redundant in practice?
  3. The compromised state is deliberately irreversible by the primary key. Is griefing (an attacker freezes your account) an acceptable trade for theft prevention, given a configured savior?
  4. For native ETH: is there any enforcement point short of consensus-layer changes that binds all value flows, or is this intrinsically an application-layer feature?
  5. Interaction with EIP-8141 frame transactions and native account abstraction: per-transfer limit checks add validation cost exactly where mempool admission is already the bottleneck (cf. the MATCHA and frame-transaction admission threads). Is a storage-touching limit check even compatible with scalable mempool validation?

Fragmentation is the obvious cost: per-token security state, inconsistent UX, and a registry becoming a systemic dependency. I am not convinced asset-level is strictly better. I am asking whether the enforcement-point distinction is real and worth the complexity.