ERC TBD: Portable Spend Grants

Hi all,

This is the discussion thread for a new ERC, Portable Spend Grants. I’ll open the ERCs PR as soon as this thread has a URL I can put in discussions-to.

Vectors, a compact Solidity reference, and an ERC-7730 display descriptor are in the same branch under assets/erc-draft_spend_grants/.

Why

If you’ve built with agents or session keys, you’ve probably needed this: “this key can spend up to X of these assets for a while, and I can kill it.” ERC-20 approve doesn’t fit, since it usually covers one token with no expiry and no time window. Calendar-day caps don’t fit either. They reset at midnight, so a delegate can spend the full cap at 11:59 and again at 12:01.
This draft defines that one object. The signed type is SpendGrant.

What gets signed

The principal signs:

  • who may act (the delegate)
  • which assets (native currency is the ERC-7528 0xEeee…EEeE address; everything else is ERC-20)
  • per-call, trailing-window, and lifetime caps for each asset
  • who may receive funds
  • a validity range

The digest is bound to one chain and one registry. The registry records remaining usage and revocation, and it never moves tokens. Whatever contract actually transfers value has to call consume in the same transaction, or the spend doesn’t count.

For example: “on this chain, the agent may spend up to 1000 USDC and 0.5 WETH per trailing 24 hours.” That’s a single signature and a single hash, and you revoke it once.

The window looks back windowSeconds from now. It isn’t “today.” A cap of zero means zero, never unlimited.

“Portable” refers to the terms: any wallet or tool can hash, render, and check them. It doesn’t mean the grant itself moves. Using it on another chain, registry, or executor needs a new signature.

These two roles are easy to mix up. The delegate is whoever the principal named in the grant. The executor is the one contract allowed to call consume, and it’s set immutably on the registry. So you choose the executor by choosing which registry to sign against.

Three design choices

  • Each asset has its own budget. assetCombine is in the signed type, but only 0 is valid for now. A shared budget across assets would be useful, but people budget in dollars, and that needs a price reference this draft doesn’t define. I kept the field so a later mode won’t change the type hash.
  • No rendering hash. There’s a canonical plain-text rendering, but it’s built entirely from signed fields, so hashing it into the grant wouldn’t commit to anything new.
  • Exact rolling window at flat cost. The reference keeps each asset’s debits in a 1024-slot ring with a running sum and drops expired debits from the front. Execution gas through the reference executor (excluding the 21k base and calldata) is about 161k for the first spend on a grant, about 89k after that regardless of how many debits are live, and about 70k once the ring has wrapped. The 1024 limit belongs to the reference, not to the signed grant.

Try it

It’s deployed on Arc testnet (chain 5042002) with verified source:

One Arc-specific catch: list USDC by its token address 0x3600…0000, not the native address. On Arc they’re the same balance, so listing both gives you two separate budgets over one pool of funds.

There’s an ERC-7730 descriptor for that registry, so a wallet can show the delegate, the recipient, the window, each asset’s caps in that asset’s units, and the validity dates. It rejects any assetCombine other than 0. It can’t show executor(), so the wallet still has to look that up on its own.

The rolling window is fuzzed against a naive model, including full-ring wraparound. Halmos proves the expiry rule, the cap checks at full 256-bit width, two-spend sequences, revocation, and executor-only consume.

The reference is unaudited, so please keep it on testnets.

How this relates to other ERCs

  • ERC-7710 / 7715. 7710 covers redeeming a delegation, and 7715 is the wallet RPC for requesting permissions. 7710 leaves the permission bytes opaque, and this draft is one typed meaning those bytes could carry. It doesn’t require 7710.
  • ERC-8226. This is a regulated single-asset grant with a compliance provider and freeze, which is a different problem. Where the failure conditions are the same, I reused its reason names (EXPIRED, REVOKED, OVER_TX_CAP, …).
  • ERC-8312. This tracks remaining usage for a capability it treats as opaque. This draft defines the capability itself, and the registry is its v1 store for remaining usage.
    A follow-up extension will add a swap price check inside the grant. The owner signs the price policy, and the agent can tighten it but not loosen it.

Where I’d like feedback

  1. EIP-7702 principals. The key’s ECDSA signature is checked before ERC-1271, so outstanding grants survive delegation changes. The tradeoff: delegating to smart-account code doesn’t retire the key, so grants the key signed, and any new ones it signs, stay valid until they’re revoked in the registry. The key can undo the delegation at any time anyway, so this gives it nothing it doesn’t already have. Does anyone object to that ordering?
  2. consume and the delegate. consume doesn’t bind msg.sender to delegate; the binding is “this registry’s executor.” Is that enough, or should consume also check delegate?
  3. Live-debit bound. A registry may cap live debits per asset and revert with WINDOW_FULL past that cap (the reference uses 1024). Should the spec set a minimum, so a delegate making lots of small payments knows what any conformant registry will allow?