Cosmic Bull

sealedbid

gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/sealedbid on onyx-1 testnet

realmapplicationauctionslive on onyx-1

Sell to the highest bidder, with every bid sealed until bidding closes.

Because the winner pays the runner-up's price, a bidder can name what the lot is really worth to them without overpaying for it.

In detail curated

SEALED-BID SECOND-PRICE (VICKREY) SALE by commit-reveal, with ESCROWED deposits and PULL settlement. It is the portfolio's second auction and deliberately the OTHER mechanism: r/auction is an open ascending-bid sale where every bid is public the moment it lands and the winner pays WHAT THEY BID, so a bidder's willingness to pay is revealed to rivals in time to be used against them. Here each bidder posts a hash during the commit phase, reveals in a second phase, and THE WINNER PAYS THE SECOND-HIGHEST REVEALED BID -- so truthful bidding is the dominant strategy and the price is set by the runner-up rather than by the winner's own number. Two phases with distinct minimum lengths (MinCommitBlocks 10, MinRevealBlocks 20) are what makes that possible on a public chain: a bid is a 32-byte sha256 commitment over a length-prefixed (amount, salt) pair, so the amount is not readable from chain state until its owner publishes the salt. CUSTODY: Held == sealed + revealed + feeledger.Liabilities(), deposits are escrowed at Commit (so a commitment is backed, not merely asserted), settlement credits inside the realm and every payout is PULL (Claim / ClaimAll), and a bidder who commits and never reveals FORFEITS their deposit to the seller -- the only sanction that makes the reveal phase self-enforcing. Close is PERMISSIONLESS and was exercised live by a LOSING BIDDER, neither seller nor winner. Audit PROCEED: 0 RED, two YELLOW both REMEDIATED before deploy (Y1 slot squatting had no price; Y2 two renderers disagreed about escaping). 39-test suite green, 53-case mutation sweep FULLY COVERED with 0 uncaught and the subject restored byte-identically. BYTE-MATCH PASSED; it is the second deploy over gnomcp #107's package_dir path.

Use it

A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.

Read it — no key, no account, no GNOT

gnokey query vm/qeval \
  --data 'gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/sealedbid.FeeBps()' \
  --remote https://rpc.onyx.testnets.gno.land:443

FeeBps is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.

Change its state

Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 13 crossing functions, with the argument names filled in.

Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.

Import it from another realm

import "gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/sealedbid"

The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.

Deployments

onyx-1 testnet onyx-1

RenderedSourceCall builderState

chain-attested
Import pathgno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/sealedbid
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Deployed at height558,757
Deploy transactionf188a58f83b1352f30ef0258a1daab7ccc07c91c870c908683868254f5a8709a look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used54,605,981
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml sealedbid.gno
Deployed bytessealedbid.gno — 40,774 bytes
sha256f07f0c7fecad6368369521881d8e5dc1aa2f747be68f4ffd9a0fad84e7d78b03
Realm addressg1hkzn29tgvl66hmx6pn8uqn0f87wu653cgt0zan

Do not take the hash above on trust. $download returns the bytes onyx-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://onyx.testnets.gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/sealedbid$download&file=sealedbid.gno' | shasum -a 256

Expected: f07f0c7fecad6368369521881d8e5dc1aa2f747be68f4ffd9a0fad84e7d78b03 — 40,774 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 44 exported functions.

Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:

AcceptAdminBidsOnBlocksLeftCancelClaimClaimAllClaimableOfCloseCommitCommitHashCommitmentOfCreateSaleDepositOfFeeBpsFeesAccruedForfeitedOfForfeitedTotalHasRevealedHasSaleHeightHeldIsCloseableLiabilitiesLotMinDepositNumSalesPhasePriceProvisionalPriceRenderRevealRevealedRevealedAmountOfRevealedOnSalesOfSealedSecondBidSetFeeBpsSetFeeRecipientStatusSweepDenomTransferAdminWinningBidWithdrawFees

Overview

Realm sealedbid is a SEALED-BID SECOND-PRICE (Vickrey) auction settled in ugnot, holding real coins. A SELLER lists a lot; BIDDERS commit to a price they never disclose, by publishing only sha256(amount, salt) and escrowing a deposit; after the commit window closes they OPEN their bids by revealing (amount, salt); the highest revealed bid wins and the winner pays the SECOND highest price, their deposit refunded down to it.

It is a different clearing rule and a different threat model from this portfolio's other market realm. r/auction is ascending English open-outcry: every bid is public the instant it lands, bidders react to each other, and the winner pays their own bid. Here no bidder learns another's number until the commit window has shut, so there is nothing to react to and nothing to front-run; and because the winner pays the runner-up's price, bidding one's true valuation is the dominant strategy rather than a mistake. The two realms answer the same question — who gets the lot, at what price — by mechanisms that do not resemble each other.

COMMIT-REVEAL is the mechanism this portfolio did not have. Nothing in it hashed anything, on any of the three trees, before this realm.

Custody — the conservation invariant is the product

A custodial realm, so the monetary invariant governs it as it does r/loans, r/auction and r/deposits:

Held == sealed + revealed + feeledger.Liabilities()

where `sealed` is the sum of deposits behind commitments that have NOT been opened in a non-terminal sale, `revealed` the sum of deposits behind commitments that HAVE been opened and are awaiting close, and the ledger holds every claimable kind — losing-bid refunds, the winner's change, seller proceeds — plus the fee pot. The two held buckets are disjoint by construction: opening a bid moves its deposit from the first to the second, never duplicating it, and Close drains both into the ledger. Every value transition preserves the equality; see the DELTA-form conservation test.

CORRECTION, recorded rather than shipped silently: the §7 discovery gate for this realm predicted THREE disjoint held buckets, naming `forfeited` as the third. Architecture refuted that. A forfeited deposit is credited to the seller at Close in the same transaction that voids the commitment, so it is never held as a distinct balance across any two transactions — there is no third bucket to be disjoint from. `forfeited` survives as a CUMULATIVE STATISTIC (ForfeitedOf, ForfeitedTotal), which is a report and not a custody claim. The gate's file is not edited; this is the amendment.

Payouts are PULL, never push. Close credits balances and sends nothing, so a hostile or contract-dead loser cannot make Close fail for everyone else, and the settlement path has no cross-call reentrancy surface.

Non-revelation, and why withholding never pays

A sealed-bid auction has a liveness problem an ascending one does not: a bidder who dislikes the outcome can simply never open their bid, stalling settlement or distorting the price. The answer here is adopted by citation rather than invented — r/moul/agents/bubblerumble6 reached it independently, and its policy is the right one: a commitment not opened in its window is VOID, nothing is refunded, and the deposit is credited to the seller. So withholding a reveal costs the withholder their whole deposit and pays them nothing, and the auction always closes: Close is permissionless and needs no bidder to act.

Two consequences of that policy are hazards, stated rather than discovered:

Privacy — what is hidden, and what is not

The BID AMOUNT is hidden until the bidder opens it. The DEPOSIT is not: it is a coin transfer and is public, and since a valid bid cannot exceed its deposit, the deposit leaks an UPPER BOUND on the bid. A bidder who wants more privacy than that over-deposits and takes the change back at Close. This is the real ceiling of the scheme and is stated here because no amount of hashing moves it.

The salt is what keeps the commitment from being brute-forced over the plausible round numbers, so MinSaltLen bytes are required. The realm can check a salt's LENGTH and can never check its ENTROPY: a 16-byte salt of "aaaaaaaaaaaaaaaa" satisfies the guard and protects nothing. Use random bytes. Compute the commitment locally, or with a READ-ONLY query against CommitHash — never in a transaction, which would publish the salt and defeat the whole mechanism.

What the realm settles, and what it does NOT

The realm settles PAYMENT and records the WINNER; it does not custody or deliver the lot, which is free text and changes hands off-chain (or by a downstream realm reading [Winner]). The same scope decision r/auction states, for the same reason: an on-chain-asset auction is a v2 and this v1 omits the asset-escrow surface deliberately.

REALM-CALLER CAVEAT, inherited from the siblings: coinio's receipt guard admits only EOA payers, so every BIDDER is an EOA. The SELLER is whoever calls CreateSale; a realm may sell, but a realm seller must expose its own crossing path to Claim or its proceeds strand — the seller's concern, not a third-party hazard. assertNoSend reads the ORIGIN envelope, so every non-payable function refuses a transaction that attached coins anywhere.

Why crypto/sha256 directly

The discovery gate found genuine prior art for the hashing scheme — p/moul/agents/commit/v0, whose Hash is LENGTH-PREFIXED so that Hash("ab","c") differs from Hash("a","bc") — and deferred the import-or- reimplement choice to architecture. It is resolved here in favour of calling crypto/sha256 directly and FOLLOWING that package's scheme, because the package resolves to a DIFFERENT IMPORT PATH on each chain this portfolio targets, and the namespace rewriter substitutes only our own address. An import of it would make the mainnet twin of this realm non-substitutable, which is a structural cost that 1.4 KB of saved code does not buy.

Imports

Constants and variables

Sale status values. Only Open is non-terminal; the phase inside Open is derived from the chain height, never stored.

const (
	SaleOpen      = "open"      // in its commit, reveal, or awaiting-close phase
	SaleSettled   = "settled"   // closed WITH a winner; proceeds credited
	SaleUnsold    = "unsold"    // closed with no valid opening, or below reserve
	SaleCancelled = "cancelled" // seller cancelled before any commitment
)

Phase values returned by Phase for a sale that is still Open.

const (
	PhaseCommit    = "commit"    // accepting commitments
	PhaseReveal    = "reveal"    // accepting openings
	PhaseCloseable = "closeable" // reveal window shut; Close will succeed
)

Input bounds.

const (
	// MinBid is the dust floor for any revealed bid, any deposit and any
	// non-zero reserve; MaxBid keeps every bid and the fee split far from
	// int64 overflow. The bidder must ESCROW at least their bid, so the real
	// ceiling is their balance.
	MinBid = int64(1_000)
	MaxBid = int64(1_000_000_000_000) // 1,000,000 GNOT

	// Window bounds, in blocks. The reveal floor is higher than the commit
	// floor on purpose: failing to commit costs nothing, while failing to
	// reveal costs the whole deposit, so the window in which that can happen
	// gets the wider floor. See the short-reveal-window hazard above.
	MinCommitBlocks = int64(10)
	MinRevealBlocks = int64(20)
	MaxWindowBlocks = int64(7_500_000)

	// CommitmentLen is the length of a commitment string: sha256 as 64
	// lowercase hex characters, exactly as [CommitHash] emits it.
	CommitmentLen = 2 * sha256.Size

	// MinSaltLen is the required salt length. It bounds brute force over
	// plausible bid amounts; it cannot bound a caller's choice of entropy.
	MinSaltLen = 16
	MaxSaltLen = 128

	// MaxLotLen bounds the stored lot description.
	MaxLotLen = 200

	// MaxBiddersPerSale bounds the commitments on one sale. It is the reason
	// the duplicate-commitment scan and the Close payout loop may be linear:
	// both are O(MaxBiddersPerSale) and neither is caller-extensible.
	//
	// It is ALSO a scarce resource, which is why a sale carries minDeposit.
	// A slot is never released — there is no withdraw-commitment path — so
	// without a seller-sized entry price, 25 addresses committing MinBid
	// each fill a sale for 25,000ugnot, reveal MinBid, and are refunded in
	// full fee-free: an honest bidder is locked out for the cost of gas.
	// Audit Y1, and the reason minDeposit exists. See [CreateSale].
	MaxBiddersPerSale = 25

	// MaxSalesPerSeller bounds one seller's stored sales; terminal sales do
	// NOT free the slot, because a settled/unsold/cancelled sale is a
	// permanent record a bidder and an auditor may need to read.
	MaxSalesPerSeller = 50

	// RenderLimit bounds every rendered list — an unbounded Render was YELLOW
	// in three prior audits.
	RenderLimit = 20
)

Denom is the only asset this realm accepts.

const Denom = "ugnot"

MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%. The fee applies only to the seller's PROCEEDS at close — never to a refund, never to the winner's change, and never to a forfeited deposit (see Close).

const MaxFeeBps = int64(1000)

Functions

AcceptAdmin

func AcceptAdmin(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as AcceptAdmin(cross(cur), ...).

BidsOn

func BidsOn(saleID int64) int64

BidsOn is how many commitments a sale holds; RevealedOn how many are opened.

BlocksLeft

func BlocksLeft(saleID int64) int64

BlocksLeft is how many blocks remain in the sale's current window (0 once the reveal window has shut or the sale is terminal).

Cancel

func Cancel(cur realm, saleID int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Cancel(cross(cur), ...).

Cancel withdraws a sale that has taken no commitment. Seller only. Once any commitment stands the sale can only run to Close — a seller cannot pull the lot out from under an escrowed bidder, and cannot strand a deposit.

Claim

func Claim(cur realm, amount int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Claim(cross(cur), ...).

Claim sends amount ugnot of the caller's claimable balance — refunds, winner's change, and seller proceeds — to the caller.

ClaimAll

func ClaimAll(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as ClaimAll(cross(cur), ...).

ClaimAll sends the caller's entire claimable balance to the caller.

ClaimableOf

func ClaimableOf(a address) int64

Close

func Close(cur realm, saleID int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Close(cross(cur), ...).

Close settles a sale whose reveal window has shut. Anyone may call it; no sale's terminal state may depend on a party acting.

The clearing rule: among the OPENED bids the highest wins and pays max(second-highest, reserve, MinBid) — a strict Vickrey price, floored so that a lone bidder does not win for nothing and so that a reserve is honoured as a price and not merely as a veto. Ties on the highest amount go to the EARLIEST opening, which makes the tie-break deterministic and independent of storage order, and a tie sets the second price equal to the first, so a tied winner pays their full bid.

The money, all of it credited and none of it sent:

If no bid was opened, or the highest opened bid is below the reserve, the sale is Unsold: every opened bid is refunded in full and only the unopened commitments forfeit.

Commit

func Commit(cur realm, saleID int64, commitment string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Commit(cross(cur), ...).

Commit escrows a deposit behind a sealed bid. The caller attaches the deposit and supplies only `commitment` — CommitHash(amount, salt) as 64 lowercase hex characters — so the realm learns the bid's CEILING (the deposit) and not the bid. The deposit is the ceiling on what can later be opened, so escrow at least what you intend to bid; escrow more to hide it better. One commitment per bidder per sale, and a commitment already present on this sale is refused (copying another bidder's hash without knowing its contents would otherwise be free and could never be opened).

CommitHash

func CommitHash(amount int64, salt string) string

CommitHash returns the commitment for a bid of `amount` under `salt`: the sha256 of a LENGTH-PREFIXED encoding of the two parts, as 64 lowercase hex characters. The length prefixes are what make the encoding unambiguous — without them a commitment to (12, "34salt...") and one to (1, "234salt...") could collide, and a bidder could open a commitment as a bid they never made. The scheme follows p/moul/agents/commit/v0; see the package doc for why it is reimplemented rather than imported.

This is a VIEW. Compute your commitment locally or with a read-only query — calling it inside a transaction publishes the salt and destroys the seal.

CommitmentOf

func CommitmentOf(saleID int64, bidder address) string

CommitmentOf returns the bidder's stored commitment on a sale ("" if none).

CreateSale

func CreateSale(cur realm, lot string, reserve, minDeposit, commitBlocks, revealBlocks int64) int64

Crossing function. Callable from a transaction via MsgCall, and from another realm as CreateSale(cross(cur), ...).

CreateSale lists a lot for sealed-bid second-price sale. The caller becomes the seller. `reserve` is 0 (no reserve) or a floor in [MinBid, MaxBid] that the HIGHEST revealed bid must meet for the sale to settle. Commitments are accepted for `commitBlocks` blocks, then openings for `revealBlocks` blocks, after which anyone may Close. The current protocol fee is snapshotted in. Returns the id.

`minDeposit` is the ENTRY PRICE per commitment, and it is the seller's one defence against slot squatting: a sale has at most MaxBiddersPerSale slots, a slot is never released, and a squatter who reveals a dust bid is refunded in full fee-free — so with no entry price a rival locks an honest bidder out of any sale for the cost of gas. Size it to the lot, not to the dust floor. 0 means "no preference", resolved to the max(MinBid, reserve) floor, which is the weakest setting and leaves the squatting cost at MaxBiddersPerSale times that floor. The floor includes the reserve because a bid under the reserve cannot win, so escrowing less than the reserve buys only a slot.

DepositOf

func DepositOf(saleID int64, bidder address) int64

DepositOf returns the bidder's escrowed deposit on a sale (0 if none).

FeeBps

func FeeBps() int64

FeesAccrued

func FeesAccrued() int64

ForfeitedOf

func ForfeitedOf(saleID int64) int64

ForfeitedOf is the total deposit liquidated to the seller on a closed sale (0 before close). A cumulative statistic, not a held balance.

ForfeitedTotal

func ForfeitedTotal() int64

HasRevealed

func HasRevealed(saleID int64, bidder address) bool

HasRevealed reports whether the bidder has opened their bid on a sale.

HasSale

func HasSale(saleID int64) bool

Height

func Height() int64

Held

func Held() int64

IsCloseable

func IsCloseable(saleID int64) bool

IsCloseable reports whether Close would succeed right now.

Liabilities

func Liabilities() int64

Lot

func Lot(saleID int64) string

Lot returns the sale's free-text lot description.

MinDeposit

func MinDeposit(saleID int64) int64

MinDeposit is the resolved entry price for one commitment on this sale: the smallest deposit Commit accepts. It is at least max(MinBid, reserve) and is the seller's defence against slot squatting (audit Y1; see CreateSale).

NumSales

func NumSales() int64

Phase

func Phase(saleID int64) string

Phase reports where a sale is: "commit", "reveal" or "closeable" while it is open, else its terminal status. The phase is derived from the chain height and is never stored, so it cannot disagree with the guards.

Price

func Price(saleID int64) int64

Price is the SECOND price the winner actually paid once the sale has Settled, else 0. It is not the winning bid; see WinningBid.

ProvisionalPrice

func ProvisionalPrice(saleID int64) int64

ProvisionalPrice is what the winner would pay if the sale closed right now: max(second bid, reserve, MinBid), or 0 if nothing is opened yet or the highest opening is below the reserve.

Render

func Render(path string) string

Reveal

func Reveal(cur realm, saleID int64, amount int64, salt string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Reveal(cross(cur), ...).

Reveal opens the caller's sealed bid on a sale whose commit window has shut. `amount` and `salt` must hash to the stored commitment, `amount` must be in [MinBid, MaxBid], and `amount` must not exceed the deposit — a bidder who committed above their escrow has no valid opening and forfeits at Close. Opening does not move coins; it moves the deposit from the `sealed` bucket to the `revealed` one and records the bid for the clearing scan.

Revealed

func Revealed() int64

RevealedAmountOf

func RevealedAmountOf(saleID int64, bidder address) int64

RevealedAmountOf returns the bidder's OPENED bid amount (0 while sealed). A revealed bid is public by construction — it travelled in a transaction.

RevealedOn

func RevealedOn(saleID int64) int64

SalesOf

func SalesOf(seller address) int64

Sealed

func Sealed() int64

SecondBid

func SecondBid(saleID int64) int64

SecondBid is the second-highest OPENED bid, or 0. With a tie on the highest it equals the highest.

SetFeeBps

func SetFeeBps(cur realm, bps int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeBps(cross(cur), ...).

SetFeeRecipient

func SetFeeRecipient(cur realm, recipient address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeRecipient(cross(cur), ...).

Status

func Status(saleID int64) string

Status is the sale's STORED state -- one of "open", "settled", "unsold" or "cancelled". It answers a different question from Phase: an open sale has three distinct phases but only one status, and a closed one has a status that no longer moves with the chain height.

SweepDenom

func SweepDenom(cur realm, denom string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SweepDenom(cross(cur), ...).

SweepDenom recovers out-of-band coins to the fee recipient. For the ledger denom the reserve is sealed + revealed + Liabilities() — escrowed deposits, claimable refunds, change, proceeds and the fee pot are all structurally unreachable.

TransferAdmin

func TransferAdmin(cur realm, successor address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as TransferAdmin(cross(cur), ...).

WinningBid

func WinningBid(saleID int64) int64

WinningBid is the highest OPENED bid on a sale, available as soon as it is revealed (a revealed bid is public). It is what the winner bid, not what they pay.

WithdrawFees

func WithdrawFees(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as WithdrawFees(cross(cur), ...).


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importschain, chain/runtime, chain/runtime/unsafe, crypto/sha256, encoding/hex, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv
First-party dependenciesgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source

Source fileonyx/r/sealedbid/sealedbid.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical