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:
- Allowances: a contract holding an existing
transferFromapproval moves tokens without re-entering the wallet’s validation logic, so a wallet-level daily limit does not bind. - 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:
- 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.
- Does the allowance bypass argument hold up, or are there wallet-side mitigations that make asset-side enforcement redundant in practice?
- 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?
- 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?
- 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.