Sealed-Bid Award Mechanism for Task Tenders (companion to ERC-8183 / 8195 / 8414)

The gap

The agent task stack now has three ways to post and settle a task. ERC-8183 escrows a single job and lets an evaluator release it. ERC-8195 defines five procurement modes. ERC-8414 makes the task itself an ERC-721 with a vault inside it. All three leave the same question outside the standard: when several agents want one task, who is awarded, and at what price?

  • ERC-8183 runs bidding through an off-chain hook and only receives the result through setProvider (see its Example 2).
  • ERC-8195’s Auction mode relays bids off-chain and hardcodes lowest-bid-wins. The thread already notes there is no bid commitment or reveal structure at the interface level, and that point was never picked up.
  • ERC-8414 says plainly that bidding is a companion extension, not kernel.

So each market writes its own award logic, the same agent bids under three incompatible rules, and no indexer can say what an award meant or whether the price was fair.

What this proposes

A small companion standard that answers only that question. It fixes three things:

  1. A sealed-bid procedure: commit, reveal, award, with a bond that is returned on reveal and slashed on silence.
  2. A stateless mechanism interface, award(bids, reserve, units) → (winner, price), that a tender contract calls once reveals close.
  3. Three normative pricing rules: first-price, second-price (Vickrey), and uniform-price for identical units. Anything else is a plugin that implements the same interface.

It holds no task funds, judges no deliverable, and writes no reputation. It returns an award that an ERC-8183 hook can pass to setProvider and setBudget, that an ERC-8195 Auction-mode contract can read in place of selectLowestBidder, or that an ERC-8414 eligibility profile can require of the fulfiller of record.

Why this shape

The design follows Myerson’s optimal auction results (Mathematics of Operations Research, 1981), read as a procurement problem where the private type is each agent’s cost.

  • Revelation principle. Any feasible auction is equivalent to a direct mechanism: bidders report a type, the mechanism computes an allocation and a payment. So the on-chain interface should be exactly that pair of functions, and commit–reveal is what makes a sealed report credible on a public ledger.
  • Revenue equivalence. The requester’s expected payment is fixed once the allocation rule and the reserve are fixed. A standard therefore only needs to pin down those two, and can leave the pricing rule to a plugin without changing what the requester expects to pay.
  • Second-price with a reserve is optimal in the symmetric regular case, and its payment rule contains no distributional parameters. That is why Vickrey is the recommended default: it stays incentive compatible even when the requester’s beliefs about agent costs are wrong, and autonomous agents from different operators have no shared prior to bid against under first-price.
  • The optimal reserve is strictly tighter than the requester’s outside value, and no-award is a legitimate outcome. Hence reserve is a field of its own rather than a budget the hook happens to see.
  • Discriminating rules and correlated-type mechanisms are deliberately out of scope. The first needs per-bidder beliefs; the second needs the requester to pay losing bidders, which the vault standards forbid. Both remain expressible as plugins.

Interfaces

interface IAwardMechanism {
    struct Bid   { address bidder; uint256 amount; }   // amount = price asked
    struct Award { address winner; uint256 price;  }

    function mechanismId() external pure returns (bytes4);   // bytes4(keccak256("award.vickrey")) etc.

    /// MUST be pure. Bids arrive in commit order. Empty return = no award.
    /// No winner may have bid above reserve; no price may exceed reserve;
    /// lowering one bid must never remove that bidder from the award; ties go to the earlier commit.
    function award(Bid[] calldata bids, uint256 reserve, uint256 units)
        external pure returns (Award[] memory);
}

interface ISealedBidTender {
    enum Phase { None, Commit, Reveal, Awarded, Void }

    struct TenderTerms {
        bytes32 taskRef;         // the task in whichever escrow standard opened this tender
        address mechanism;       // IAwardMechanism
        uint256 reserve;         // > 0
        uint256 units;           // >= 1
        uint64  commitDeadline;
        uint64  revealDeadline;  // > commitDeadline
        uint256 bond;            // escrowed per commit, returned on reveal, slashed otherwise
        address bondAsset;       // address(0) = native
    }

    event TenderOpened(bytes32 indexed tenderId, bytes32 indexed taskRef, address indexed requester,
                       address mechanism, uint256 reserve, uint256 units, uint64 commitDeadline, uint64 revealDeadline);
    event BidCommitted(bytes32 indexed tenderId, address indexed bidder, bytes32 commitment);
    event BidRevealed(bytes32 indexed tenderId, address indexed bidder, uint256 amount);
    event BidSlashed(bytes32 indexed tenderId, address indexed bidder, uint256 bond);
    event TenderAwarded(bytes32 indexed tenderId, address indexed winner, uint256 price, bytes4 mechanismId);
    event TenderVoid(bytes32 indexed tenderId);

    function openTender(TenderTerms calldata terms) external returns (bytes32 tenderId);
    // commitment = keccak256(abi.encode(tenderId, msg.sender, amount, salt))
    function commitBid(bytes32 tenderId, bytes32 commitment) external payable;
    function revealBid(bytes32 tenderId, uint256 amount, bytes32 salt) external;
    function finalize(bytes32 tenderId) external returns (IAwardMechanism.Award[] memory);

    function termsOf(bytes32 tenderId) external view returns (TenderTerms memory);
    function requesterOf(bytes32 tenderId) external view returns (address);
    function phaseOf(bytes32 tenderId) external view returns (Phase);
    function awardOf(bytes32 tenderId) external view returns (IAwardMechanism.Award[] memory);
}

Normative rules, with revealed amounts sorted b(1) ≤ b(2) ≤ … and reserve r:

id winners price award iff
award.first-price lowest units bids own bid bid ≤ r
award.vickrey (units = 1) lowest bid min(b(2), r), or r if alone b(1) ≤ r
award.uniform-price lowest units bids min(b(units+1), r) bid ≤ r

Uniform-price is scoped to one unit per bidder; multi-unit demand should use a VCG plugin.

What I would like the forum’s view on

  1. Reserve as a first-class field. Should the escrow standards expose a reserve separately from budget or maxPrice, given that the optimal reserve is tighter and non-award must be allowed?
  2. Bidder heterogeneity. ERC-8004 reputation makes agents asymmetric, and asymmetric optimal auctions discriminate. Should scoring rules be a mechanism plugin, or stay out of the standard entirely?
  3. Bonds. An unrevealed commitment has to cost something or commit–reveal is free to grief. Kernel field, as drafted, or an admission hook?
  4. Composition. Is a stateless IAwardMechanism enough for ERC-8183’s hook, ERC-8195’s selectWorker, and ERC-8414’s eligibility profile, or does each need an adapter? In particular, for ERC-8183: is a hook permitted to call setProvider and setBudget back on the job, or must the client sign those?

The full draft text follows the ERC template and will go up as a PR to ethereum/ERCs once this thread has had a first round. Reference implementation and test vectors to follow; the vectors assert the monotonicity condition and, for Vickrey, that truthful bidding is dominant. Spec text is CC0.

1 Like

I think the composition question is the one thing to close before this goes to a PR. A valid auction result should not itself acquire authority to mutate whichever task standard it points at. The companion should produce an award; the target standard’s existing authority model should decide how that award becomes effective.

So I would separate:

auction validity → Award(winner, price)

from:

Award → authorized target transition

and make the second step adapter-specific.

For 8183, that means the client still calls setProvider / setBudget; the hook verifies that those values equal the committed award. The hook does not become the client.

For 8195, the adapter projects the award into its Auction transition.

For 8414, the award supplies eligibility only. winner can be consumed by the committed eligibility policy; price does not rewrite rewardPerCompletion, which is already an immutable tender term. The existing versioned TaskRoot and submission taskVersion already determine which eligibility policy applies, so I would not add a second version-binding mechanism here.

The one common binding the companion does need is the target itself. bytes32 taskRef is too underspecified across three standards with different identifier domains. I would define one canonical target commitment, e.g.

targetRef = keccak256(abi.encode(chainId, targetContract, targetId))

with a specified normalization of targetId.

Then the boundary is closed:

valid award
+
canonical target
+
target-authorized adapter
→
effective award

Bonds can stay exactly where they are: they secure the companion’s commit/reveal procedure, not the task kernel. If slashed value is routed anywhere else, that destination should simply be an explicit tender term rather than an incidental property of an adapter.

That leaves the companion doing one job: produce a replay-safe, target-bound award. Each task standard remains authoritative over what that award is allowed to change.

2 Likes

Thanks — this is the right cut, and I’ll take all four into the draft.

Separating award validity from target transition. Agreed, and it answers my own question 4 better than I had framed it. The hook must not become the client. The companion’s contract ends at a replay-safe, target-bound Award(winner, price); what that award is permitted to change is the target standard’s decision, enforced by its own authority model. For 8183 that means the client still calls setProvider / setBudget and the hook only checks equality against the stored award. I’ll restate the composition section that way and drop any language suggesting adapters hold authority.

8414 and price. You’re right that price cannot rewrite rewardPerCompletion. The honest consequence is that on a tender with an immutable reward the companion delivers eligibility and the price is informative, not settled. I’d rather state that plainly than pretend otherwise: the allocation is still incentive compatible (that part depends only on the winner rule), but price discovery on 8414 happens when the vault is priced, not at award. One place it can still bite is the reserve: a 8414 tender that wants a sealed-bid round would naturally set reserve = rewardPerCompletion, so that any bid above the fixed reward is simply non-award. I’ll spell that out as the recommended pattern rather than invent a second price channel.

Canonical target. Adopting targetRef = keccak256(abi.encode(chainId, targetContract, targetId)) with targetId as bytes32: a bytes32 identifier (8183 jobId, 8195 taskId) is used as is, a uint256 identifier (8414 tokenId) is left-padded. taskRef goes away.

Bonds. Keeping them in the companion. Slashed value gets an explicit slashRecipient term rather than being an adapter side effect; I’ll remove the deployment-level sink from the reference implementation.

Revised draft and updated reference implementation to follow in the thread before the PR.

1 Like

Revised draft and reference implementation per the above: https://github.com/shentonyan/sealed-bid-award-erc
:wink:

2 Likes

Does truthful bidding still hold for the ERC-8414 integration when the auction price is only informational?

Suppose the fixed reward is 100. My actual cost is 60 and another bidder asks 50. Bidding truthfully loses. Bidding 40 wins while I still receive 100 and make a profit of 40.

Doesn’t this mean incentive compatibility depends on the payment rule as well as allocation? Should the fixed-reward integration explicitly drop the truthful-bidding claim and explain what the bid is meant to represent?

2 Likes

Truthful bidding is a property of the allocation and payment rule together. If the fixed-reward 8414 integration consumes winner but discards price, then the Vickrey payment rule is no longer the bidder’s payment rule.

With rewardPerCompletion = 100, true cost 60, and another bid at 50, reporting 60 loses while reporting 40 wins and still pays 100 to the fulfiller. So truthful cost revelation is not dominant in that integration.

I would therefore separate two cases explicitly:

Price-binding integration: Award.price becomes the target payment. The Vickrey truthfulness claim applies.

Allocation-only integration: the target consumes only Award.winner and keeps an independently fixed payment. price is informational and the integration MUST NOT inherit the truthful-bidding claim.

For 8414 that seems to line up with the distinction already identified: an existing tokenId with fixed rewardPerCompletion is allocation-only; if price discovery is wanted, run the award against the pre-mint taskHash and mint with rewardPerCompletion = award.price.

More generally:

IC(award) != IC(target ∘ award) unless the target preserves the payment rule on which the incentive-compatibility proof depends. The award can carry its evidence across the boundary; the truthfulness claim has to be re-established under the target’s actual utility semantics.

3 Likes

You’re right, and my line in #3 that on a fixed-reward 8414 tender “the allocation is still incentive compatible” was wrong. Thanks for the clean counterexample.

It generalises, and it’s worse than “not truthful”. In procurement form, incentive compatibility requires X(c) = c·Q(c) + ∫_c^c̄ Q(s) ds + K, and a fixed reward means X(c) = R·Q(c). Together they give (R − c)·Q′(c) = 0, so the probability of winning can’t depend on the bid below R. Once the payment is fixed, the only IC rule is a posted price: accept or decline at R, with ties broken by something other than the bid. A bid ranking on top of a fixed reward collapses to the same thing. Everyone with c < R bids the minimum, and the award goes to whoever committed first. With uniform costs and R = 0.5, the expected cost of whoever does the work is 27% higher at n = 3 than under a price-binding second-price award, and 63% higher at n = 5. So on an allocation-only target the sealed-bid round buys nothing.

