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

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("![%s](%s)\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}