1// Package dutchauction ports the classic Solidity Dutch-auction pattern to2// gno.land: a seller lists an item at a high starting price that decays3// linearly, block by block, down to a floor price. The first buyer to call4// Buy pays whatever the price is at that moment — real ugnot, escrowed by5// the chain with the transaction and forwarded to the seller on settlement.6// Overpayment is refunded automatically. State-mutating functions are7// crossing functions (`cur realm`) per the gno 0.9 interrealm convention.8package dutchauction910import (11 "errors"12 "strconv"13 "strings"1415 "chain"16 "chain/banker"17 "chain/runtime"18 "chain/runtime/unsafe"1920 "gno.land/p/nt/avl/v0"21)2223const denom = "ugnot"2425type status int2627const (28 statusActive status = iota29 statusSold30 statusCancelled31)3233func (s status) String() string {34 switch s {35 case statusActive:36 return "active"37 case statusSold:38 return "sold"39 case statusCancelled:40 return "cancelled"41 default:42 return "unknown"43 }44}4546// auction is one listing: price falls linearly from StartPrice to47// FloorPrice over Duration blocks starting at StartHeight, then holds flat48// at FloorPrice until bought or cancelled.49type auction struct {50 ID string51 Item string52 Seller address53 StartPrice int6454 FloorPrice int6455 StartHeight int6456 Duration int6457 Status status58 Buyer address59 SoldPrice int6460}6162var (63 auctions avl.Tree // id string -> *auction64 nextID uint6465 totalListed uint6466 totalSold uint6467 totalVolume int6468)6970var (71 errEmptyItem = errors.New("dutchauction: item description required")72 errBadPrice = errors.New("dutchauction: startPrice must be greater than floorPrice, floorPrice must be >= 0")73 errBadDuration = errors.New("dutchauction: durationBlocks must be positive")74 errNotFound = errors.New("dutchauction: auction not found")75 errNotActive = errors.New("dutchauction: auction is not active")76 errNotSeller = errors.New("dutchauction: caller is not the seller")77 errNotUserCall = errors.New("dutchauction: buy must be a direct user transaction")78 errUnderpaid = errors.New("dutchauction: payment below current price")79 errSpoofedRealm = errors.New("dutchauction: spoofed realm")80)8182// caller authenticates the crossing frame and returns the immediate caller.83func caller(cur realm) address {84 if !cur.IsCurrent() {85 panic(errSpoofedRealm)86 }87 return cur.Previous().Address()88}8990// List creates a new Dutch auction for item, starting at startPrice ugnot91// and falling linearly to floorPrice over durationBlocks. Returns the new92// auction's ID. Crossing function.93func List(cur realm, item string, startPrice, floorPrice, durationBlocks int64) string {94 seller := caller(cur)9596 item = strings.TrimSpace(item)97 if item == "" {98 panic(errEmptyItem)99 }100 if floorPrice < 0 || startPrice <= floorPrice {101 panic(errBadPrice)102 }103 if durationBlocks <= 0 {104 panic(errBadDuration)105 }106107 nextID++108 id := strconv.FormatUint(nextID, 10)109 auctions.Set(id, &auction{110 ID: id,111 Item: item,112 Seller: seller,113 StartPrice: startPrice,114 FloorPrice: floorPrice,115 StartHeight: runtime.ChainHeight(),116 Duration: durationBlocks,117 Status: statusActive,118 })119 totalListed++120 return id121}122123// Buy purchases auction id at its current price. The caller must send at124// least that many ugnot with the transaction; any excess is refunded125// immediately. Only a direct EOA transaction (MsgCall) may buy, so the126// payment envelope can't be spoofed via an ephemeral MsgRun realm. Returns127// the price actually paid. Crossing function.128func Buy(cur realm, id string) int64 {129 if !cur.Previous().IsUserCall() {130 panic(errNotUserCall)131 }132 buyer := cur.Previous().Address()133134 a := getAuction(id)135 if a.Status != statusActive {136 panic(errNotActive)137 }138139 price := priceAt(a, runtime.ChainHeight())140 sent := unsafe.OriginSend()141 paid := sent.AmountOf(denom)142 if paid < price {143 panic(errUnderpaid)144 }145146 a.Status = statusSold147 a.Buyer = buyer148 a.SoldPrice = price149 totalSold++150 totalVolume += price151152 bnk := banker.NewBanker(banker.BankerTypeOriginSend, cur)153 pkgAddr := cur.Address()154 if paid > price {155 bnk.SendCoins(pkgAddr, buyer, chain.NewCoins(chain.NewCoin(denom, paid-price)))156 }157 bnk.SendCoins(pkgAddr, a.Seller, chain.NewCoins(chain.NewCoin(denom, price)))158159 return price160}161162// Cancel withdraws an active auction. Only the seller may cancel, and only163// before it's bought. Crossing function.164func Cancel(cur realm, id string) {165 who := caller(cur)166167 a := getAuction(id)168 if a.Status != statusActive {169 panic(errNotActive)170 }171 if who != a.Seller {172 panic(errNotSeller)173 }174 a.Status = statusCancelled175}176177// CurrentPrice returns id's price at the current chain height.178func CurrentPrice(id string) int64 {179 return priceAt(getAuction(id), runtime.ChainHeight())180}181182// AuctionInfo returns a snapshot of auction id.183func AuctionInfo(id string) (item string, seller address, status string, currentPrice int64, floorPrice int64, startPrice int64) {184 a := getAuction(id)185 return a.Item, a.Seller, a.Status.String(), priceAt(a, runtime.ChainHeight()), a.FloorPrice, a.StartPrice186}187188// priceAt computes the linear-decay price of a at the given height. Once189// settled (sold or cancelled), the price is frozen: SoldPrice for a sale,190// zero for a cancellation.191func priceAt(a *auction, height int64) int64 {192 if a.Status != statusActive {193 return a.SoldPrice194 }195 elapsed := height - a.StartHeight196 if elapsed <= 0 {197 return a.StartPrice198 }199 if elapsed >= a.Duration {200 return a.FloorPrice201 }202 drop := a.StartPrice - a.FloorPrice203 return a.StartPrice - drop*elapsed/a.Duration204}205206func getAuction(id string) *auction {207 v := auctions.Get(id)208 if v == nil {209 panic(errNotFound)210 }211 return v.(*auction)212}213214// Render produces the gnoweb Markdown view: a table of every auction,215// newest first, with live prices. Not a crossing function.216func Render(path string) string {217 var b strings.Builder218 b.WriteString("# Dutch Auction\n\n")219 b.WriteString("A price that only ever falls. List an item high; it decays block by ")220 b.WriteString("block toward a floor; the first `Buy` call wins it at whatever the ")221 b.WriteString("price is at that instant. Real ugnot, escrowed by the chain and paid ")222 b.WriteString("straight to the seller — overpayment is refunded automatically.\n\n")223224 b.WriteString("- **Total listed:** " + strconv.FormatUint(totalListed, 10) + "\n")225 b.WriteString("- **Total sold:** " + strconv.FormatUint(totalSold, 10) + "\n")226 b.WriteString("- **Total volume:** " + strconv.FormatInt(totalVolume, 10) + " ugnot\n")227 b.WriteString("- **Current height:** " + strconv.FormatInt(runtime.ChainHeight(), 10) + "\n\n")228229 if auctions.Size() == 0 {230 b.WriteString("_No auctions yet — call `List` to open one._\n")231 return b.String()232 }233234 b.WriteString("## Auctions\n\n")235 b.WriteString("| ID | Item | Status | Price now | Floor | Start | Seller |\n")236 b.WriteString("| ---: | --- | --- | ---: | ---: | ---: | --- |\n")237238 height := runtime.ChainHeight()239 ids := make([]uint64, 0, auctions.Size())240 auctions.Iterate("", "", func(key string, value any) bool {241 n, _ := strconv.ParseUint(key, 10, 64)242 ids = append(ids, n)243 return false244 })245 for i := len(ids) - 1; i >= 0; i-- {246 id := strconv.FormatUint(ids[i], 10)247 v := auctions.Get(id)248 a := v.(*auction)249 b.WriteString("| " + a.ID + " | " + a.Item + " | " + a.Status.String() +250 " | " + strconv.FormatInt(priceAt(a, height), 10) +251 " | " + strconv.FormatInt(a.FloorPrice, 10) +252 " | " + strconv.FormatInt(a.StartPrice, 10) +253 " | `" + a.Seller.String() + "` |\n")254 }255256 return b.String()257}258AuctionInfo(id string) (item string, seller string, status string, currentPrice int64, floorPrice int64, startPrice int64)
Buy(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string) int64
Cancel(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string)
CurrentPrice(id string) int64
List(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, item string, startPrice int64, floorPrice int64, durationBlocks int64) string
Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.