To your question: yes, allocation-only integrations should drop the truthfulness claim and say what a bid means. For the next revision:

  1. An integrationProfile in TenderTerms (price-binding or allocation-only), so bid semantics are fixed and visible before anyone commits.
  2. A fourth normative rule, award.posted-price: the earliest commitment with bid ≤ r wins at price r. That is the honest rule for allocation-only targets, and the 8414 adapter will require it for fixed-reward tokens.
  3. Your example added to the repository as a formal counterexample next to the Lean truthfulness proof, so the proof’s scope is stated by a theorem rather than a comment.
1 Like

Agreed on the split, and “IC(award) ≠ IC(target ∘ award)” is the right way to put it. I’ll make the two cases normative.

One addition: I think price-binding is necessary but not sufficient. The target also has to be committed to executing the award. If the requester can decline after seeing the revealed bids, it is choosing the reserve ex post, and that choice can depend on the winner’s own bid. Example: a requester who executes only if price − winning bid ≤ 10. Cost 40, other bid 60. Bidding 40 truthfully is declined (markup 20); bidding 50 is executed at 60 for a payoff of 20. So the condition I’d write is:

IC(target ∘ award) holds only if the target (a) pays Award.price and (b) executes every non-empty award, with no discretion conditioned on revealed bids.

That makes your pre-mint route for 8414 the right one, but it has to be atomic. If the award is computed first and the requester mints later, (b) fails, and the revealed costs leak into whatever the requester does next (the ratchet problem; McAfee and Vincent 1997). An award-then-mint adapter gives both conditions: the requester escrows r up front, and finalize mints with rewardPerCompletion = Award.price and refunds r − price. The award still holds no authority over anything the requester hasn’t pre-committed.

Does (b) match how you’d draw the 8183 boundary? There the client can always leave the job unassigned, so I don’t see a way to get commitment without an escrowing adapter on that side as well.

McAfee, R. P., & Vincent, D. R. (1997). Sequentially optimal auctions. Games and Economic Behavior, 18(2), 246–276.

Yes. I’d draw the 8183 boundary the same way, with one qualification: escrow is one implementation of the requirement, not the requirement itself.

The requirement is that before bids are revealed, the target has already committed sufficient authority to execute every admissible non-empty award. Otherwise Award is still only a valid computation; the requester deciding whether to setProvider / fund after seeing it is a new outcome-conditioned authority decision.

So I think the composition condition is:

IC(target ∘ award) requires both payment-binding and execution-binding.

For 8183 today, the ordinary client-driven setProvider → fund path is not execution-binding. An integration claiming the auction’s IC property therefore needs an escrowed or equivalently binding pre-authorization path that finalizes the awarded provider and payment without renewed requester discretion.

I would also make the 8414 path atomic rather than describe it as “award, then mint”: the precommitment should authorize one finalize → mint(Award.price) + refund transition. The award determines the parameters; it does not acquire authority by being computed.

On the execution-binding condition: there is a second way an award can stop being final even when the target is pre-committed. If the winner is later disqualified (a missed reveal found after the fact, or a bond challenge), the award changes after setProvider or finalize has already read it.

I would not let the mechanism overwrite an award in place. Suggest an awardState (proposed, challenged, final) and a challenge window, with a replacement emitting AwardSuperseded(oldAwardId, newAwardId) so an 8183 hook or an indexer can tell what an earlier read meant. The atomic finalize transition should only consume an award in final.

Resolution systems show this is not rare. From 1 Jan to 2 Oct 2026, 323 Polymarket markets were disputed twice or more, and the median time to resolution was 88.5 h after a second dispute. A second challenge round is a normal case, so the interface should name it.

1 Like

Agreed, and I’ll adopt your terms: payment-binding and execution-binding, with escrow named as one way to get the second rather than its definition.

Your framing also exposed a gap in my own adapter. In AwardThenMint, settle is permissionless and the requester has no path to the escrow except through it, so execution can’t be withheld. But it can fail. The token id is fixed by the task hash at open and nothing reserves it. If anyone mints that id on the task contract before settle, the mint reverts, and settle reverts with it, permanently. That is a reachable state in which the award is final and execution is impossible, which is exactly what execution-binding has to rule out. Committing the money isn’t enough; the transition itself has to be reserved before reveal.

So I’d state the condition over reachable states rather than transactions:

Execution-binding: before the commit deadline, the target (or an adapter it trusts) holds an authorization such that, for every admissible non-empty award, the transition that executes it at Award.price can be triggered by anyone and cannot be pre-empted, blocked, or made to revert by the requester or a third party.

Same-transaction atomicity is the simplest way to meet that, and I’ll make the 8414 path one finalize → mint(Award.price) + refund transition, as you describe. I’d still keep finalize itself permissionless at the tender level, since other targets read the award too. What matters is that once the award is fixed, nothing can stand between it and execution. Since ERC-8414 leaves minting to the implementation, that means the adapter must hold exclusive rights to the id it will mint, either because the task contract lets only that adapter mint in its namespace or because the id is reserved at open. I’ll fix the reference adapter and add the squatting case as a test.

The same test applies to 8183: an escrowing adapter is execution-binding only if the client can’t race its setProvider + fund, for instance because the adapter itself is the job’s client.

Thanks. I agree a consumer must never act on an award that can still change, but I think this design avoids the problem rather than needing a dispute layer for it.

The award is a pure function of on-chain data: the revealed bids in commit order, the reserve and the number of units. A missed reveal can’t be discovered after the fact. finalize can only run once the reveal deadline has passed, and it slashes every unopened commitment in the same call. There is no bond challenge either, because the bond only enforces opening, and whether a commitment was opened is on chain. So nothing that would justify disqualifying a winner can arrive after finalize. The Polymarket disputes are about whether an off-chain event happened, which needs an oracle and time; nothing here does.

What can go wrong after the award is the winner failing to deliver, and that belongs to the target: the 8183 evaluator, or the 8414 verifier. A requester that wants to re-award opens a new tender for the same targetRef, and an indexer can follow that by targetRef.

Your point does show the spec should say outright what it now only implies: Awarded and Void are terminal, and awardOf never changes once set. I’ll add that as a MUST. A target that wants a challenge window for its own reasons can wait before consuming the award, but I’d keep that out of the award interface so that every consumer doesn’t carry a state machine only some of them need.

Fair, that’s convincing. The award is a pure function of on-chain data, so nothing can arrive after finalize that would change it. Making Awarded and Void terminal and awardOf immutable a MUST covers what I was worried about, and a target that needs a challenge window can simply wait before consuming the award. Thanks.

2 Likes

The draft requires finalize() to recheck award conditions 2-4, including that no winner bid above the reserve. But _checkAward() checks only award count, price, nonzero winners and duplicates.

With units = 1:

reserve = 100
Alice revealed 150
Bob revealed 90

a buggy or malicious mechanism can return Award(Alice, 1) and pass validation. So can Award(Carol, 1) if Carol never revealed.

Could the invariant explicitly require:

Every returned winner MUST correspond to a revealed Bid supplied to award(), with amount <= reserve.

And could finalize() enforce both membership and reserve eligibility? The tender already stores reveal status and bid amounts. Neither check requires understanding the pricing rule.

Related: should the effective mechanism logic remain fixed from openTender() through finalization?

The draft already requires pure, stateless mechanisms, so a storage-based upgradeable proxy would violate that requirement. But the tender’s interface checks do not establish purity: external pure calls cannot prevent state reads at the EVM level. A proxy could change its implementation after bidders commit or reveal while retaining the same address and mechanismId. Pinning its code hash alone would leave that possibility open, since the implementation address lives in storage.

Could the spec make this immutability assumption explicit and explain how bidders should verify it?

2 Likes

The Rationale scopes award.uniform-price to one unit per bidder because multi-unit demand invites demand reduction. That scope is enforced per address, though. A bidder is an address and each address commits once, so one operator with two addresses is a two-unit bidder and the tender cannot tell.

Small example with units = 2 and r = 100. B reveals 50, C reveals 80. Operator O has cost 30 per unit and committed 30 from two addresses.

