Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}