Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}