Skip to content

EIP-7702 session keys v1: chain-enforced per-tx cap + expiry + allowlist, no bundler #33

Description

@hellno

Why

Every limit the agent operates under today is enforced by the daemon's policy gate — software. This issue makes the limits chain-enforced: the user's EOA reversibly delegates (EIP-7702) to an audited contract, and the agent's session key physically cannot exceed its cap, outlive its expiry, or touch a non-allowlisted target — even if the daemon's software fence were gone. Verified white space (research 2026-06-11): Kohaku has no 7702/session-key work, eth-infinitism ships no scoped-key contract, Walletbeat marks scoped permissions as future work; MetaMask's Agent Wallet EAP (June 8, 2026) started the clock.

Decisions already made

No bundler, ever, in this scope. Both candidate stacks support the session key sending a plain, self-gas-paid transaction (verified in source):

MetaMask Delegation Framework Porto / IthacaAccount
7702 delegate EIP7702StatelessDeleGator (stateless; EOA key stays root) IthacaAccount (on-chain key registry)
Grant off-chain EIP-712 delegation + caveat enforcers (ERC-20 amount, timestamp, allowed-targets, value-LTE, …) on-chain authorize(Key) + setCanExecute + setSpendLimit
No-bundler execution permissionless DelegationManager.redeemDelegations, caveat-checked ERC-7821 execute external-caller path (opData = nonce ++ sig)
Revoke disableDelegation (delegator-only, on-chain) revoke(keyHash) (self-call by admin key)
Assurance 12 firm audit reports (Cyfrin ×7, ConsenSys Diligence ×5, → 2026-05) 2-week individual-researcher review; no firm report found

Sources: https://github.com/MetaMask/delegation-framework · https://github.com/ithacaxyz/account

Backend chosen at implementation time against these frozen criteria, in order: (1) assurance evidence at impl time (a Porto firm audit reopens the question), (2) Sepolia + mainnet deployment availability, (3) grant-card legibility, (4) reuse for x402's ERC-7710 settlement path. Grant/revoke/execute semantics below are backend-neutral so the choice doesn't churn the wire.

v1 scope (cut 2026-06-12): SessionScope = { per_tx_cap_wei, expiry_unix, allow_to, token } — the single per-tx cap is the only spend cap. Period caps and per-method granularity are deferred (both backends support them; pure additions later). Expiry and allowlist are not caps and not cuttable — a grant without them is unbounded authority.

In scope

  • SessionGrant { scope } / SessionRevoke { key_id } in deckard-contract under capability name session-keys-7702 (depends on Wire-contract evolution: Hello capability discovery + the five evolution rules #31; additive per its evolution rules).
  • Session key generated and held by the daemon (a second, lesser keystore class); local policy stays fence build(gpui): default to the git GPUI pair (Zed + gpui-component) over crates.io #1; the daemon enforces min(local policy, session scope).
  • 7702 authorization signing with hard invariants: never sign chain_id = 0 (spec-valid on all chains — unconditional refusal); delegate address must be on a hardcoded, versioned allowlist of audited deployments — user-supplied delegates rejected unconditionally (>97% of early mainnet delegations were malicious sweepers, per Wintermute); self-sponsored delegation signs auth nonce +1 (the tx consumes the current nonce); un-delegation (address(0)) clears code not storage, so re-delegation across storage-incompatible implementations is refused.
  • SessionGrantNeedsApproval always — no policy mode auto-grants authority. Grant card renders delegate identity + version and every scope field. RevokeAll/STOP additionally queues on-chain revokes for every live grant. Grant/revoke/expiry events go to the append-only audit log.
  • Acceptance on the anvil Sepolia fork (chosen backend deployed from source if no canonical Sepolia deployment):
    • S1 grant {cap 0.05 ETH, expiry 24 h, allowlist [A]} → NeedsApproval, card complete, approve → live
    • S2 session key sends a plain EIP-1559 tx within the cap to A → lands, gas paid by session EOA, no bundler process anywhere
    • S3 over-cap attempt → reverts on-chain (not a daemon Deny)
    • S4 target ∉ allowlist → reverts on-chain
    • S5 warp past expiry → reverts on-chain
    • S6 revoke (one tx) → retry of S2 reverts; local session key zeroized
    • S7 un-delegate to address(0) → EOA code empty, plain sends work as before
    • S8 grant naming an unpinned delegate → typed Deny, nothing signed
    • S9 authorization request with chain_id = 0 → refused, nothing signed
    • S10 transcript scan → no key bytes, no passphrase
  • KB correction in docs/research/01-landscape-2026.md: MetaMask Advanced Permissions = Early Access 2026-06-08, not shipped 2026-04-06.

Not in scope

  • Period/daily caps, per-method (selector) granularity (deferred 2026-06-12).
  • Walletbeat rubric contribution (cut 2026-06-12 — revisit with shipped evidence).
  • Exporting a grant to an external agent runtime (the future smart-account-autonomous mode) — semantics are shaped so it later needs no wire change, but the key does not leave the daemon in v1.
  • Mainnet (guardrail unchanged; canonical deployments + audit posture gate it).

Definition of done

  • All in-scope checkboxes; S1–S10 runnable via the demo harness.
  • cargo fmt --all --check clean; just check green (both feature configs); cargo test --workspace green.
  • New dependencies (contract bindings etc.) listed and explicitly approved before merge, per repo rule.

Open questions

  • Fund the session EOA at grant time (visible, capped) or lazily via the low-gas pre-flight?
  • Do queued on-chain revokes broadcast while the vault is locked (pre-signed revoke txs), or is "revoke on next unlock" honest enough for v1?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions