token.gno
10.34 Kb · 332 lines
1// Parametric GRC20 realm for gno.land.
2//
3// All configuration (name, symbol, decimals, supply, metadata) lives in
4// config.gno. This file holds only the logic, which is not meant to be edited.
5//
6// The realm:
7// - creates a GRC20 token through gno.land/p/nt/grc20/v0
8// - registers it with gno.land/r/nt/grc20reg/v0 (required to be discoverable
9// by GnoSwap, wallets and explorers)
10// - mints the initial supply to the deploying address
11// - exposes the GRC20 methods as top-level functions callable via MsgCall
12// - enforces a hard ceiling (maxSupply) on minting
13// - lets the owner give up minting forever (DropOwnership)
14package gnomic
15
16import (
17 "chain/runtime"
18 "chain/runtime/unsafe"
19 "math"
20 "strconv"
21 "strings"
22
23 "gno.land/p/nt/grc20/v0"
24 "gno.land/p/nt/ownable/v0"
25 "gno.land/p/nt/ufmt/v0"
26 "gno.land/r/nt/grc20reg/v0"
27)
28
29var (
30 // Token is the public handle on the token: other realms can read it, but
31 // cannot mutate balances (the PrivateLedger stays private).
32 Token *grc20.Token
33
34 // Ownable holds the administrative authority (mint / administrative burn).
35 Ownable *ownable.Ownable
36
37 privateLedger *grc20.PrivateLedger
38 userTeller grc20.Teller
39
40 // unit = 10^tokenDecimals, the conversion factor between whole units and
41 // base units.
42 unit int64
43
44 // hardCap = maxSupply * unit, expressed in base units.
45 hardCap int64
46
47 // totalMinted is the CUMULATIVE amount ever minted, and never goes down.
48 //
49 // The ceiling is checked against this figure rather than against circulating
50 // supply, because Burn lowers supply: checking that, burning would reopen
51 // minting headroom and "21 million" would become an instantaneous limit
52 // instead of a final one. With the cumulative counter burned tokens are gone
53 // for good, which is what a fixed supply is supposed to mean.
54 totalMinted int64
55
56 // tokenKey is the canonical key in the GRC20 registry: "<pkgpath>.<SYMBOL>".
57 // It is the identifier used by GnoSwap and friends.
58 tokenKey string
59
60 deployHeight int64
61 realmAddr address
62)
63
64func init(cur realm) {
65 unit = pow10(tokenDecimals)
66 hardCap = mulOrPanic(maxSupply, unit)
67 initial := mulOrPanic(initialSupply, unit)
68 if initial > hardCap {
69 panic("token: initialSupply exceeds maxSupply")
70 }
71
72 // Who administers the token:
73 // 1. ownerAddress from config.gno, when set (e.g. a multisig or a DAO);
74 // 2. otherwise the realm/user that ran the addpkg;
75 // 3. otherwise the EOA that signed the deploy transaction.
76 owner := address(ownerAddress)
77 if !owner.IsValid() {
78 owner = cur.Previous().Address()
79 }
80 if !owner.IsValid() {
81 owner = unsafe.OriginCaller()
82 }
83 if !owner.IsValid() {
84 panic("token: cannot determine the owner address")
85 }
86
87 Token, privateLedger = grc20.NewToken(tokenName, tokenSymbol, tokenDecimals, 0, cur)
88 userTeller = privateLedger.CallerTeller()
89 Ownable = ownable.NewWithAddress(owner)
90
91 if initial > 0 {
92 if err := privateLedger.Mint(owner, initial); err != nil {
93 panic(err)
94 }
95 totalMinted = initial
96 }
97
98 // Registration in the system GRC20 registry. Without this step the token
99 // exists but is invisible to GnoSwap, wallets and explorers.
100 tokenKey = grc20reg.Register(cross(cur), Token, "")
101
102 realmAddr = cur.Address()
103 deployHeight = runtime.ChainHeight()
104}
105
106// ---------------------------------------------------------------------------
107// Reads (no state cost, queryable with `gnokey query vm/qeval`)
108// ---------------------------------------------------------------------------
109
110// Name returns the token name.
111func Name() string { return Token.GetName() }
112
113// Symbol returns the ticker.
114func Symbol() string { return Token.GetSymbol() }
115
116// Decimals returns the precision.
117func Decimals() int { return Token.GetDecimals() }
118
119// TotalSupply returns the circulating supply in base units.
120func TotalSupply() int64 { return Token.TotalSupply() }
121
122// MaxSupply returns the hard ceiling in base units: it caps the cumulative
123// amount ever minted, not the circulating supply.
124func MaxSupply() int64 { return hardCap }
125
126// TotalMinted returns the cumulative amount ever minted. It never goes down,
127// not even after a Burn: hardCap - TotalMinted() is the remaining headroom.
128func TotalMinted() int64 { return totalMinted }
129
130// Burned returns the total destroyed: cumulative minted minus circulating.
131func Burned() int64 { return totalMinted - Token.TotalSupply() }
132
133// Holders returns the number of addresses with a non-zero balance.
134func Holders() int { return Token.KnownAccounts() }
135
136// BalanceOf returns the balance of owner in base units.
137func BalanceOf(owner address) int64 { return Token.BalanceOf(owner) }
138
139// Allowance returns how much spender may draw from owner.
140func Allowance(owner, spender address) int64 { return Token.Allowance(owner, spender) }
141
142// TokenKey returns the token's key in the GRC20 registry ("<pkgpath>.<SYMBOL>"),
143// to be used with r/nt/grc20reg and with GnoSwap.
144func TokenKey() string { return tokenKey }
145
146// RealmAddress returns the address of the realm itself: the address to send
147// funds meant for the contract (an airdrop budget, for instance).
148func RealmAddress() address { return realmAddr }
149
150// Owner returns the current administrator ("" once ownership has been dropped).
151func Owner() address { return Ownable.Owner() }
152
153// ---------------------------------------------------------------------------
154// Standard GRC20 writes
155// ---------------------------------------------------------------------------
156
157// Transfer sends amount (base units) from the caller to to.
158func Transfer(cur realm, to address, amount int64) {
159 checkErr(userTeller.Transfer(0, cur, to, amount))
160}
161
162// Approve authorises spender to draw up to amount from the caller.
163func Approve(cur realm, spender address, amount int64) {
164 checkErr(userTeller.Approve(0, cur, spender, amount))
165}
166
167// TransferFrom moves amount from from to to, consuming the caller's allowance.
168func TransferFrom(cur realm, from, to address, amount int64) {
169 checkErr(userTeller.TransferFrom(0, cur, from, to, amount))
170}
171
172// Burn destroys amount of the caller's tokens, reducing the supply.
173func Burn(cur realm, amount int64) {
174 checkErr(privateLedger.Burn(cur.Previous().Address(), amount))
175}
176
177// ---------------------------------------------------------------------------
178// Administration
179// ---------------------------------------------------------------------------
180
181// Mint creates amount (base units) in favour of to. Owner only, and never
182// beyond maxSupply. After DropOwnership this function is unusable forever: the
183// supply becomes immutable upwards.
184func Mint(cur realm, to address, amount int64) {
185 Ownable.AssertOwnedBy(cur.Previous().Address())
186 if amount <= 0 {
187 panic("token: amount must be positive")
188 }
189 if totalMinted > hardCap-amount {
190 panic("token: mint would exceed maxSupply")
191 }
192 totalMinted += amount
193 checkErr(privateLedger.Mint(to, amount))
194}
195
196// TransferOwnership hands administration over to newOwner.
197func TransferOwnership(cur realm, newOwner address) {
198 checkErr(Ownable.TransferOwnership(0, cur, newOwner))
199}
200
201// DropOwnership gives up administration for good: no future mint will ever be
202// possible. The operation cannot be undone.
203func DropOwnership(cur realm) {
204 checkErr(Ownable.DropOwnership(0, cur))
205}
206
207// ---------------------------------------------------------------------------
208// Render
209// ---------------------------------------------------------------------------
210
211func Render(path string) string {
212 parts := strings.Split(path, "/")
213
214 switch {
215 case path == "":
216 return renderHome()
217 case len(parts) == 2 && parts[0] == "balance":
218 addr := address(parts[1])
219 if !addr.IsValid() {
220 return "invalid address\n"
221 }
222 return ufmt.Sprintf("%s %s\n", format(Token.BalanceOf(addr)), tokenSymbol)
223 default:
224 return "404\n"
225 }
226}
227
228func renderHome() string {
229 s := ""
230 if tokenLogoURI != "" {
231 s += ufmt.Sprintf("\n\n", tokenSymbol, tokenLogoURI)
232 }
233 s += ufmt.Sprintf("# %s ($%s)\n\n", tokenName, tokenSymbol)
234 if tokenDescription != "" {
235 s += tokenDescription + "\n\n"
236 }
237 s += "| | |\n|---|---|\n"
238 s += ufmt.Sprintf("| Symbol | %s |\n", tokenSymbol)
239 s += ufmt.Sprintf("| Decimals | %d |\n", tokenDecimals)
240 s += ufmt.Sprintf("| Circulating supply | %s |\n", format(Token.TotalSupply()))
241 s += ufmt.Sprintf("| Max supply | %s |\n", format(hardCap))
242 s += ufmt.Sprintf("| Minted in total | %s |\n", format(totalMinted))
243 s += ufmt.Sprintf("| Burned | %s |\n", format(Burned()))
244 s += ufmt.Sprintf("| Holders | %d |\n", Token.KnownAccounts())
245 s += ufmt.Sprintf("| Registry key | `%s` |\n", tokenKey)
246 s += ufmt.Sprintf("| Mint | %s |\n", mintStatus())
247 s += ufmt.Sprintf("| Deployed at block | %d |\n", deployHeight)
248 s += "\n"
249 if tokenWebsite != "" {
250 s += ufmt.Sprintf("- Website: %s\n", tokenWebsite)
251 }
252 if tokenTwitter != "" {
253 s += ufmt.Sprintf("- X: %s\n", tokenTwitter)
254 }
255 s += "\nLook up a balance: `:balance/<address>`\n"
256 return s
257}
258
259func mintStatus() string {
260 if !Ownable.Owner().IsValid() {
261 return "**closed for good** (ownership dropped)"
262 }
263 if totalMinted >= hardCap {
264 return "**exhausted**: the mint ceiling is reached, no further token can be created"
265 }
266 return ufmt.Sprintf("%s left, controlled by %s",
267 format(hardCap-totalMinted), Ownable.Owner().String())
268}
269
270// ---------------------------------------------------------------------------
271// Helpers
272// ---------------------------------------------------------------------------
273
274// format turns base units into a readable decimal string.
275//
276// The sign is applied at the end rather than by negating v: for |v| < unit the
277// whole part is 0, so the minus would be lost (format(-500000) gave "0.5").
278// Negating v directly is no good either, because -math.MinInt64 overflows;
279// instead the whole part and the remainder are negated, both safe in magnitude.
280func format(v int64) string {
281 if unit == 1 {
282 return strconv.FormatInt(v, 10)
283 }
284 neg := v < 0
285 whole := v / unit
286 frac := v % unit
287 if whole < 0 {
288 whole = -whole
289 }
290 if frac < 0 {
291 frac = -frac
292 }
293
294 fs := strconv.FormatInt(frac, 10)
295 for len(fs) < tokenDecimals {
296 fs = "0" + fs
297 }
298 fs = strings.TrimRight(fs, "0")
299
300 out := strconv.FormatInt(whole, 10)
301 if fs != "" {
302 out += "." + fs
303 }
304 if neg {
305 out = "-" + out
306 }
307 return out
308}
309
310func pow10(n int) int64 {
311 r := int64(1)
312 for i := 0; i < n; i++ {
313 r *= 10
314 }
315 return r
316}
317
318func mulOrPanic(a, b int64) int64 {
319 if a < 0 || b <= 0 {
320 panic("token: invalid supply parameters")
321 }
322 if a > math.MaxInt64/b {
323 panic("token: supply * 10^decimals exceeds int64")
324 }
325 return a * b
326}
327
328func checkErr(err error) {
329 if err != nil {
330 panic(err.Error())
331 }
332}