ERC-8414: Token-Bound Task Tenders

Reviewed and merged as the second entry under companions/. The ecrecover-based BIP-340 check held up against every official test vector with a 32-byte message, including the ten invalid cases, and against 40 random signatures from the BIP’s own reference signer; the digest binds exactly the values a relayer could otherwise choose. Three docstring-and-test notes are on the PR, none of them merge conditions. Kernel and ERCs PR #2005 remain unchanged.

There is no longer a trusted designated relayer: anyone can submit a valid, correctly bound verdict. The remaining assumptions concern the issuer’s judgment, custody of the signing key, production and availability of the verdict, and timely on-chain submission. Signature verification authenticates the verdict; it does not establish that the judgment is correct.

1 Like

@garyyang-finchip on #17, agreed that window sizing stays a publisher duty. Some measured data on what “real availability” looks like for a judged path that can escalate, since that is where the duty is hardest to meet.

We indexed UMA Optimistic Oracle and Polymarket adapter events on Polygon from 1 January to 2 October 2026: 3,005 disputes on 2,666 markets. Median time from first dispute to on-chain resolution was 3.3 h. For the 323 markets disputed twice or more, the question goes to a token-holder vote, and the median after the second dispute was 88.5 h.

So the distribution is bimodal, and the tail is where the money is contested. A tender whose acceptance authority is itself an escalating procedure (an ACDF body with appeals, or an oracle) needs judgmentWindow to cover the escalated mode, not the median. Otherwise claimUnjudged settles exactly the submissions someone thought worth contesting. A 2-day window, as in the ERC-8436 Sepolia case, is below the escalated median. It may be worth one sentence in the sizing guidance: size against the authority’s worst-case decision horizon, including appeals.

@predge-ai thank you for bringing numbers — that is the first measured availability data this thread has had, and it lands on exactly the sentence that needed it.

Your framing is sharper than mine was. The clause was written against a judge who might be slow; your data describes a judge who is fast when nobody cares and slow precisely when they do. For an escalating authority the slow mode is not noise around a median, it is the mode selected by contestation, so a window sized to the median does not merely risk the occasional default claim — it hands the default claim to the contested cases as a class. That is the adverse selection stated exactly, and it is the kind of silent failure the draft is supposed to name. So the sentence is added — Security Considerations, judged-path paragraph, committed to the PR branch before this reply:

Where the authority is itself an escalating procedure — a panel with appeals, an optimistic oracle whose disputes go to a vote — real availability means its worst-case decision horizon including escalation, not its median: a window sized to the median settles by default exactly the submissions that someone found worth contesting. An authority that fixes that horizon as a published parameter can enforce the rule itself, refusing to open a case whose worst case does not fit inside the tender’s remaining window.

The second sentence is there because of the 2-day case you cite, which is mine (ERC-8436’s Sepolia Case A), and which I’d read the other way. Its authority is an ACDF policy, and in ACDF the worst-case decision horizon is not a tail to be measured — it is a frozen parameter of the policy, maxTotalDuration, which the policy registry refuses to accept unless it covers every round and every appeal window (maxTotalDuration ≥ (maxAppeals + 1) × roundDuration + maxAppeals × appealWindow). Case A’s policy fixed it at 3 hours including its one appeal. Then the 8414 adapter does what the merged companions do, one step earlier: it derives the consumer deadline from the tender itself — min(submittedAt + judgmentWindow, settleBy) − margin — and the registry refuses admission unless the whole procedure, appeals included, fits inside it. So the 2-day window did not need to cover UMA’s 88.5 h; it needed to cover 3 h, by construction, and it did sixteen times over. If the procedure still runs out at its hard cap, the issue ends in NoDecision, and the adapter’s committed disposition for that is nothing — the tender’s own claimUnjudged clock governs, which is the kernel’s silence-pays default applied knowingly rather than stumbled into.

Where your numbers bite is the other kind of authority: one whose escalation horizon is exogenous, like an oracle whose second dispute goes to a token-holder vote with no cap. An adapter for that in the judgment slot cannot publish a maxTotalDuration it can honor, so the duty falls back on the publisher, and 88.5 h is a median, not a worst case — the tail beyond it is exactly where the money is contested. For such an authority I’d size judgmentWindow against the vote path’s hard cap if the oracle has one, and if it has none, treat that as a reason to prefer an authority that does. One more constraint for either kind: judgmentWindow is also the fulfiller’s maximum wait on silence, so “worst case” means the authority’s real worst case, not a padded one, and the settlement window (settleBy − submitBy) has to leave room for it, or a late acceptance converts into a default claim.

Thanks for the careful spec. The vault-lock invariant, the machineSettled snapshot, and the way rejectFulfillment and claimUnjudged meet at exactly submittedAt + judgmentWindow are well thought through. Two points where I could not make the text consistent with itself, and one smaller question. (Based on ERCS/erc-8414.md at commit 2253323; I did not run the reference implementation or tests.)

1. Eligibility cannot be enforced soundly on the judged path, because claimUnjudged bypasses acceptance.

  • Submission section: eligibility narrowing (“awarded bidders only, staked fulfillers only, allowlists”) “belongs to the acceptance decision and to companion bidding standards”, and “acceptance is the single gate that makes a submission count”.
  • Judgment-deadline section: claimUnjudged is permissionless and settles any Pending, judged submission after judgmentWindow, without the acceptance authority acting at all.
  • So acceptance is not the single gate. After the window, any submitter that the acceptance authority never ruled on is paid, eligible or not. For an awarded-bidders-only companion (the case the Submission section points to), an ineligible party can submit and wait for the judge to lapse.
  • The Security section’s remedy is “an admission hook in front of submitFulfillment”. But submitFulfillment is permissionless on the same contract and records msg.sender as fulfiller of record, so a wrapper in front of it cannot stop direct calls, and a wrapper would itself become the fulfiller of record.

Suggestion: an optional, ERC-165-discoverable gate interface consulted inside submitFulfillment and re-checked (or snapshotted at submission, like machineSettled) before claimUnjudged pays out. Or, if this is intended, state plainly that eligibility profiles are only sound when the judge’s availability is guaranteed.

2. judgmentWindow has no upper bound, so “the wait is bounded” is nominal.

  • judgmentWindow MUST be greater than zero, and the spec itself notes that type(uint64).max is a legal value.
  • reclaimEscrow and reclaimResidual MUST revert while pendingOf > 0, and the text justifies this with “every Pending submission becomes claimable … at submittedAt + judgmentWindow, so funders can always be made whole eventually”.
  • With a very large window set at mint (terms are immutable), a single Pending submission, including one the demander submits itself since submission is permissionless, makes third-party funders’ attributed funds non-reclaimable for that long. On the judged path the only unpaid exit is a rejection, which the acceptance authority controls.
  • Funders can read the terms before calling fundTask, so this is a disclosure issue rather than theft, but the current text presents the bound as a funder protection.

Suggestion: a normative maximum (or a SHOULD with a recommended cap and a wallet warning), and/or allow reclaim of the unreserved part of the vault (escrow minus lockedEscrowOf) while submissions are pending, rather than blocking all reclaim.

3. Smaller question: pacing and the refusal window.

The Security section says a Pending submission in a saturated epoch can be simultaneously unrejectable, unacceptable and unclaimable. The judge’s right to refuse ends at the window, but the claim can then be blocked by pacing for up to an epoch. Is it intended that a judge who was on time but whose ruling was blocked by the epoch bound loses the right to refuse? An explicit statement of the intended behaviour would help implementers.

@shentonyan thank you for reading the text at 2253323 against itself — both inconsistencies are real, and they are the text’s, not the kernel’s. The kernel does what my #21 reply said it does; the draft said something stronger in two places and something unimplementable in a third. All three are fixed on the PR branch before this reply, and nothing in the interface or the reference implementation changes.

1. Eligibility on the judged path. You are right that “acceptance is the single gate that makes a submission count” is false as written: the deadline default is a second exit, and it checks nothing. The honest statement is the one #21 gave in prose and the spec now gives normatively. Submission section:

On the machine path, verification is the single gate that makes a submission count. On the judged path, acceptance is the single affirmative gate, but not the only exit: the deadline default pays a Pending submission that was never ruled on, and it checks nothing about eligibility. Eligibility on the judged path is therefore enforced by rejecting ineligible submissions inside judgmentWindow, and it holds exactly as far as the judge’s availability does. Eligibility that must hold without a live judge belongs on the machine path, where the default claim is unavailable and releaseExpired recycles refused slots.

And you are right that the remedy sentence in Security Considerations named two things that cannot exist on this kernel. A wrapper in front of submitFulfillment becomes the fulfiller of record and is bypassed by direct calls, and a “bond escrowed alongside the delivery” has nowhere to attach. That sentence now reads:

submitFulfillment is permissionless and records msg.sender, so nothing placed in front of it can gate it: a wrapper would become the fulfiller of record, and direct calls bypass it. Deployments that expect hostile submitters SHOULD therefore enforce admission where the kernel lets them — at the acceptance decision, by rejecting unbonded, unlisted, or unawarded submissions inside the window, or on the machine path, by a verifier that refuses their proofs so that releaseExpired recycles the slot — […] A submission-time admission slot would be a kernel change and is recorded as a revision candidate, not smuggled in here.

On your first suggestion, the discoverable gate consulted inside submitFulfillment and snapshotted like machineSettled: it is the right shape, and it is now v2 candidate 3 in the repository’s ROADMAP.md with your credit. I’m not adding it to this draft because it is a new slot and a new MUST, it changes the ITaskTender identifier, and it re-opens the one boundary the kernel is built on — it enforces what is observable, and a policy contract is observable, which is exactly the argument for it and the reason it deserves a deliberate decision rather than a late amendment. Your second suggestion — say plainly that judged-path eligibility is only as sound as the judge’s availability — is what the draft now says.

2. judgmentWindow and the funders’ lockup. Also right, and the self-submission observation is the sharp version: since submission is permissionless, the demander can start the clock on its own vault. Two facts bound it that the text did not state. First, reclaim opens only on cancellation or after settleBy, and both also close submission, so no new reservation can be made once reclaim is open: the lockup after closure is at most one window from the last submission. Second, the default claim is permissionless, so a funder waiting on a stale reservation can clear it (paying its fulfiller of record) rather than wait for anyone. What remains is that the window itself has no cap, and the draft presented “bounded” as if that were a protection. It now says:

The wait is bounded by judgmentWindow: […] once reclaim is open no new submission can be made, so the last reservation is clearable within one window of the last submission, subject to epoch pacing. Because submission is permissionless, anyone — the demander included — can start that clock, so judgmentWindow is also the funders’ maximum lockup after the tender closes; it is a term, readable before fundTask, and type(uint64).max is a legal value. Publishers SHOULD size it to the judge’s horizon and no longer, and wallets and funding routers SHOULD display it beside the amount as the lockup a contribution accepts.

I have not added a normative cap, because there is no number that is right for both a three-hour panel and a week-long appeal body, and a cap in the kernel would be the first term the kernel chose for the publisher. Partial reclaim of the unreserved part while submissions are pending is v2 candidate 4, with the reason it is not a patch: the refund rule fixes the pool and the denominator at the first reclaim and zeroes a funder’s outstanding contribution, so a reservation freed by rejection after a partial reclaim would have no funder left to return to and would fall to the owner’s residual. A running-ratio design fixes that, but it is a different refund arithmetic.

3. Pacing and the refusal window. This one the kernel already answers, and the text now states it: rejection is never paced. The epoch bound sits on payment — acceptance, settlement, and the default claim — not on refusal, so a judge inside its window can always refuse, saturated epoch or not. The “unrejectable” in the Security paragraph was always the window closing, not pacing, and the paragraph now says so in place. A judge whose acceptance was blocked by a saturated epoch loses nothing either: late acceptance has no bar, so it accepts in the next epoch, or the fulfiller claims — the same money.

The commit is on the PR branch; the reference implementation and tests are unchanged, and the five edits are all in prose.

Thanks, that’s a careful pass, and the new wording on the judged path is exactly the distinction I was after: acceptance is the only affirmative gate, but not the only exit.

It also matches how the sealed-bid companion ended up integrating. Both of its 8414 adapters gate on the machine path: the token’s acceptance authority is a verifier that settles only the award winner’s work, so eligibility doesn’t depend on a judge being available, and refused slots recycle through releaseExpired as you describe. (Repo: GitHub - shentonyan/sealed-bid-award-erc · GitHub )

On candidate 3: when you get to it, the tender is a ready consumer to test it against. With admission checked at submission rather than at verification, losing bidders couldn’t submit at all, which saves them gas and keeps refused slots out of the pacing budget. I’m happy to adapt the companion to a draft of the slot whenever there is one.

Agreed on not capping judgmentWindow in the kernel. Showing it next to the amount as the lockup a funder accepts seems like the right place for that decision.:smiley:

1 Like

One case for the admission tests: a bidder wins at task version 1, then the specification changes before submission. Should the original award still apply, or should this profile freeze the task at award time?

@shentonyan Thanks for confirming the companion integrates on the machine path. That is the configuration candidate 3 is meant to serve, and your gas and pacing argument is the clearest statement of why admission belongs at submission rather than at verification. I will post here the moment a draft of the slot lands in the repository so you can adapt the companion against it.

@ted0x Good case, and it belongs to the profile rather than the kernel. The kernel versions the task document (version increments on every update until the document is frozen) and records the binding version each submission cites, but it has no notion of an award. The profile therefore has two sound options.

The one I would make normative is version-locking: an award names the task version (and the taskHash) it was granted against, and admission accepts only submissions citing that version. A publisher who updates the document after award has, by that act, withdrawn it from the awarded bidder and must re-award.

Freezing at award time is a stricter option a profile may offer, but it is an irreversible write by the update authority, and I would not have a bidding flow make that choice for the publisher by default.

The case is now in ROADMAP.md under candidate 3’s test list with your credit.