O opens winners price O’s profit
both O1, O2 50 40
one O1, B 80 50

That is the demand reduction the Rationale warns about, and the reveal phase lets O choose it after seeing B and C, for one forfeited bond. The commit-reveal paragraph covers Vickrey and first-price but not this case. Simultaneous reveals remove the after-the-fact choice but not the incentive, since O can make the same cut by committing once.

So uniform-price on an open tender either needs an admission rule binding one unit to one operator, or the Rationale should say the per-address scope does not hold without one.

1 Like

Thanks, you’re right on both counts, and the first was a real bug, not just a gap in the text. The Finalizing paragraph said “conditions 2 to 4”, but its own parenthetical skipped condition 2, the implementation followed the parenthetical, and nothing checked that a winner had revealed at all.

Both are fixed. finalize now checks every award against the revealed bids: the winner revealed a bid in this tender, that bid is at most the reserve, and the price lies between the bid and the reserve. The last part is a new condition 7, no winner paid less than it asked, which every normative rule already satisfies. As you said, none of these depends on the pricing rule. Foundry tests cover three faulty mechanisms, one for each check.

On immutability, the spec now says award’s output must depend only on its arguments and must not change over the contract’s life, so a mechanism must not be upgradeable or delegate its computation. The tender records the mechanism’s EXTCODEHASH at openTender, emits it in TenderOpened, and finalize reverts if it has changed. That catches code replaced at the same address but, as you point out, not a proxy, whose hash stays fixed while its implementation moves. Security Considerations now says so plainly: bidders should check that the code at the pinned hash computes the result itself, for example against a known reference deployment, rather than trust mechanismId, which any contract can return. If you know a way to enforce “no delegation” on chain that isn’t brittle, I’d like to hear it; scanning bytecode for DELEGATECALL also matches data bytes.

Credited in the repo README: GitHub - shentonyan/sealed-bid-award-erc · GitHub

1 Like

You’re right. The tender sees addresses, not operators, so “one unit per bidder” is really one unit per address, and the rationale claimed more than the contract enforces. I reproduced it by brute force. With units = 2, an operator whose cost is 10 facing rivals at 10 and 30 gets 0 by bidding 10 from both addresses, and 20 by bidding 10 and 30, because its second address sets the clearing price. And as you note, sealing the bids doesn’t help, since the operator can choose after seeing the earlier reveals.

For the draft I took your second option. The Rationale now says uniform-price is truthful only where an admission layer binds each operator to one address, and Security Considerations tells deployments using uniform-price with units > 1 to use such a layer or accept the weakness. I’d rather not put operator identity into this ERC itself, since it needs an identity or attestation scheme underneath. An admission slot like the one the ERC-8414 author has listed as a v2 candidate would be a natural place to plug one in.

One thing works in the default’s favour: single-unit award.vickrey is immune. A second identity only adds another bid, which can lower the lowest competing bid but never raise it, so it can never raise the price the operator is paid. The same test checks that across 2,592 cases. The general problem is false-name bidding (Yokoo, Sakurai and Matsubara 2004), now cited in the draft.

Good case. I’d freeze earlier than award time: at opening. Bidders price the task they saw when they committed, so a change at any point after the first commitment, before or after the award, means the bids no longer price the same work. A requester who can change the task after seeing the bids is renegotiating with the winner’s revealed cost in hand, which is the ex-post discretion the price-binding profile is meant to rule out.

So in the companion the rule I’d write is: an award applies only to the task content committed when the tender opened, and changing that content means a new tender. The two 8414 routes enforce it differently:

  • Award-gated (token minted first, fixed reward): the gate should check that the task’s content still matches the hash the tender was opened against, so a changed task voids the award’s eligibility instead of carrying it over.
  • Award-then-mint (price-binding): the adapter records the task hash at opening, so the content is fixed before any bid, but your question shows a gap in my reference adapter. It mints with the requester as update authority, so if that authority can change the task after the mint, the requester could alter the work after the award. The fix is for the adapter to keep the update authority itself, or to mint with none, until the job settles.

@Garyyang, what exactly can the update authority change after minting? If it can’t touch anything bidders price, the second route is already safe and only the first needs the content check.