Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

dutchauction.gno

7.45 Kb · 257 lines
  1// Package dutchauction ports the classic Solidity Dutch-auction pattern to
  2// gno.land: a seller lists an item at a high starting price that decays
  3// linearly, block by block, down to a floor price. The first buyer to call
  4// Buy pays whatever the price is at that moment — real ugnot, escrowed by
  5// the chain with the transaction and forwarded to the seller on settlement.
  6// Overpayment is refunded automatically. State-mutating functions are
  7// crossing functions (`cur realm`) per the gno 0.9 interrealm convention.
  8package dutchauction
  9
 10import (
 11	"errors"
 12	"strconv"
 13	"strings"
 14
 15	"chain"
 16	"chain/banker"
 17	"chain/runtime"
 18	"chain/runtime/unsafe"
 19
 20	"gno.land/p/nt/avl/v0"
 21)
 22
 23const denom = "ugnot"
 24
 25type status int
 26
 27const (
 28	statusActive status = iota
 29	statusSold
 30	statusCancelled
 31)
 32
 33func (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}
 45
 46// auction is one listing: price falls linearly from StartPrice to
 47// FloorPrice over Duration blocks starting at StartHeight, then holds flat
 48// at FloorPrice until bought or cancelled.
 49type auction struct {
 50	ID          string
 51	Item        string
 52	Seller      address
 53	StartPrice  int64
 54	FloorPrice  int64
 55	StartHeight int64
 56	Duration    int64
 57	Status      status
 58	Buyer       address
 59	SoldPrice   int64
 60}
 61
 62var (
 63	auctions    avl.Tree // id string -> *auction
 64	nextID      uint64
 65	totalListed uint64
 66	totalSold   uint64
 67	totalVolume int64
 68)
 69
 70var (
 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)
 81
 82// 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}
 89
 90// List creates a new Dutch auction for item, starting at startPrice ugnot
 91// and falling linearly to floorPrice over durationBlocks. Returns the new
 92// auction's ID. Crossing function.
 93func List(cur realm, item string, startPrice, floorPrice, durationBlocks int64) string {
 94	seller := caller(cur)
 95
 96	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	}
106
107	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 id
121}
122
123// Buy purchases auction id at its current price. The caller must send at
124// least that many ugnot with the transaction; any excess is refunded
125// immediately. Only a direct EOA transaction (MsgCall) may buy, so the
126// payment envelope can't be spoofed via an ephemeral MsgRun realm. Returns
127// 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()
133
134	a := getAuction(id)
135	if a.Status != statusActive {
136		panic(errNotActive)
137	}
138
139	price := priceAt(a, runtime.ChainHeight())
140	sent := unsafe.OriginSend()
141	paid := sent.AmountOf(denom)
142	if paid < price {
143		panic(errUnderpaid)
144	}
145
146	a.Status = statusSold
147	a.Buyer = buyer
148	a.SoldPrice = price
149	totalSold++
150	totalVolume += price
151
152	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)))
158
159	return price
160}
161
162// Cancel withdraws an active auction. Only the seller may cancel, and only
163// before it's bought. Crossing function.
164func Cancel(cur realm, id string) {
165	who := caller(cur)
166
167	a := getAuction(id)
168	if a.Status != statusActive {
169		panic(errNotActive)
170	}
171	if who != a.Seller {
172		panic(errNotSeller)
173	}
174	a.Status = statusCancelled
175}
176
177// CurrentPrice returns id's price at the current chain height.
178func CurrentPrice(id string) int64 {
179	return priceAt(getAuction(id), runtime.ChainHeight())
180}
181
182// 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.StartPrice
186}
187
188// priceAt computes the linear-decay price of a at the given height. Once
189// 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.SoldPrice
194	}
195	elapsed := height - a.StartHeight
196	if elapsed <= 0 {
197		return a.StartPrice
198	}
199	if elapsed >= a.Duration {
200		return a.FloorPrice
201	}
202	drop := a.StartPrice - a.FloorPrice
203	return a.StartPrice - drop*elapsed/a.Duration
204}
205
206func getAuction(id string) *auction {
207	v := auctions.Get(id)
208	if v == nil {
209		panic(errNotFound)
210	}
211	return v.(*auction)
212}
213
214// 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.Builder
218	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")
223
224	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")
228
229	if auctions.Size() == 0 {
230		b.WriteString("_No auctions yet — call `List` to open one._\n")
231		return b.String()
232	}
233
234	b.WriteString("## Auctions\n\n")
235	b.WriteString("| ID | Item | Status | Price now | Floor | Start | Seller |\n")
236	b.WriteString("| ---: | --- | --- | ---: | ---: | ---: | --- |\n")
237
238	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 false
244	})
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	}
255
256	return b.String()
257}