Skip to content

feat(bounty): real escrow — Permit2 vouchers, x402-escrow facilitator, drand panel seeds, decay, escalation, on-chain grounding#635

Open
bussyjd wants to merge 1 commit into
feat/servicebounty-eval-marketfrom
feat/servicebounty-escrow
Open

feat(bounty): real escrow — Permit2 vouchers, x402-escrow facilitator, drand panel seeds, decay, escalation, on-chain grounding#635
bussyjd wants to merge 1 commit into
feat/servicebounty-eval-marketfrom
feat/servicebounty-escrow

Conversation

@bussyjd

@bussyjd bussyjd commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Stack position

PR 2 of 2, stacked on #634 (feat/servicebounty-eval-market). Review #634 first; this PR's diff is only the escrow leg. The full architecture is documented in plans/servicebounty-technical-spec.md (included here) — it is the canonical reference for this stack.

What

Replaces the dev-ledger escrow seam from #634 with real, non-custodial money movement on Base Sepolia:

  • Permit2 SignatureTransfer vouchers (internal/x402/escrow/permit2.go): the poster signs an EIP-712 PermitBatchTransferFrom voucher agent-side (remote signer, or --key for dev) covering the bounty's seats (reward / bond / eval / escalation legs). Deterministic nonces (keccak256(uid|leg)) make re-funding idempotent and replay-proof. Funds never leave the poster's wallet until capture — the facilitator holds signatures, not money.
  • cmd/x402-escrow facilitator: new in-cluster service (x402 ns, ClusterIP-only, port 8403, distroless). Routes: POST /escrow/reserve|capture|void/{id}, GET /escrow/info, /healthz; bearer-authed (constant-time). Capture recipients must be a subset of the voucher's signed seats with exact amounts — funds flow poster → recipients directly through Permit2; the facilitator only pays gas.
  • Voucher ferry: vouchers ride bounty annotations (obol.org/{reward,bond,eval,eval-r1}-voucher); the controller ferries them to the facilitator and never signs anything. Escrow URL/token reach the controller via env only.
  • drand-seeded panel lotteries: evaluator panels are drawn from a drand quicknet beacon (round strictly after bounty creation + 30 s), BLS-verified in-process; provenance (round, randomness, signature) recorded in status. A failed beacon fetch requeues — there is no silent local-randomness fallback (test-pinned anti-grinding property).
  • Reputation decay: read-time half-life on evaluator track records; stale Full evaluators demote to Probation effective-tier without writes.
  • Escalation panels: high-dispersion or knife-edge verdicts trigger a fresh 2k+1 panel (excluding round 0 + fulfiller), poster-funded within a window; round-1 median is final.
  • On-chain grounding: evaluator reveals can be mirrored to the ERC-8004 ValidationRegistry; the controller verifies responder/score match and counts grounded evals into ladder weight — never blocking the verdict.

Trust model (documented, test-named)

v1 residue: voucher recipients are facilitator-policy-bound, not signature-bound (Permit2 SignatureTransfer lets the spender pick to; our facilitator enforces the seat-subset rule — TestVoucherRecipientAddressIsPolicyBoundNotSignatureBound pins this honestly). The upgrade path is a witness+disperse contract; spec §5 covers it.

Validation

  • Full unit suite green: voucher EIP-712 round-trips, subset-capture enforcement, nonce determinism, drand verification, decay math, escalation triggers, grounding matchers, structural pins (no new routes/secrets from bounty reconcile).
  • Adversarially review-gated during development (two independent reviewers; all actionable findings addressed).
  • A live Base Sepolia reserve→capture smoke with real Permit2 transfers is running now; evidence (tx hashes, balance deltas) will be posted as a PR comment.

🤖 Generated with Claude Code

@bussyjd

bussyjd commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Live Base Sepolia escrow smoke — PASSED ✅ (reserve → capture, real Permit2)

Run against a live k3d cluster with this branch's serviceoffer-controller + x402-escrow images, a funded poster wallet, and a fresh zero-balance fulfiller EOA. Shortest capture path (--dangerously-skip-verification, poster-as-judge) to isolate the money leg.

Sequence: bounty postEscrowAwaitingVoucher (spender correctly surfaced from GET /escrow/info) → fund before claim correctly refused (voucher binds the fulfiller seat) → claimfund --signer-url (voucher signed by the agent remote signer; deterministic nonce; CLI never holds a key) → EscrowReserved=Truesubmitaccept → facilitator capture → Phase Paid, escrowState: Captured within 10 s.

On-chain evidence (Base Sepolia):

Item Value
Capture tx (permitTransferFrom) 0x5739e85bea31b714b14ac5c6c41369950b2f796720297864a9f63b4503861695 — status 0x1, block 42750844, to = Permit2 0x…aC78BA3, gas 106,631 (also recorded in status.captureTxHash)
USDC Transfer poster 0xC0De…297E → fulfiller 0xBf6D…252f, 10000 micro-USDC exactly
Poster USDC delta 1,189,600 → 1,179,600 (−0.01 exact)
Fulfiller USDC delta 0 → 10,000 (+0.01 exact)
One-time Permit2 approval 0x7e6c5fa5516e094462a15e34cf427580bf9c1ac5ed6a80dd73257c70556b1d7b — status 0x1

Non-custodial property held throughout: funds stayed in the poster's wallet from fund until capture; the facilitator held only the signature and paid only gas.

One minor gap noted for follow-up: the facilitator has no GET /escrow/{id} read endpoint, so per-hold state is evidenced via controller conditions only.

…tator, drand seeds, decay, escalation, grounding

Squash of: bc00584 (escrow slice) + 59ce619 (technical spec, plans/servicebounty-technical-spec.md)
@bussyjd bussyjd force-pushed the feat/servicebounty-escrow branch from 1418c14 to f6e5c1b Compare June 12, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant