1// Parametric GRC20 realm for gno.land.2//3// All configuration (name, symbol, decimals, supply, metadata) lives in4// 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/v08// - registers it with gno.land/r/nt/grc20reg/v0 (required to be discoverable9// by GnoSwap, wallets and explorers)10// - mints the initial supply to the deploying address11// - exposes the GRC20 methods as top-level functions callable via MsgCall12// - enforces a hard ceiling (maxSupply) on minting13// - lets the owner give up minting forever (DropOwnership)14package gnomic1516import (17 "chain/runtime"18 "chain/runtime/unsafe"19 "math"20 "strconv"21 "strings"2223 "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)2829var (30 // Token is the public handle on the token: other realms can read it, but31 // cannot mutate balances (the PrivateLedger stays private).32 Token *grc20.Token3334 // Ownable holds the administrative authority (mint / administrative burn).35 Ownable *ownable.Ownable3637 privateLedger *grc20.PrivateLedger38 userTeller grc20.Teller3940 // unit = 10^tokenDecimals, the conversion factor between whole units and41 // base units.42 unit int644344 // hardCap = maxSupply * unit, expressed in base units.45 hardCap int644647 // totalMinted is the CUMULATIVE amount ever minted, and never goes down.48 //49 // The ceiling is checked against this figure rather than against circulating50 // supply, because Burn lowers supply: checking that, burning would reopen51 // minting headroom and "21 million" would become an instantaneous limit52 // instead of a final one. With the cumulative counter burned tokens are gone53 // for good, which is what a fixed supply is supposed to mean.54 totalMinted int645556 // tokenKey is the canonical key in the GRC20 registry: "<pkgpath>.<SYMBOL>".57 // It is the identifier used by GnoSwap and friends.58 tokenKey string5960 deployHeight int6461 realmAddr address62)6364func 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 }7172 // 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 }8687 Token, privateLedger = grc20.NewToken(tokenName, tokenSymbol, tokenDecimals, 0, cur)88 userTeller = privateLedger.CallerTeller()89 Ownable = ownable.NewWithAddress(owner)9091 if initial > 0 {92 if err := privateLedger.Mint(owner, initial); err != nil {93 panic(err)94 }95 totalMinted = initial96 }9798 // Registration in the system GRC20 registry. Without this step the token99 // exists but is invisible to GnoSwap, wallets and explorers.100 tokenKey = grc20reg.Register(cross(cur), Token, "")101102 realmAddr = cur.Address()103 deployHeight = runtime.ChainHeight()104}105106// ---------------------------------------------------------------------------107// Reads (no state cost, queryable with `gnokey query vm/qeval`)108// ---------------------------------------------------------------------------109110// Name returns the token name.111func Name() string { return Token.GetName() }112113// Symbol returns the ticker.114func Symbol() string { return Token.GetSymbol() }115116// Decimals returns the precision.117func Decimals() int { return Token.GetDecimals() }118119// TotalSupply returns the circulating supply in base units.120func TotalSupply() int64 { return Token.TotalSupply() }121122// MaxSupply returns the hard ceiling in base units: it caps the cumulative123// amount ever minted, not the circulating supply.124func MaxSupply() int64 { return hardCap }125126// 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 }129130// Burned returns the total destroyed: cumulative minted minus circulating.131func Burned() int64 { return totalMinted - Token.TotalSupply() }132133// Holders returns the number of addresses with a non-zero balance.134func Holders() int { return Token.KnownAccounts() }135136// BalanceOf returns the balance of owner in base units.137func BalanceOf(owner address) int64 { return Token.BalanceOf(owner) }138139// Allowance returns how much spender may draw from owner.140func Allowance(owner, spender address) int64 { return Token.Allowance(owner, spender) }141142// 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 }145146// RealmAddress returns the address of the realm itself: the address to send147// funds meant for the contract (an airdrop budget, for instance).148func RealmAddress() address { return realmAddr }149150// Owner returns the current administrator ("" once ownership has been dropped).151func Owner() address { return Ownable.Owner() }152153// ---------------------------------------------------------------------------154// Standard GRC20 writes155// ---------------------------------------------------------------------------156157// 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}161162// 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}166167// 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}171172// 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}176177// ---------------------------------------------------------------------------178// Administration179// ---------------------------------------------------------------------------180181// Mint creates amount (base units) in favour of to. Owner only, and never182// beyond maxSupply. After DropOwnership this function is unusable forever: the183// 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 += amount193 checkErr(privateLedger.Mint(to, amount))194}195196// TransferOwnership hands administration over to newOwner.197func TransferOwnership(cur realm, newOwner address) {198 checkErr(Ownable.TransferOwnership(0, cur, newOwner))199}200201// DropOwnership gives up administration for good: no future mint will ever be202// possible. The operation cannot be undone.203func DropOwnership(cur realm) {204 checkErr(Ownable.DropOwnership(0, cur))205}206207// ---------------------------------------------------------------------------208// Render209// ---------------------------------------------------------------------------210211func Render(path string) string {212 parts := strings.Split(path, "/")213214 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}227228func 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 s257}258259func 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}269270// ---------------------------------------------------------------------------271// Helpers272// ---------------------------------------------------------------------------273274// 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 the277// 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 < 0285 whole := v / unit286 frac := v % unit287 if whole < 0 {288 whole = -whole289 }290 if frac < 0 {291 frac = -frac292 }293294 fs := strconv.FormatInt(frac, 10)295 for len(fs) < tokenDecimals {296 fs = "0" + fs297 }298 fs = strings.TrimRight(fs, "0")299300 out := strconv.FormatInt(whole, 10)301 if fs != "" {302 out += "." + fs303 }304 if neg {305 out = "-" + out306 }307 return out308}309310func pow10(n int) int64 {311 r := int64(1)312 for i := 0; i < n; i++ {313 r *= 10314 }315 return r316}317318func 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 * b326}327328func checkErr(err error) {329 if err != nil {330 panic(err.Error())331 }332}333Allowance(owner string, spender string) int64
Approve(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, spender string, amount int64)
BalanceOf(owner string) int64
Burn(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, amount int64)
Burned() int64
Decimals() int
DropOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Holders() int
MaxSupply() int64
Mint(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, amount int64)
Name() string
Owner() string
RealmAddress() string
Render(path string) string
Symbol() string
TokenKey() string
TotalMinted() int64
TotalSupply() int64
Transfer(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, amount int64)
TransferFrom(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, from string, to string, amount int64)
TransferOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newOwner string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.