Skip to content
Beltran12138Public

About

Suitability as an on-chain object, not a prompt: revocable warrants, preflight and attestations for AI-agent trading (MetaMask Agent Wallet plugin; Monad + Avalanche)

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Warrant

A prompt is not suitability. When an AI agent trades for you, the only thing standing between "what you meant" and "what it did" is usually a sentence in a system prompt. Warrant turns that sentence into an on-chain object: a versioned, revocable, expiring warrant that a principal grants to an agent, plus a preflight that scores every proposed trade against it and an on-chain attestation that the agent saw that scorecard before it acted.

Warrant reveals, it does not enforce. It never custodies funds, never blocks a trade and never tries to bypass a wallet's own guard rails. What it adds is verifiability: after the fact anyone can check what the agent was allowed to do, and prove it was told the trade was out of bounds.

Demo videos: Monad (2:31: real quote, mm-signed attestation, ERC-8004, audit) https://youtu.be/qOab45ZLuJg · Avalanche Fuji (2:44) https://youtu.be/yitnbGgzKOA · Pitch deck (EN/中文): docs/warrant-pitch.pdf

New for ETH Shenzhen 2026: the agent no longer grades its own homework on price. Every scorecard number used to come from the agent's own plugin. WarrantPriceCheck re-prices each signed disclosure on chain with Chainlink Data Feeds, and an agent that signs PASS on a quote the oracle puts out of bounds is recorded as contradicted, with an ERC-8004 reputation entry to match. The same check settles ERC-8183 jobs (an agent hired to swap is paid only if what it delivered matches its own disclosure and the Chainlink-fair price; Warrant's first real swap was settled this way) and answers ERC-8004 validation requests, so an honest agent can carry a credential. All live on Arc mainnet; see Chainlink price check, ERC-8183 swap job and what was built for this event.

Renamed from "Mandate" on 2026-09-26 to avoid confusion with an unrelated project of the same name. Contract, command and package names changed; nothing else did.

Why not just Guard Mode?

The MetaMask Agent Wallet already ships a policy layer (Guard Mode). Its policy file expresses three things: address allow/block lists, allowed chains, and a rolling 24-hour USD outflow limit (read from mm wallet policy template, @metamask/agent-wallet 6.2.0). That is a spend limit. It cannot say "never accept more than 1% slippage", "not into a pool this thin", "not if fees eat 0.8% of the trade", or "no new token approvals". Those are suitability limits, and four of Warrant's seven dimensions exist only here.

Dimension Guard Mode policy Warrant
Trade size 24h outflow total (partial) per-trade USD cap
Slippage tolerance — ✓
Price impact — ✓
Fee load — ✓
Recipient is self address allowlist (partial) ✓
Cross-chain route allowed chains (partial) ✓
New ERC-20 approval — ✓

Warrant is a complement, not a replacement: Guard Mode enforces spend, Warrant reveals suitability and leaves a public record.

