feat: erc-7710 sub-agent budget example#1
Open
rsynthlabs wants to merge 8 commits into
Open
Conversation
main MetaMask Hybrid smart account grants sub-agent EOA a 5.00 USDC cap via ERC-7710 delegation. sub-agent burns through it in five x402 calls to r402; 6th DelegationManager.redeemDelegations reverts at the ERC20TransferAmountEnforcer caveat. exits 1 with 'budget exhausted'. - deterministic sub-agent key derived from MAIN_PRIVATE_KEY (fund ETH once) - main smart account deployed via direct SimpleFactory.deploy from main EOA (no bundler dep) - caveat-enforcer revert matched via viem BaseError.walk, with a tolerant pattern that survives toolkit version drift - x402 EIP-3009 flow duplicated inline to honor 'don't touch buyer.ts'
…rt matcher guard main() with import.meta.url check so tests can import deriveSubAgentKey and isCaveatExhaustedRevert without firing the live demo. pin the golden sub-agent address derived from the fixture key so any change to SUB_KEY_TAG fails loudly (existing funded sub-agents would be stranded by a silent change). assert ERC20TransferAmountEnforcer + ValueLteEnforcer caveats present and terms encode the 5 USDC cap.
encodeDelegations, encodeSingleExecution, and hashDelegation live at the /utils subpath export, not the root. typecheck passed at scaffold time (likely a moduleResolution: 'bundler' quirk that surfaces chunk re-exports in TS's view of the root) but the runtime ESM namespace doesn't carry them, so the demo crashed at module load with SyntaxError: does not provide an export named 'encodeDelegations'. no logic change. call sites unchanged. tests unaffected (they only import root-level symbols).
…ectly
contracts.DelegationManager is a viem-action namespace with
{ constants, encode, execute, read, simulate } keys, not a raw ABI.
the previous code aliased the namespace as DelegationManagerAbi and
passed it to walletClient.writeContract({ abi, ... }), which made viem
call abi.filter(...) and crash with 'abi.filter is not a function' at
the first redeem.
switch both redeem call sites to
contracts.DelegationManager.execute.redeemDelegations({ client,
delegationManagerAddress, delegations, modes, executions }). the
namespace helper handles delegation + execution encoding internally
and simulates before writeContract, so caveat-enforcer reverts still
surface as viem ContractFunctionRevertedError and the existing
isCaveatExhaustedRevert matcher works unchanged.
drop encodeDelegations and encodeSingleExecution imports (the
namespace handles both); keep hashDelegation (still used for the
digest log line).
verified.signer is the historical author of the payload anchored at KNOWN_TX (the May 23 demo anchor, signed by buyer 0x156d727f...5bf3); it does not change based on who paid x402. subAccount.address is today's caller. comparing them always fails unless KNOWN_TX happens to have been anchored by the sub-agent, which it wasn't and which the demo shouldn't depend on. the x402-leg invariant we actually need is 'r402 accepted our payment proof' — already enforced by callWithX402 throwing X402Rejected on facilitator reject. so the assertion was redundant on the success path and wrong on the failure path. drop the signerOk check and the exit-3 mismatch path. keep verified.signer in the output as truthful info about the anchored payload, relabeled 'verify' + '(anchored signer)' so the next reader doesn't mistake it for an identity assertion. README mocks updated to match the new line shape; examples/README exit codes table drops the now-dead exit 3 row. partial-run note: the broken run consumed $1 USDC against the cap on tx 0xf503b7ce..., so the next mainnet run from the current state has 4 redeems remaining + 1 overflow attempt (cap exhausts at call 4/5 instead of 5/5). fresh-account future runs return to the full 5+1.
mainnet smoke after commits 698af01 + d763ea1 + db7900e ran 4 successful redeems, then the 5th on-chain redeem reverted at the ERC20TransferAmountEnforcer exactly as designed — but isCaveatExhaustedRevert returned false and the catch block printed 'demo failed:' + stack trace instead of the intended 'budget exhausted. 5/5 used. ok.' exit 1. cause: the matcher walked for ContractFunctionRevertedError and bailed out (returned false) if walk found nothing, even though the outer ContractFunctionExecutionError's .message / .details already carried the revert string ('Details: execution reverted: ERC20TransferAmountEnforcer:allowance-exceeded' was visible in the printed stack). depending on whether the RPC returns decoded Error(string) data or just a message, the reason can land on inner.reason, inner.shortMessage, or only the outer wrapper's fields. drop the early bail. collect every plausible text field across both the walked inner CFRE (when present) and the outer BaseError (shortMessage, details, message), join them, run the same regex against the blob. behavior unchanged on the happy path (inner.reason-populated) — strictly more permissive for live simulate-wrapped reverts. negative tests pin that unrelated ERC20 reverts (e.g. transfer-amount-exceeds-balance) still return false. mainnet cap is now fully exhausted on the live delegation (0x09cde18..., 5/5 redeems consumed). no further mainnet rerun is needed to validate on-chain behavior; the four new unit tests cover the simulate-wrapped shape that the previous matcher missed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
summary
ships
examples/sub-agent-budget.tsfor cook-off track 1 (best x402 + ERC-7710). a main MetaMask Hybrid smart account grants a sub-agent EOA a 5.00 USDC spending cap via ERC-7710 delegation on base mainnet; the sub-agent burns through it in five 1.00 USDC x402 calls to liver402.rsynth.ai/api/verify; the sixth call'sDelegationManager.redeemDelegations(...)reverts on-chain at theERC20TransferAmountEnforcer. exits 1 withbudget exhausted. 5/5 used. ok.mainnet smoke: passed. cap fully exhausted on the live delegation against main smart account
0x09cde18.... 4 successful redeems + 1 caveat-enforced revert observed; on-chain behavior matches the demo spec.what's in the diff
examples/sub-agent-budget.tsREADME.md## sub-agent budgetssection between integration + architectureexamples/README.md.env.exampleMAIN_PRIVATE_KEYpackage.json+ lockfile@metamask/smart-accounts-kit ^1.5.0zero changes to
src/,examples/buyer.ts,src/canonical.ts,tests/verify.crosslang.test.ts, or the x402 server stack. no new x402 version handling. no bundler dep — main smart account deploys via directSimpleFactory.deploy()from the main EOA.architecture (path C)
keccak256(MAIN_PRIVATE_KEY || 'r402-sub-agent-v1')— fund ETH once, replayable across runsscope: erc20TransferAmount, tokenAddress: USDC, maxAmount: 5_000_000(5.00 USDC), enforced byERC20TransferAmountEnforcerat0xf100b081...contracts.DelegationManager.execute.redeemDelegations(...)pulling 1.00 USDC main → sub; sub then pays r402 via standard x402 EIP-3009 (same shape asexamples/buyer.ts, intentionally duplicated)BaseError.walk+ a tolerant field-fallback matcher that survives toolkit version driftverification
pnpm typecheckcleanpnpm test— 36/37 (one pre-existing skipped); 17 new unit tests cover key derivation, delegation struct shape, caveat layout, and 5 distinct revert-error shapes for the matchercommit trail
8 commits:
e7c0c94 chore: pin @metamask/smart-accounts-kit; env scaffold
89c1586 feat: examples/sub-agent-budget.ts erc-7710 demo
12367cb test: unit cover sub-agent-budget derivation, caveat layout, revert matcher
14e071a docs: sub-agent budgets in README; full example walkthrough
698af01 fix(sub-agent-budget): correct @metamask/smart-accounts-kit imports
d763ea1 fix(sub-agent-budget): use contracts.DelegationManager namespace correctly
db7900e fix(sub-agent-budget): drop incorrect signer-match assertion
5dffdf4 fix(sub-agent-budget): walk fallback for simulate-wrapped revert
bugfix commits 5-8 caught at mainnet smoke (toolkit API mismatches + wrong assertion + viem error-shape mismatch); each has a focused diff and standalone commit body.