') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(bounty): real escrow — Permit2 vouchers, x402-escrow facilitator, drand panel seeds, decay, escalation, on-chain grounding by bussyjd · Pull Request #635 · ObolNetwork/obol-stack · GitHub
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

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

Copy link
Copy Markdown
ContributorAuthor

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):

ItemValue
Capture tx (permitTransferFrom)0x5739e85bea31b714b14ac5c6c41369950b2f796720297864a9f63b4503861695 — status 0x1, block 42750844, to = Permit2 0x…aC78BA3, gas 106,631 (also recorded in status.captureTxHash)
USDC Transferposter 0xC0De…297E → fulfiller 0xBf6D…252f, 10000 micro-USDC exactly
Poster USDC delta1,189,600 → 1,179,600 (−0.01 exact)
Fulfiller USDC delta0 → 10,000 (+0.01 exact)
One-time Permit2 approval0x7e6c5fa5516e094462a15e34cf427580bf9c1ac5ed6a80dd73257c70556b1d7b — 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
bussyjdforce-pushed the feat/servicebounty-escrow branch from 1418c14 to f6e5c1bCompareJune 12, 2026 13:35
Sign up for freeto 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

@bussyjd