core.gno
6.61 Kb · 192 lines
1// Package core is the platform's immutable ledger (design §2.5.2): every
2// collection, its official grc721 token and royalty, its provenance
3// (commit–reveal v2), the engine registry, fees and the admin timelock.
4//
5// What core guarantees, whoever holds the admin key:
6//
7// - I-1 admin cannot move, approve or burn a token, and holds no funds;
8// the op list is a closed set of parameters (admin.gno).
9// - I-2 pause only gates create, mint and trade. Transfer, burn,
10// finalize, distribute and every cancel stay open. A pause expires by
11// itself, cannot be renewed while it runs, and each scope has a budget
12// of MaxPauseBlocks per PauseWindowBlocks (SA-L2).
13// - I-5 supply only grows through the collection's bound minter, never
14// past maxSupply, and a token id is never minted twice.
15// - I-6 after Launch the engine, params, supply, payees, sale window and
16// commitments are frozen.
17// - I-10/11 a collection becomes Final at most once, only with both
18// reveals or after revealDeadline, and that deadline is bounded, so
19// the pre-reveal transfer lock always ends.
20// - I-12 engine code never runs on a state-changing path, except the
21// creator's own CreateCollection (ValidateParams) and the engine's own
22// RegisterEngine (one sample render). Every exported non-crossing
23// function is read-only, so an engine cannot call back into a mutator.
24// - I-13 every stored string passed safe.AssertText.
25//
26// core imports no mutable realm: minters and engines are known only by
27// package path and interface value.
28package core
29
30import (
31 "chain"
32 "chain/runtime"
33 "chain/runtime/unsafe"
34 "strconv"
35
36 "gno.land/p/nt/avl/v0"
37 "gno.land/p/nt/grc721/royalty/v0"
38 "gno.land/p/nt/grc721/v0"
39
40 "gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/safe/v1"
41 "gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/types/v1"
42)
43
44// Hard limits. They are constants on purpose: nobody, admin included, can
45// raise them after deploy (block counts assume ~3.54 s blocks, RA M-1).
46const (
47 ProtocolFeeCapBps = 500 // 5% of any sale
48 RoyaltyCapBps = 1000 // 10% (RA M-3)
49 CreateFeeCap = 1_000_000_000 // 1,000 GNOT in ugnot
50 MaxPayees = 4
51 MaxSupplyCap = 10_000
52 MaxMintPerTx = 10 // hypothesis: measure n=10 gas/deposit on Onyx
53 MaxReserveBps = 1000 // creator reserve <= 10% of supply
54 MaxSaleBlocks = 732_000 // ~30 days
55 RevealWindowMin = 1_017 // ~1 hour
56 RevealWindowMax = 170_000 // ~7 days
57 BeaconGraceBlocks = 254 // beacon may reveal first in the window's last ~15 min (< RevealWindowMin, so the creator always gets to go first)
58 TimelockBlocks = 48_800 // ~48 hours
59 MaxPauseBlocks = 195_000 // ~8 days: pause budget per scope and window
60 PauseWindowBlocks = 732_000 // ~30 days (SA-L2)
61
62 MaxNameLen = 64
63 MaxDescriptionLen = 1024
64 MaxParamsLen = 1024
65 MinSecretLen = 32
66 MaxSecretLen = 256
67 MaxPageSize = 50
68)
69
70// Provenance badges (design §4.3).
71const (
72 BadgeBoth = "2/2"
73 BadgeCreatorOnly = "creator-only"
74 BadgeBeaconOnly = "beacon-only"
75 BadgeFallback = "fallback"
76)
77
78type collection struct {
79 id, name, symbol, description string
80 creator, pendingCreator address
81 engineID, params string
82 maxSupply int64
83 nextTID int64 // next token id; minted so far = nextTID-1
84 reserved int64
85 royaltyBps int64
86 primaryPayees, royaltyPayees []types.Payee
87 minter string
88 state types.State
89 startHeight, endHeight int64
90 revealWindow int64
91 closeHeight int64 // sell-out height, or endHeight once time ran out
92 useBeacon bool
93 createdHeight int64
94 primaryBps int64 // protocol fee snapshot at Launch (SA-I5)
95 burnedCommits []string // commitments whose secret went public; never accepted again here (SA-L1)
96 approvals avl.Tree // "t:"+tid or "o:"+owner+":"+operator -> int64 nonce (SA-L3)
97 prov *provenance
98 token *grc721.Token
99 ledger *grc721.PrivateLedger
100 roy *royalty.Royalty
101 st *tokenState
102 mintedBy avl.Tree // address -> int64, lifetime, written on every mint (I-9)
103}
104
105func (c *collection) minted() int64 { return c.nextTID - 1 }
106
107var (
108 // selfPath is this realm's package path, captured once in init so read
109 // functions can derive sub-addresses without a cur.
110 selfPath string
111
112 collections avl.Tree // safe.PadInt(cid) -> *collection
113 byCreator avl.Tree // creator + ":" + padded cid -> cid
114 collCount int64
115)
116
117func init(cur realm) {
118 selfPath = cur.PkgPath()
119}
120
121const cidWidth = 10
122
123func collKey(cid string) string {
124 return safe.PadInt(safe.ParseID("cid", cid), cidWidth)
125}
126
127func mustCollection(cid string) *collection {
128 v := collections.Get(collKey(cid))
129 if v == nil {
130 panic("unknown collection " + strconv.Quote(cid))
131 }
132 return v.(*collection)
133}
134
135// stateAt is the effective state at height h; the stored state lags until
136// the next write materializes it (refresh).
137func stateAt(c *collection, h int64) types.State {
138 switch c.state {
139 case types.StateScheduled:
140 if h > c.endHeight {
141 return types.StateClosed
142 }
143 if h >= c.startHeight {
144 return types.StateMinting
145 }
146 case types.StateMinting:
147 if h > c.endHeight {
148 return types.StateClosed
149 }
150 }
151 return c.state
152}
153
154func refresh(c *collection, h int64) {
155 s := stateAt(c, h)
156 if s == c.state {
157 return
158 }
159 if s == types.StateClosed && c.closeHeight == 0 {
160 c.closeHeight = c.endHeight
161 }
162 c.state = s
163 chain.Emit("CollectionState", "cid", c.id, "state", s.String())
164}
165
166// revealDeadline is when anyone may finalize without the missing secrets.
167// It is at most endHeight + RevealWindowMax, which bounds the transfer
168// lock (I-11).
169func revealDeadline(c *collection) int64 {
170 closed := c.closeHeight
171 if closed == 0 {
172 closed = c.endHeight
173 }
174 return closed + c.revealWindow
175}
176
177func caller(cur realm) address {
178 return cur.Previous().Address()
179}
180
181// assertNoDeposit refuses coins on functions that take none, so nothing can
182// get stuck in core (RA L-1). The origin-send envelope belongs to the first
183// realm of the call; it is only ours to judge when a wallet called us.
184func assertNoDeposit(cur realm) {
185 if cur.Previous().IsUserCall() && !unsafe.OriginSend().IsZero() {
186 panic("this function does not accept coins")
187 }
188}
189
190func itoa(n int64) string { return strconv.FormatInt(n, 10) }
191
192func height() int64 { return runtime.ChainHeight() }