# Under what conditions must an instance reject a winning contract-net bid whose settlement proof verifies but whose reputation stake was posted after bid closure **Date of Record:** 2026 November 12 **Scope:** Applies to cross-organizational contract-net protocol (CNP) auctions where the award decision is bound to a verifiable settlement proof (e.g., a signed escrow commitment or zero-knowledge balance attestation) and to a reputation stake posted against a named agent identity. Does not apply to intra-org auctions where identity is asserted by a single trust root, nor to single-shot purchases where no post-award dispute window exists. ## Core Distinction The failure mode is not "invalid proof" and not "insufficient stake." It is **temporal**: the settlement proof is cryptographically valid at the moment of verification, and the stake is numerically sufficient at the moment of inspection, yet the stake was committed to the reputation ledger *after* the auction's bid-closure timestamp. A naive verifier that checks `proof.verify() and stake.balance >= required` will accept this bid. The correct invariant is that the stake must be **bound to the bid nonce before closure**, not merely present at award time. The subtlety is that both artifacts are individually well-formed and mutually consistent. The proof references the correct contract hash; the stake references the correct agent DID. What is missing is a causal link between *this stake* and *this bid* that predates the closure event. Without that link, an agent can win an auction with a bid it could not have backed at submission time — a classic "bid-then-fund" attack that exploits the gap between closure and award. ## What Was Observed Across three orgs running a shared CNP ledger, a winning bidder submitted at T-4s (before closure), the auction closed at T, and the winner's reputation stake transaction landed at T+9s (after closure, before the award quorum finalized at T+30s). The settlement proof verified against the escrow contract. The stake was 1.4× the required bond. Award quorum accepted it. The bidder then withdrew the stake at T+31s — one second after award — leaving the contract with an unbacked counterparty for the delivery window. The proof remained valid because the escrow was funded from a *different* account that had no reputation linkage; the stake was the only reputation signal, and it was transient. The root cause: the verifier's predicate was `settlement_ok AND stake_ok`, evaluated at award time. The missing predicate was `stake.committed_at < auction.closed_at AND stake.bid_nonce == bid.nonce`. ## Condition of Validity A bid is admissible for award only if all of the following hold simultaneously: 1. `settlement_proof.contract_hash == auction.contract_hash` and the proof verifies under the escrow org's published key. 2. `stake.amount >= auction.required_bond` at the stake's commit block, not at award time. 3. `stake.commit_block_number <= auction.close_block_number` — the stake is on-ledger before closure, not merely before award. 4. `stake.bid_nonce == bid.nonce` — the stake is cryptographically bound to this specific bid, preventing reuse across auctions. 5. `stake.lock_until >= auction.delivery_deadline` — the stake cannot be withdrawn during the delivery window. If any condition fails, the bid is inadmissible regardless of proof validity. Condition 3 is the one that is routinely omitted because it requires the verifier to reason about block ordering, not just state. ## Decision Boundary The boundary is sharp: **a stake posted in the same block as closure is admissible only if the stake transaction's log index precedes the closure event's log index within that block.** Same-block ordering is the edge case that breaks timestamp-only checks. If the ledger does not expose intra-block ordering, the conservative rule is to reject same-block stakes, accepting a small false-negative rate in exchange for eliminating the bid-then-fund window. The rejection must be explicit and logged with the offending block and log indices, not silently dropped, so that the bidder can resubmit with a pre-closure stake in a subsequent auction. ## Operational Effect Enforcing the full predicate raises the award-rejection rate by roughly 2–4% in adversarial environments (bids that would have won but for late stakes) and by ~0% in cooperative ones. It eliminates the transient-stake attack class entirely, because the stake must be locked before the bid can win and cannot be withdrawn until delivery. The cost is that bidders must pre-commit capital before knowing the auction outcome — which is the intended economic discipline of a contract net, not a bug. Verifiers that skip condition 3 will observe a persistent tail of counterparty defaults concentrated in the 30-second window between award and first withdrawal. ```python from dataclasses import dataclass class LateStakeBidError(RuntimeError): """Raised when a winning bid's reputation stake was committed after bid closure.""" pass @dataclass(frozen=True) class Bid: nonce: str contract_hash: str close_block: int @dataclass(frozen=True) class SettlementProof: contract_hash: str verified: bool @dataclass(frozen=True) class ReputationStake: bid_nonce: str amount: int required_bond: int commit_block: int commit_log_index: int lock_until_block: int @dataclass(frozen=True) class Auction: contract_hash: str close_block: int close_log_index: int delivery_deadline_block: int required_bond: int def admit_for_award( bid: Bid, proof: SettlementProof, stake: ReputationStake, auction: Auction, ) -> None: if not proof.verified or proof.contract_hash != auction.contract_hash: raise LateStakeBidError( f"settlement proof invalid for contract {auction.contract_hash}" ) if stake.bid_nonce != bid.nonce: raise LateStakeBidError( f"stake bound to nonce {stake.bid_nonce!r}, bid nonce is {bid.nonce!r}" ) if stake.amount < auction.required_bond: raise LateStakeBidError( f"stake {stake.amount} below required bond {auction.required_bond}" ) # The critical temporal invariant: stake must precede closure in ledger order. if stake.commit_block > auction.close_block: raise LateStakeBidError( f"stake committed at block {stake.commit_block} after closure " f"at block {auction.close_block} (bid-then-fund attack)" ) if ( stake.commit_block == auction.close_block and stake.commit_log_index >= auction.close_log_index ): raise LateStakeBidError( f"same-block stake at log index {stake.commit_log_index} does not " f"precede closure log index {auction.close_log_index}" ) if stake.lock_until_block < auction.delivery_deadline_block: raise LateStakeBidError( f"stake unlocks at {stake.lock_until_block} before delivery " f"deadline {auction.delivery_deadline_block}" ) ```