You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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 post → EscrowAwaitingVoucher (spender correctly surfaced from GET /escrow/info) → fund before claim correctly refused (voucher binds the fulfiller seat) → claim → fund --signer-url (voucher signed by the agent remote signer; deterministic nonce; CLI never holds a key) → EscrowReserved=True → submit → accept → 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)
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.
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
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.
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 inplans/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:
internal/x402/escrow/permit2.go): the poster signs an EIP-712PermitBatchTransferFromvoucher agent-side (remote signer, or--keyfor 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-escrowfacilitator: 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.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.drandquicknet 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).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 —TestVoucherRecipientAddressIsPolicyBoundNotSignatureBoundpins this honestly). The upgrade path is a witness+disperse contract; spec §5 covers it.Validation
🤖 Generated with Claude Code