← Back to Realms

Realm

gno.land/r/moul/x/daily/dutchauction/v0

Overview

Realm Path
gno.land/r/moul/x/daily/dutchauction/v0
Exported Functions
6
State Entries
20
Source Files
2
Total Package Entries
29

Exported Functions

6 exported functions

State

20 state entries

Source Code

FILES
dutchauction.gno
go
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}
258

Raw Package Data

Raw JSON data