Attack Lab
Every row below is a real NEAR testnet transaction against tab-vault.testnet. Not a browser simulation, not a reference model.
spender tab-merchant.testnet
Conservation
| # | Scenario | Expected | Actual | Result | Transaction |
|---|---|---|---|---|---|
| D0 | Deposit tUSDC into the vault via ft_transfer_call | 100000000 ledger | 100000000 ledger | PASS | 1N7Xwhi6… |
| F1 | Merchant received exactly the executed charges | 60000000 delta | 60000000 delta | PASS | n/a |
| F2 | Contract stats count only confirmed settlements (delta) | charges=6, volume=60000000 | charges=6, volume=60000000 | PASS | n/a |
| F3 | Vault ledger conservation | 30000000 | 30000000 | PASS | n/a |
Caps
| # | Scenario | Expected | Actual | Result | Transaction |
|---|---|---|---|---|---|
| S1 | Legitimate charge inside every cap | executed | executed | PASS | E2BnPa2f… |
| S2 | Prompt-injection charge above per-charge cap | PER_CHARGE_CAP | PER_CHARGE_CAP | PASS | 9LJTMXod… |
| S3 | Second legitimate charge inside period budget | executed | executed | PASS | GvmLUqUR… |
| S3b | Period budget exhaustion | PERIOD_CAP | PERIOD_CAP | PASS | BCCXHCPV… |
Access control
Lifecycle
| # | Scenario | Expected | Actual | Result | Transaction |
|---|---|---|---|---|---|
| S5 | Charge while the owner has paused the Tab | TAB_NOT_ACTIVE | TAB_NOT_ACTIVE | PASS | Dnqk7jwQ… |
| S12 | Charge after the on-chain expiry timestamp | TAB_EXPIRED | TAB_EXPIRED | PASS | 4EiewNFx… |
| S13a | Charge before revocation | executed | executed | PASS | HkoAt6nR… |
| S13b | Charge after permanent revocation | TAB_NOT_ACTIVE | TAB_NOT_ACTIVE | PASS | GazzFcuv… |
Rollover
Balance
| # | Scenario | Expected | Actual | Result | Transaction |
|---|---|---|---|---|---|
| S8 | Charge larger than the vault ledger balance | INSUFFICIENT_BALANCE | INSUFFICIENT_BALANCE | PASS | 9yqC5Baz… |
Transfer rollback
| # | Scenario | Expected | Actual | Result | Transaction |
|---|---|---|---|---|---|
| S10a | NEP-141 transfer to an unregistered account fails | failed | failed: FT_TRANSFER_PROMISE_FAILED | PASS | 3kXkt43o… |
| S10b | Callback restores ledger and cap counters | ledger 60000000, spent 0, nonce 0 | ledger 60000000, spent 0, nonce 0 | PASS | n/a |
Replay
Withdrawal
| # | Scenario | Expected | Actual | Result | Transaction |
|---|---|---|---|---|---|
| S14 | Owner withdraws from the vault | +10000000 wallet, -10000000 ledger | +10000000 wallet, -10000000 ledger | PASS | EBG1SGFu… |
Agent, gasless and x402 evidence
Run 26 Oct 2026 · real testnet transactionsGasless merchant + agent charge
The merchant and agent sign charge delegates with their scoped keys; the relayer pays gas. Both NEAR balances are unchanged. A prompt-injected agent redirect is rejected on-chain.
Gasless checkout route
The browser calls the merchant route; the server signs the delegate and the keeper relays it. This exercises the same path the merchant page uses with the gasless toggle.
Prompt-injection containment
The agent is instructed to pay an attacker. The on-chain allowlist rejects it with RECEIVER_NOT_ALLOWED and moves nothing; the legitimate merchant invoice still settles.
Agent pays a paid API
An unpaid request returns a real HTTP 402 challenge. The MCP tool pays the invoice from a Tab, then the API verifies the on-chain charge_executed event before returning data. Replay, wrong payer, over-cap, redirected receiver and revoked Tab are all rejected.
What the contract enforces
- Caps: per-charge, period and lifetime limits are checked before the token call.
- Authorization: only the designated spender can call
charge; the owner controls pause/revoke. - Receiver policy: a fixed allowlist rejects redirected charges.
Anyis required for rotating Intent deposit addresses. - Replay: each Tab has a monotonic
next_nonce; a consumed nonce cannot be reused. - Rollback: if the NEP-141 transfer fails, the callback restores the ledger and counters without erasing later in-flight charges.
Reproduce the run
The runners need the funded testnet accounts, the merchant charge key and the gas-only relayer. The setup scripts create them and write secrets only to the gitignored .env.
pnpm tsx scripts/create-testnet-accounts.ts pnpm tsx scripts/deploy-testnet.ts pnpm tsx scripts/setup-spender-key.ts merchant pnpm tsx scripts/setup-spender-key.ts agent pnpm testnet:e2e pnpm test:meta pnpm test:relayer-usage pnpm test:agent-attack pnpm test:x402
docs/evidence/testnet-e2e.json and the copy rendered on this page.