Who it's for

  • Principals who let an agent trade (a person, a fund's risk desk, a DAO treasury): they set the limits once on-chain and can revoke them at any time without touching the agent.
  • Agent builders on the MetaMask Agent Wallet: one command before each swap, and an audit trail their users can check.
  • Anyone reviewing an agent after the fact: every attestation is public, so "was the agent told this trade was out of bounds?" has a verifiable answer.

Related work: ERC-8226 (Regulated Agent Mandate)

ERC-8226 ("RAMS", Draft, by Brickken) lets a verified principal delegate scoped, time-bounded, financially capped authority to an on-chain agent, and has a regulated token check that mandate before an agent-initiated action. It overlaps with Warrant's first half (principal → agent, revocable, expiring, capped, EIP-712) and answers a different question:

ERC-8226 RAMS Warrant
Question Was this agent authorised to act for this principal on this asset? Was this particular trade suitable, and was the agent told before it acted?
Limits asset address, action type, per-tx and cumulative quantity, validity window slippage, price impact, fee load, recipient, new approvals, cross-chain route, USD size
Where it applies tokens that implement the RAMS gate (regulated assets) any swap an mm agent is about to make
Mode enforces: the gated token reverts reveals: signed disclosure, then audit
Principal eligibility IComplianceProvider (KYC / investor status) out of scope

A RAMS mandate can hold and a trade still be unsuitable (a 3% slippage sale of a bond the agent was allowed to sell); a Warrant scorecard can pass for an asset the agent was never mandated to touch. They are complements. mm warrant preflight --rams <registry> reads a RAMS mandate into the scorecard when the trade touches a RAMS-gated asset (see below).

Architecture

principal ──setWarrant / revoke──▶ WarrantRegistry (on-chain, per principal→agent, versioned)
                                         │ getWarrant
agent ── mm swap quote ──▶ mm warrant preflight ──▶ scorecard (pass / warn / fail)
                                         │
agent ──attestPreflight(quoteHash, version, verdict, scorecardHash)──▶ PreflightAttested event
   or
agent ──mm wallet sign-typed-data (EIP-712, off-chain)──▶ anyone ──submit──▶ WarrantAttestor
                                                          (checks signer + version) ──▶ SignedPreflightAttested
                                    WarrantReputation ──giveFeedback──▶ ERC-8004 Reputation (agent's identity)
Layer What Where
Contract WarrantRegistry: set / revoke / read warrants; attestPreflight emits an event bound to the current warrant version (stale or revoked versions revert) contracts/src/WarrantRegistry.sol
Contract WarrantAttestor: relays an agent-signed EIP-712 attestation; checks the signer is the agent and the warrant is active at the signed version (reads the registry, never writes it); rejects replays and malleable signatures contracts/src/WarrantAttestor.sol
Scoring 7-dimension scorer + worst-case loss, pure functions src/lib/score.ts
Chain I/O read the warrant, canonical scorecard hash, quote hash src/lib/onchain.ts
CLI mm warrant preflight plugin command src/commands/warrant/preflight.ts
Agent instructions when to preflight, stop, ask, attest skills/warrant/SKILL.md
Live walkthrough grant → preflight → attest → revoke on a real testnet scripts/demo-onchain.mjs
Relayer put an agent-signed attestation on-chain scripts/submit-signed.mjs
RPC shim lets mm wallet send-transaction reach Monad testnet (see Limitations) scripts/mm-rpc-shim.mjs
Contract WarrantReputation: each verified attestation becomes one ERC-8004 feedback entry on the agent's identity (see below) contracts/src/WarrantReputation.sol
ERC-8004 identity mint the agent's identity and bind its agentWallet to the mm wallet scripts/register-agent.mjs
Audit violation detector: trades without an active warrant, without a disclosure, or after disclosing FAIL src/lib/audit.ts, scripts/audit.mjs
ERC-8226 --rams <registry>: when the sold asset is RAMS-gated, the agent's mandate check becomes a scorecard dimension readRamsCheck in src/lib/onchain.ts; vendored reference in rams/; scripts/demo-rams.mjs

Tech stack: Solidity 0.8.28 + Foundry 1.8.3 · TypeScript on Node 22+ · viem 2.56 · MetaMask Agent Wallet plugin SDK (@metamask/agent-wallet 6.2.0, oclif) · Arc mainnet · Monad testnet · Avalanche Fuji C-Chain.

Deployments

WarrantRegistry has the same address on both chains (same deployer, same nonce): 0x37cdFe2a144993dC3145367305fF66E29302E673

Chain Deploy tx Explorer
Monad testnet (10143) 0x62d54a4690013a4a5c2fec2653d7442dfa3741f3bf0845c625eb37911f3d08a8 contract
Avalanche Fuji C-Chain (43113) 0xd224000c67d4c0146e70726481582c5a0e8a3e51f5521d0ac2c46a89309c68e9 contract

Deployment records: contracts/broadcast/Deploy.s.sol/{10143,43113}/. The records also contain the first deployment under the old name (MandateRegistry, 0xf0145a8b…1f48), kept as history.

WarrantAttestor is also at the same address on both chains, pointing at the registry above: 0xC356ac5ebD7d249102D3C9c25764A8815c1718aA (deploy txs: Monad 0xa65b871f…1a92, Fuji 0x75d86fbf…a859; records in contracts/broadcast/DeployAttestor.s.sol/).

Chainlink price check on Arc mainnet (2026-10-14)

The gap it closes. A Warrant disclosure proves the agent was shown a scorecard, but every number on it (price impact, fees, output) came from the agent's own plugin and the quote it was handed. An agent that skips preflight, runs a modified plugin or is fed a bad quote can sign PASS on anything, and the chain had no independent way to say otherwise.

What WarrantPriceCheck does (contracts/src/WarrantPriceCheck.sol):

  1. The terms are bound by the agent's existing signature. For a checkable disclosure, the attestation's quoteHash is keccak256(abi.encode(quoteIdHash, sellToken, buyToken, sellAmount, buyAmount)). A relayer cannot swap in friendlier amounts (TermsMismatch), and the agent needs no second signature.
  2. Chainlink prices the trade. fairOut reads the sell and buy tokens' USD feeds (latestRoundData), rejects a non-positive answer (BadPrice) or a stale one (StaleFeed: an unpriceable quote must not be recorded as checked), and converts across token and feed decimals.
  3. The shortfall is compared with the warrant. shortfallBps = (fairOut − quotedOut) / fairOut (0 when the quote beats the oracle) against the warrant's cost budget, maxPriceImpactBps + maxFeeBps, plus a tolerance for oracle lag: a feed only updates when the price moves past its deviation threshold, so the tolerance is the sum of both feeds' thresholds (0.5% + 0.5% = 100 bps).
  4. The verdict is checked, not trusted. Out of bounds and disclosed PASS = contradicted. The contract only accepts disclosures WarrantAttestor already verified (agent signature, active warrant version), each once. checkAndRate also posts ERC-8004 feedback from the contract itself: tag1 warrant-oracle, tag2 consistent (value +1) or contradicted (value −1).

The plugin uses the same contract before the agent signs: mm warrant preflight --price-check <addr> adds an eighth dimension, Chainlink price check, which fails exactly where the contract would record a PASS as contradicted and warns inside the tolerance band. An honest agent therefore never gets contradicted; one that lies, or skips the plugin, does.

Address / transaction (Arc mainnet, chain 5042)
WarrantPriceCheck 0xdF3fE689F0a3C74Ce299018cB573870769f04571, deployed in 0xcc34e3d7…b956 (contracts/script/DeployPriceCheckArc.s.sol, 1,258,254 gas), wired to the deployed WarrantAttestor and Arc's ERC-8004 registries
Chainlink feeds USDC / USD 0x84EA90AC…7905 for native USDC (18 decimals) · EURC / USD 0x361b95c1…Bf23 for EURC 0xbEf5f6d5…21c1 (6 decimals). Both: 8 decimals, 24 h heartbeat, 0.5% deviation threshold; max age set to 25 h
Disclosure, relayed and rated 0x9c4a0ae8…02be (WarrantReputation.submitAndRate)
Chainlink check, rated 0xfcceb21a…4117 (WarrantPriceCheck.checkAndRate)
Staged dishonest agent, caught agentId 2437, agentWallet 0x8F31FaFc…957B (throwaway key; register, bind, warrant); PASS relayed 0xd6f22c09…b8af; contradicted 0xbde80bd4…8f4d (scripts/stage-liar-arc.mjs)

The live run. A real mm swap quote on Arc (0.5 USDC → EURC via Uniswap, quote 0xc700a574…1a29) went through mm warrant preflight --registry … --attestor … --price-check 0xdF3f…4571. Chainlink priced 0.5 USDC at 448,542 EURC base units; the quote offered 449,499, so the price check passed (0 bps shortfall). The verdict was still FAIL, on fee load (gas was 199 bps of a $0.50 trade against a 50 bps limit), exactly as on 2026-10-05. The mm server wallet signed the FAIL disclosure (quoteHash = the terms hash 0xd8d15a0f…2b40), and two relayed transactions recorded it, rated it warrant-preflight / fail, and had WarrantPriceCheck re-price it: PriceChecked logged fairOut 448,542, quotedOut 449,499, shortfall 0, budget 200, out of bounds false, contradicted false. ERC-8004 getSummary(1419, [WarrantPriceCheck], "warrant-oracle", "consistent") now returns 1. No swap was executed.

The case it exists for, on a fork. node scripts/demo-pricecheck.mjs forks Arc mainnet and uses the contracts and live feeds deployed there. Two staged agents (throwaway keys, fork only) get an ERC-8004 identity and the same warrant, and both face the same bad quote, 5% below the Chainlink-fair output:

  fair 448542 vs quoted 426114 EURC base units
  preflight: FAIL  Chainlink price check: 500bps below the Chainlink-fair output
▶ honest agent signs FAIL …  PriceChecked: shortfall 500 bps, budget 200 bps, out of bounds true, contradicted false
▶ liar agent signs PASS …    PriceChecked: shortfall 500 bps, budget 200 bps, out of bounds true, contradicted true
  honest agentId 2436: consistent 1, contradicted 0
  liar   agentId 2437: consistent 0, contradicted 1

The same case on Arc mainnet. scripts/stage-liar-arc.mjs staged it for real: a throwaway agent key, an ERC-8004 identity whose registration says it is a staged dishonest agent (agentId 2437), the same warrant the mm wallet holds, and a staged quote 5% below what Chainlink said 0.5 USDC buys. The agent signed PASS without running preflight; the disclosure was relayed and rated like any other, and checkAndRate logged fairOut 448,542, quotedOut 426,114, shortfall 500 bps, budget 200 bps, out of bounds true, contradicted true. getSummary(2437, [WarrantPriceCheck], "warrant-oracle", "contradicted") returns count 1, value −1. The dishonest agent and its quote are staged, not observed: no real trading agent has been caught lying.

Chainlink usage (files)

Chainlink product Where Role
Data Feeds (AggregatorV3Interface: latestRoundData, decimals) contracts/src/WarrantPriceCheck.sol (fairOut, _price) The independent price every PASS is checked against; staleness and non-positive answers revert
Same feeds, read through the contract src/lib/onchain.ts (readOracleCheck), src/lib/score.ts (oracleDimension), src/commands/warrant/preflight.ts (--price-check) The agent sees the oracle's verdict before it signs
Feed addresses and parameters contracts/script/DeployPriceCheckArc.s.sol Arc mainnet USDC/USD and EURC/USD
Tests against the live feeds contracts/test/ArcPriceCheck.fork.t.sol, test/pricecheck.test.mjs Fair output recomputed from raw feed answers

ERC-8183 swap job and ERC-8004 validation on Arc mainnet (2026-10-14)

Two more places the Chainlink check now reaches, both live on Arc mainnet.

1. A paid job, settled by chain data (ERC-8183). ERC-8183 (Draft) is a job with an escrowed fee and an evaluator who alone decides complete or reject. WarrantSwapJob is both the hook and the evaluator of "swap this for me" jobs, where the client is also the agent's Warrant principal:

  • Funding hands the agent the client's capital (the hook pulls it from the client).
  • Submitting requires the agent to deposit what it actually got and attach the Warrant disclosure it signed for the quote: on chain, signed by the job's provider, principal = the job's client, terms = the order. No disclosure, no delivery.
  • Settling is permissionless and decided from chain data only. The agent is paid if the delivered output is within the warrant's price-impact + fee budget of the Chainlink-fair output (plus the oracle tolerance), within the warrant's slippage limit of the output it disclosed (the disclosure is the quote, the deposit is the execution: inside a job, the two are finally linked), it did not disclose FAIL and trade anyway, and the warrant is still active at the disclosed version. Otherwise the job is rejected and the fee goes back to the client. Either way the delivered tokens go to the client: the capital was theirs.

The core is the ERC-8183 reference implementation (erc-8183/base-contracts 142e669, MIT), vendored unmodified in contracts/src/vendor/erc8183 and deployed behind a proxy with no fees. Its reference FundTransferHook returns the same token and amount, so it cannot express a swap; Arc's own ERC-8183 instance is testnet-only, has no Chainlink feeds there, and only Circle can whitelist hooks on it.

The live job. Job 1: the deployer (principal) hired the mm wallet (ERC-8004 agent 1419) to turn 0.5 USDC into EURC for a 0.01 USDC fee. The agent took a real mm swap quote (0xe984b279…b2ea), ran mm warrant preflight --price-check (PASS on all eight dimensions; Chainlink-fair 448,542 EURC units, quote 448,903), signed the disclosure, executed the swap (Warrant's first real trade, via Uniswap on Arc: 448,903 EURC units received, exactly the quote), deposited the output and submitted. settle read: shortfall 0 of 450 bps, slippage vs quote 0 of 100 bps, warrant active, no FAIL → paid; the fee went to the agent and the EURC to the client.

For a $0.50 trade, gas alone is 199 bps of the notional on Arc, over warrant v1's 50 bps fee limit, so the principal re-granted the mm wallet's warrant as v2 with a 300 bps fee limit before the job (otherwise preflight says FAIL, as on 2026-10-05, and an honest agent does not trade).

2. A credential an honest agent can ask for (ERC-8004 validation). Arc mainnet also hosts ERC-8004's ValidationRegistry. WarrantValidator answers validation requests for Warrant disclosures with the same rule as WarrantPriceCheck: 100 when the disclosure survives the oracle, 0 for a PASS the oracle puts out of bounds, tag warrant-oracle. Requests can only come from the agent's side, so this is the opt-in, positive half; checkAndRate (no consent needed) stays the way to catch a liar. The registry does not tie a request to the agent whose disclosure it names, so respond requires the identity's agentWallet to be the disclosure's signer; without that, anyone could collect a credential for someone else's honest disclosure (test_credentialCannotBeBorrowed). The 2026-10-14 FAIL disclosure of agent 1419 was validated: response 100, getSummary(1419, [WarrantValidator], "warrant-oracle") = count 1, average 100.

Address / transaction (Arc mainnet)
WarrantValidator 0x38Bc8a02…ce65, deployed in 0x0d70f356…7965; ERC-8004 ValidationRegistry 0x8004Cc84…AB58
Validation of agent 1419's disclosure 0x0466ddf1…3517 request, response 100
ERC-8183 core (proxy) 0x15faD1B6…efD1, implementation 0x7F2e54d3…E62B
WarrantSwapJob 0x63a97f29…9e0f (contracts/script/DeploySwapJobArc.s.sol; 6 txs incl. whitelisting, ~6.99M gas)
Warrant v2 for the mm wallet 0xbcac0320…7547 (fee limit 300 bps)
Job 1 create, order, fee set by the agent 0xad7f609c…8352, fund
The swap 0xeca1755b…95fa (mm swap execute, 0.5 USDC → 448,903 EURC units)
Disclosure relayed + Chainlink check 0x5cc325f0…d303, 0xc26cbf4d…d059
Delivery approve, submit (by the mm wallet; Guard Mode email approval each)
Settled: paid 0xc5124ed1…43a3 (job Completed; 0.01 USDC to the agent, 448,903 EURC units to the client)

Driven by scripts/swap-job-arc.mjs (the agent's steps are printed as mm wallet send-transaction commands) and scripts/validate-arc.mjs. The fork tests use a 6-decimal stand-in for USDC: Arc's ERC-20 USDC moves native balances through Arc protocol precompiles (0x1800…0000 transfer, 0x1800…0001 isBlocklisted) that stock Foundry does not implement, and Arc Foundry ships Linux/macOS binaries only. The real USDC path is what the mainnet job above exercised.

Arc mainnet (2026-10-05)

The whole stack runs on Arc mainnet (chain 5042, USDC as gas), wired to Arc's own ERC-8004 v2.0.0 registries, and one real quote went the whole way:

Address / transaction
WarrantRegistry 0x80B435fF87276f1302BfCAB4D7202555EeCDe73f
WarrantAttestor 0x33B7B8db2927783dEbb58a589CF5c7B95824fae1
WarrantReputation 0x30906fFDE26d87868B602D34DA96f2C0006e3962 (feedback client of ERC-8004 ReputationRegistry 0x8004BAa1…9b63)
Deploy + warrant v1 to the mm wallet contracts/script/DeployArc.s.sol; records in contracts/broadcast/DeployArc.s.sol/5042/ (4 txs, ~1.99M gas)
Agent identity ERC-8004 agentId 1419 on IdentityRegistry 0x8004A169…a432: register, agentWallet := mm wallet (signed by the mm wallet)
Disclosure, relayed and rated 0x2123414c…11f6

The quote is a real mm swap quote on Arc: 0.5 USDC → EURC through Uniswap (0xb5a7cebf…9ad3), price impact 0.08%, slippage 50 bps. Preflight against the warrant on Arc returned FAIL on one dimension, fee load: $0.01 (199 bps) against a 50 bps limit, because on Arc the gas is paid in the coin being sold and the quote prices it (0.00997 USDC network fee on a $0.50 trade). The mm server wallet signed that scorecard (mm wallet sign-typed-data, chain 5042), and one relayed transaction recorded it in WarrantAttestor with the quote hash 0xac086309…b68c and scorecard hash 0x3b065817…a596 that preflight printed, and posted ERC-8004 feedback warrant-preflight / fail for agent 1419. No swap was executed: the agent disclosed FAIL and stopped. Before deploying, contracts/test/ArcMainnet.fork.t.sol ran the same flow on an Arc mainnet fork against the live registries.

Attestations signed by the MetaMask wallet itself (2026-09-27)

The agent here is the mm server wallet 0x299e298ded14b36412a7857ed02b844e7fb58e0e (Guard Mode), granted warrant v1 on both chains. Two ways to attest, both tested live:

Transaction (mm wallet send-transaction → attestPreflight) Signature (mm wallet sign-typed-data → relayed to WarrantAttestor)
Fuji PASS 0xb828…72c5, FAIL 0xea6a…9b03 PASS 0x0f5a…0cfd, FAIL 0x8721…38f7
Monad testnet PASS 0xb362…8905 (through the RPC shim) PASS 0x0540…1f6f
Agent pays gas yes (~31k–35k) no; the relayer pays (67,577 gas on Fuji; 88,716 billed on Monad, which charges the gas limit)
Guard Mode approval one email approval per transaction (these testnets are not in the default allowed_chains) none: signed immediately, in every attempt
Monad RPC shim needed yes no

The signature path is what an unattended agent would use: it signs every scorecard for free and without waiting for a human, and the evidence becomes public once anyone relays it.

ERC-8004: the agent's identity and a reputation built from its attestations (2026-09-27)

ERC-8004 (Draft) gives agents an on-chain identity (an ERC-721) and a reputation registry that any client can post feedback to. Its v2.0.0 registries are live at the same addresses on both testnets (Identity 0x8004A818…BD9e, Reputation 0x8004B663…8713). Warrant plugs into both:

  • Identity. The trading agent is an ERC-8004 identity whose agentWallet is the mm wallet. The registry only changes agentWallet with that wallet's own EIP-712 signature; here it came from mm wallet sign-typed-data (no transaction, no approval prompt), submitted by the agent's operator (scripts/register-agent.mjs).
  • Reputation. WarrantReputation (0x0FAf92b84f00201e888210B62ef5055Cc3BbefED, both chains) is the feedback client. submitAndRate(attestation, signature, agentId) accepts only an attestation whose agent is that identity's agentWallet, records it in WarrantAttestor (or finds it already recorded, so front-running the attestor cannot cost the agent its feedback), and posts one entry: value 1, tag1 = "warrant-preflight", tag2 = the verdict, feedbackHash = the attestation digest. Anyone can then ask the ERC-8004 registry itself: getSummary(agentId, [WarrantReputation], "warrant-preflight", "fail") = how many trades this agent disclosed as out of bounds before acting.
Monad testnet Avalanche Fuji
Agent identity (owner: operator 0xeb11…789E) agentId 1937, register agentId 252, register
agentWallet := mm server wallet, signed by mm 0x6ce8…dca2 0xcd02…cda5
PASS attestation rated 0xc89e…4879 (already on-chain → rated only) 0x3e46…27d2 (already on-chain → rated only)
FAIL attestation rated 0x108c…7427 (new: recorded + rated in one tx) 0x5c7c…f5cf (already on-chain → rated only)
getSummary read from the ERC-8004 registry 2 entries: 1 pass, 1 fail 2 entries: 1 pass, 1 fail
WarrantReputation deploy 0xd0b34514…3332 0x75ca407c…77cc

What this reputation measures is disclosure, not trading skill: that the agent checked each trade against a principal's warrant and signed the result before acting. A principal is only an address, so a consumer should weigh entries by principal (the SignedPreflightAttested event names it); the count alone can be inflated by an agent that grants itself warrants.

Audit: finding the agents that did not follow their warrant (2026-09-27)

Disclosure only matters if someone checks it against what the agent actually did. scripts/audit.mjs reads a principal → agent pair from chain and classifies every transaction the agent sent in a block range:

Verdict Meaning
ok an active warrant, and the latest disclosure before the trade was PASS
NO_ACTIVE_WARRANT traded while the warrant was never granted, revoked or expired
UNATTESTED traded with no unused disclosure before it (one disclosure covers one trade)
EXECUTED_AFTER_FAIL the latest disclosure before the trade said FAIL
EXECUTED_ON_WARN review, not a violation: WARN needs a human yes, which is not visible on-chain

Everything comes from the chain: warrant events and disclosures from logs, the warrant's state before the range from getWarrant at the previous block, and the agent's transactions from the blocks themselves. The agent's nonce delta over the range is the denominator, so the report says how many of its outgoing transactions it examined; an empty result never claims more than that. Transactions to Warrant or ERC-8004 contracts are bookkeeping, not trades. Exit code 2 on violations.

scripts/demo-violations.mjs stages one of each outcome with the testnet agent key (the "trades" are 1e-6 native-token transfers to 0x…dEaD, standing in for swaps; the audit does not depend on what a trade is). The audit then found them without seeing the script's labels:

Staged step Fuji Monad testnet Audit verdict
grant warrant 0x8b53…3b50 0x3da5…dfdd
disclose PASS 0x6ed9…a1fe 0x89e8…50a4 bookkeeping
trade 1 0x91ac…ed7e 0x5de3…6a87 ok
trade 2 (no disclosure) 0x245d…828f 0xca7a…bd91 UNATTESTED
disclose FAIL 0x940d…e665 0x98f8…b724 bookkeeping
trade 3 0x6b6f…fbd7 0x73d7…a15f EXECUTED_AFTER_FAIL
revoke 0xa2c0…5ef8 0x77e0…d713
trade 4 0xf82a…684c 0x45da…42e3 NO_ACTIVE_WARRANT

Both chains: 4 actions, 3 violations, coverage 6 of 6. Reproduce:

node scripts/audit.mjs fuji  --principal 0xe4ebDEbd84f80bF592ca61C6eA56d10568D23aeA --agent 0xeb114deDc3883A4300fa0bBC29F5590607c0789E --from-block 58782113 --to-block 58782127
node scripts/audit.mjs monad --principal 0xe4ebDEbd84f80bF592ca61C6eA56d10568D23aeA --agent 0xeb114deDc3883A4300fa0bBC29F5590607c0789E --from-block 66108417 --to-block 66108463

What it cannot see: whether a trade matched the quote that was disclosed (the swap transaction does not carry the quote id), and when a signed disclosure was signed (the EIP-712 payload has no timestamp, so for signed disclosures it proves existence, not order; the report says so). It scans every block in range, so it is meant for bounded windows (up to 20,000 blocks), not whole histories.

ERC-8226: a RAMS mandate and a Warrant scorecard on the same trades (2026-09-28)

The unmodified ERC-8226 reference implementation is deployed on Monad testnet (addresses and provenance in rams/README.md); its ERC-7943 mock asset stands in for a tokenized bond ("wDBOND"). The principal holds 1,000 wDBOND and grants the agent both a RAMS mandate (transferFrom only, ≤100 per trade, ≤250 in total) and a Warrant (≤100 bps slippage, etc.). scripts/demo-rams.mjs then preflights three sales with readRamsCheck, the same call mm warrant preflight --rams makes, so the RAMS answer is inside the attested scorecard hash:

Sale RAMS canExecute Warrant Attestation What happened
A. 50 wDBOND, 0.5% slippage OK PASS 0x9de6…6285 agent sold: 0x65e9…a56d
B. 80 wDBOND, 3% slippage OK FAIL (slippage) 0x5385…0bd3 agent stopped; RAMS alone would have allowed it
C. 150 wDBOND, 0.5% slippage OVER_TX_CAP FAIL (mandate) 0x8a36…3bd5 the token itself reverts MandateBlocked(OVER_TX_CAP) (checked with eth_call)

Setup transactions: mandate 0xe7d4…7671, warrant v5 0x845d…5287. Sale B is the point: a mandate that holds does not make a trade suitable. Limits of this demo: the quotes are synthetic (no DEX lists wDBOND), the "sale" settles as transferFrom to a venue address, one key plays principal, compliance operator and token admin, and the --rams CLI flag has been exercised through the same library functions but not yet against a real mm stored quote of a RAMS-gated asset. test/rams.test.mjs checks on anvil that every RAMS verdict the plugin reports matches what the gated token does with the same call (accept, OVER_TX_CAP, OVER_CUMULATIVE_CAP).

Live run (2026-09-26)

scripts/demo-onchain.mjs executed on both chains: grant → preflight against the on-chain warrant → agent attests a PASS and a FAIL scorecard → principal revokes. Every transaction below returned status = success with one event log, sent to the registry above.

Step Monad testnet Avalanche Fuji
setWarrant (principal) 0xf864…fbf8 0x50a8…df54
attestPreflight PASS (agent) 0x5fc8…9d5e 0xc719…cbca
attestPreflight FAIL (agent) 0x18d4…9a0e 0x27a2…bfc9
revoke (principal) 0xc7db…5921 0x90e7…4400

Principal 0xe4ebDEbd84f80bF592ca61C6eA56d10568D23aeA, agent 0xeb114deDc3883A4300fa0bBC29F5590607c0789E (throwaway testnet wallets).

How it integrates with each chain

The contract is plain Solidity 0.8.28 with no chain-specific precompiles; the same bytecode runs on both networks. What differs is why each chain matters for this use case.

Monad. Warrant is built as a plugin for the MetaMask Agent Wallet (mm) CLI, and the on-chain half lives on Monad testnet. An agent that trades at machine frequency needs a pre-trade attestation per quote; that is only reasonable on a chain with high throughput and fast blocks. Measured cost of one attestPreflight on Monad testnet: 35,130 gas at 102 gwei = 0.0036 MON.

Avalanche. Same contract on the Fuji C-Chain. Snowman consensus fixes the order of accepted blocks in under a second with no reorgs, so an attestation accepted before the trade stays before it. Since the Helicon upgrade (ACP-194; scheduled by avalanchego for Fuji on 2026-07-28 and mainnet on 2026-09-22, both before the Fuji transactions in this README) C-Chain execution is asynchronous: a block is accepted first, executed after, and its results settled about τ = 5 s later. So order is final at acceptance, but an agent that waits for the attestation's receipt before trading is waiting for execution, not acceptance. Measured cost of one attestPreflight on Fuji: 30,788 gas at a 160 wei gas price, i.e. effectively free. This build does not use Avalanche L1s, ICM or x402.

What the preflight checks

Seven dimensions, each pass / warn / fail against the warrant:

Dimension Warrant field
Trade size (USD) maxUsdPerTrade
Slippage tolerance maxSlippageBps
Price impact maxPriceImpactBps
Fee load maxFeeBps
Recipient is the wallet itself recipientMustBeSelf
Cross-chain route allowCrossChain
New ERC-20 approval allowNewApproval
Chainlink price check (with --price-check) maxPriceImpactBps + maxFeeBps, against Chainlink Data Feeds

Plus a worst-case realised loss (slippage to minDestAmount) and a note when the trade would likely trip the wallet's native Guard Mode 2FA. If the on-chain warrant is missing, revoked or expired, the verdict is forced to fail: the agent is not authorised.

Track record, next to the scorecard (--track-record)

The scorecard is about one trade. Whether the agent has shown any skill is a different question, and the answer goes next to the scorecard, never into the verdict or the scorecard hash:

mm warrant preflight --track-record 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 --agent-id 1419   --evidence-client <address you trust> --rpc-url https://rpc.mainnet.arc.io

It reads deflatedT feedback for the agent from the ERC-8004 reputation registry: a t-statistic on the agent's trading record, written by Evidence Deflator (tools/erc8004_evidence.py in its repository), with tag2 = bestOf<N> when the agent was picked from a field of N. The plugin puts it against the bar for that field (Bonferroni, the same formula as the website) and prints one line, for example: t = 3.42 against a bar of 4.90 for the best of 47,310: strong enough for a record picked blind, not for the best of 47,310. Anyone can write feedback in ERC-8004, so the reader chooses whose entries count (--evidence-client).

Evidence Deflator writes from 0xF02cF39d6b91Ac62B1D21d0F1C39335118c1F9A1. Its first entries (2026-10-17) are for DegenAI on Base (agent 19136) and Ethereum (agent 23121); read live on Base:

mm warrant preflight --track-record 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 --agent-id 19136   --evidence-client 0xF02cF39d6b91Ac62B1D21d0F1C39335118c1F9A1 --rpc-url https://mainnet.base.org
#   Track record (ERC-8004 agent 19136): t = 1.33 against a bar of 2.00 for a record picked blind:
#   cannot be told apart from luck. A reading of past record, not of this trade.

Quick start

Requirements: Node 22.18+, mm (npm i -g @metamask/agent-wallet), Foundry.

npm install
npm run relink    # point the local @metamask/agent-wallet at the global mm copy (see below)
npm run build
mm config set experimentalPlugins true
mm config set experimentalAllowUnverifiedInstalls true
mm plugins install "file:<absolute path to this repo>" --accept-permissions

npm run relink matters: npm install puts a second physical copy of @metamask/agent-wallet in node_modules, so the host's instanceof PluginCommand check fails with PLUGIN_INVALID_BASE. relink.cjs replaces it with a junction to the global copy. Re-run it after every npm install.

Grant a warrant (principal), on either chain:

cast send 0x37cdFe2a144993dC3145367305fF66E29302E673 \
  "setWarrant(address,(uint64,uint16,uint16,uint16,bool,bool,bool,uint64))" \
  <AGENT> "(100000,100,150,50,true,true,true,0)" \
  --rpc-url https://api.avax-test.network/ext/bc/C/rpc --private-key $PRINCIPAL_KEY

Preflight a pending quote against it (agent):

mm swap quote ...                     # creates a stored quote
mm warrant preflight \
  --registry 0x37cdFe2a144993dC3145367305fF66E29302E673 \
  --principal <PRINCIPAL> \
  --rpc-url https://testnet-rpc.monad.xyz \
  --attestor 0xC356ac5ebD7d249102D3C9c25764A8815c1718aA     # optional: also emit the EIP-712 attestation

With --attestor, the output also carries attestation.signed: the typed data and a ready-to-run mm wallet sign-typed-data command. Sign, then relay with any funded key:

node scripts/submit-signed.mjs typed-data.json <signature>   # checks the signer locally, then submits

The output ends with a ready-to-run mm wallet send-transaction … attestPreflight(…) command, so the mm wallet that will trade signs the disclosure itself (JSON output carries the same call under attestation.mm). Grant the warrant to that wallet's address (mm wallet list). A cast send variant is printed too, for agents that hold their own key. Without --registry, preflight falls back to a local warrant.json (see warrant.example.json).

On Monad testnet, mm 6.2.0 needs a local RPC shim because MetaMask's RPC proxy does not serve chain 10143 yet (the preflight output says so and adds explicit gas and fees to the payload):

node scripts/mm-rpc-shim.mjs &                          # loopback only; logs chain + method
MM_INFURA_RPC_BASE_URL=http://127.0.0.1:47812 mm wallet send-transaction --chain-id 10143 --payload '…'

Tested with the server wallet in Guard Mode: Monad testnet 0xb3628760…8905, Fuji 0xb828bf71…72c5 (PASS) and 0xea6abf03…9b03 (FAIL). Each needed one email approval, because neither testnet is in the wallet's default allowed_chains.

Live walkthrough without mm (uses synthetic quotes, real chain):

cp contracts/.env.example contracts/.env   # then put two funded testnet keys in it
node scripts/demo-onchain.mjs monad        # or: fuji

Agent skill

skills/warrant/SKILL.md tells an agent to run preflight before every swap, stop on fail, ask the human on warn, attest before executing, and never split or reshape a trade to get under a threshold.

Tests

npm test                              # scoring (14) + attestation builders (11) + audit (18) + on-chain integration on a local anvil (8) + ERC-8226 on anvil (8) + Chainlink price check, incl. an Arc mainnet fork (6) + track record from the live ERC-8004 registry on an Arc fork (5)
git clone --depth 1 https://github.com/foundry-rs/forge-std contracts/lib/forge-std   # once
(cd rams && git clone --depth 1 --branch v5.4.0 https://github.com/OpenZeppelin/openzeppelin-contracts lib/openzeppelin-contracts && git clone --depth 1 https://github.com/foundry-rs/forge-std lib/forge-std)   # once, for the ERC-8226 test
git clone --depth 1 --branch v5.6.1 https://github.com/OpenZeppelin/openzeppelin-contracts contracts/lib/openzeppelin-contracts                          # once, for ERC-8183
git clone --depth 1 --branch v5.6.1 https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable contracts/lib/openzeppelin-contracts-upgradeable  # once, for ERC-8183
cd contracts && forge test            # 78: WarrantRegistry (13) + WarrantAttestor (14) + WarrantReputation on a Fuji fork (5) + the stack on an Arc fork (1) + WarrantPriceCheck (20) + it on an Arc fork with live Chainlink feeds (3) + WarrantValidator on an Arc fork with the live ValidationRegistry (9) + WarrantSwapJob with the ERC-8183 reference core on an Arc fork (13)
cd ../rams && forge test               # the vendored ERC-8226 reference suite (105), unmodified

The integration test deploys to a local anvil, writes a warrant with cast, reads it back through the plugin, attests with the hashes the plugin produces, and revokes. Its assertions are anchored to the values written with cast, not to the plugin's own conversion code.

Limitations

  • Reveal, not enforce. A plugin cannot intercept mm's native commands; an agent can skip preflight. The attestation makes skipping detectable, not impossible.
  • Attestation signer. The mm server wallet can sign attestPreflight through mm wallet send-transaction: tested on Fuji, PASS 0xb828bf71…72c5 and FAIL 0xea6abf03…9b03, both sent from the server wallet (~30,800 gas each). Two caveats as of mm 6.2.0: Guard Mode asks for email/MFA approval for every transaction on a chain outside the wallet's allowed_chains (neither testnet is in the default list), and on Monad testnet (10143) MetaMask's RPC backend answers Invalid chainId, so mm needs scripts/mm-rpc-shim.mjs (0xb3628760…8905 went through it). Because of the per-transaction approval, the unattended scripts/demo-onchain.mjs signs with a separate agent EOA instead.
  • Fee load counts gas only when the quote prices it. Since 2026-10-05 the network fee is added to the fee load when it is paid in the coin being sold (USDC on Arc, the native coin when selling it), valued at the quote's own sell-side rate; otherwise it is left out rather than guessed (a quote from an empty wallet on Monad carries no network fee). The worst-case loss still covers only the fill at minDestAssetAmount, not gas.
  • Prices are off-chain inputs, except where Chainlink checks them. USD values in the scorecard come from the quote. With --price-check, the quote's output is re-priced on chain against Chainlink Data Feeds, with these limits: only tokens the contract has a feed for (on Arc: native USDC and EURC); a 100 bps tolerance, so a shortfall inside the feeds' deviation thresholds is not flagged; gas paid outside the quote is not in the output, so it is not counted (the fee-load dimension covers it); and the check runs only when someone calls it. A disclosure nobody checked is visible (no PriceChecked event for its digest) but not penalised.
  • Scripted demos use synthetic quotes; the plugin has run on a real one. mm swap quote works with an empty wallet: on 2026-09-27 a real Monad mainnet quote (1 MON → USDC, quote 0x13d2c3f1…e2f7) went through the installed plugin, mm warrant preflight --registry … --attestor …, scored against the on-chain warrant on Monad testnet: PASS (size $0.03, slippage 50 bps, price impact −2.39%, fees $0). mm's priceImpact is (fromUsd − toUsd) / fromUsd (reproduced from that quote's own fields), so a negative value is in the trader's favour. The scripted walkthroughs (demo-onchain.mjs, demo-violations.mjs) score the fixtures in test/fixtures.mjs so they stay reproducible; every warrant read, write and attestation is real. A second real quote (0x2ade8da4…5256) went the whole way: preflight PASS → mm wallet sign-typed-data by the server wallet → relayed through WarrantReputation on Monad testnet in one tx, 0x397aee33…7295, whose SignedPreflightAttested carries the quote hash 0xaffb6c2f…d016 and scorecard hash 0x2020c94f…91e1 that preflight printed, plus ERC-8004 feedback #3 for agent 1937. The first real swap was executed on 2026-10-14 inside an ERC-8183 job on Arc mainnet (see "ERC-8183 swap job"); outside a job, Warrant still does not execute or match trades.
  • Mainnet, but not audited. The Arc deployment is mainnet; Monad and Fuji are testnets. None of the contracts has been audited. All but one hold no funds (they record warrants and disclosures); WarrantSwapJob escrows the delivered tokens of a job between submit and settle, and the ERC-8183 core escrows the job fee. A job whose agent takes the capital and never submits loses the capital (the fee is refunded on expiry), as in the reference design.
  • Static analysis only. Slither 0.11.6 over contracts/src (vendored ERC-8183 and OpenZeppelin excluded): 14 results, none exploitable on review. Two arbitrary-send-erc20 in WarrantSwapJob: the from is the job's client in afterAction(fund) and the submitting provider in _deliver; the hook is callable only by its own ERC-8183 core, which already requires fund to come from the client and submit from the provider. One reentrancy-no-eth and three reentrancy-events: the external calls go to Warrant's own attestor and to the ERC-8004 registries. Four unused-return (ignored tuple fields; the Chainlink read checks answer > 0 and staleness), three timestamp (expiry and feed age, by design), one missing-zero-check on a constructor argument. This is not an audit. Reproduce: cd contracts && slither . --filter-paths "lib|vendor|test|script".

Pre-existing work and build window

The hackathons Warrant was entered in allow a pre-existing foundation if it is disclosed. Timeline:

Date Work Evidence
2026-09-03 Scaffolding cloned from MetaMask's agent-wallet-plugin-template (MIT): build config, plugin wiring, a hello ping sample and a security-scan workflow. The sample and workflow were removed on 2026-09-26. LICENSE keeps MetaMask's notice
2026-09-03 → 09-14 Preflight command, 7-dimension scoring (src/lib/score.ts), limits loader (src/lib/spec.ts), unit tests, offline demo. File modification dates; snapshot commit 10d4f25
2026-09-25 → Registry contract and tests, on-chain preflight (src/lib/onchain.ts), anvil integration test, deployments to Monad testnet and Fuji, live demo, agent skill, this README. Commits from b923d94 on
2026-09-26 Renamed Mandate → Warrant; redeployed as WarrantRegistry. This commit and later
2026-09-27 → 09-28 Signed attestations (WarrantAttestor), ERC-8004 identity and reputation, audit, real mm quotes, ERC-8226 (RAMS) integration. Commits and deployments listed above
2026-10-05 Arc mainnet deployment; fee load counts the quote's network fee. 9e87ccf, 3ab44f8
2026-10-14 → ETH Shenzhen 2026: WarrantPriceCheck (Chainlink Data Feeds), its unit and Arc-fork tests, --price-check in the plugin, checkAndRate in the relayer, the Arc mainnet deployment and live run, scripts/demo-pricecheck.mjs, scripts/stage-liar-arc.mjs, the ETH Shenzhen deck (docs/warrant-ethsz.pdf); then WarrantValidator (ERC-8004 validation) and WarrantSwapJob (ERC-8183, with the reference core vendored), their Arc-fork tests and the live mainnet job with Warrant's first real swap. Commits from 388053e on

Version control started late. The repository was only put under git on 2026-09-25, so work from 09-03 to 09-14 appears as one snapshot commit instead of its own history. No commit is backdated.

  • Monad Metropolis (build window 2026-09-01 → 10-13): all of the above falls inside the window; the only pre-existing code is the MetaMask template scaffolding.
  • Avalanche Buildathon (2026-09-14 → 09-30): the preflight plugin predates the window. The work done for this event is the on-chain layer: the contract, on-chain preflight, the Fuji deployment and the live demo.
  • ETH Shenzhen 2026 (2026-09-30 → 10-23): Warrant was submitted to earlier hackathons (Monad Metropolis, Avalanche Buildathon, Arc Microgrants, BLI Legal Tech Hackathon 2). The increment built for this event, listed in the row above: three new contracts (WarrantPriceCheck, WarrantValidator, WarrantSwapJob; the ERC-8183 core is vendored, not ours), 45 new contract tests, 6 new plugin tests, the --price-check dimension, the relayer step, mainnet deployments with an honest live run, a staged dishonest agent, a validated disclosure and a settled ERC-8183 job, and a fork demo. Everything else is the earlier work it builds on.

AI disclosure

This project was built with AI coding tools, as the hackathons require to be disclosed:

  • Claude Code (Anthropic; Claude Opus models, Opus 5.5 for the work from 2026-09-25) wrote most of the code, tests, scripts and this README under the author's direction; commits it co-authored carry a Co-Authored-By trailer.
  • The design was critiqued twice by a panel of other AI models ("council" reviews, 2026-09-14 and 2026-09-26). The first is why Warrant reveals instead of enforcing: an mm plugin cannot intercept native commands, so an enforcement layer would be bypassable by construction. The second surfaced the naming collision and the Guard Mode positioning above.
  • Product thesis, scope, and every go/no-go decision are the author's. Each on-chain claim in this README was checked against transaction receipts.

License

MIT. See LICENSE.

About

Suitability as an on-chain object, not a prompt: revocable warrants, preflight and attestations for AI-agent trading (MetaMask Agent Wallet plugin; Monad + Avalanche)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages