1// Package fishtoken is the unreleased GnoFish FISH ledger.2// Init mints a fixed supply once. No later call can mint.3// While transfers are closed, a normal wallet cannot send FISH, so it cannot4// fund a pool or sell through a market. The deploy wallet opens transfers.5// The five treasury wallets, the deploy wallet, and a reward realm the deploy6// wallet has granted may still send. This realm has no pool and no trade.7// Public-sale FISH is minted to the deploy wallet. fishtoken-1.8package fishtoken910import (11 "chain"12 "chain/runtime/unsafe"13 "strconv"1415 "gno.land/p/nt/grc20/v0"16 "gno.land/p/nt/seqid/v0"17)1819const (20 unit int64 = 1_000_00021 supplyWhole int64 = 100_000_00022 seasonWhole int64 = 43_000_00023 liquidityWhole int64 = 20_000_00024 ecosystemWhole int64 = 15_000_00025 communityWhole int64 = 12_000_00026 teamWhole int64 = 5_000_00027 saleWhole int64 = 5_000_00028)2930var (31 seasonAddr = address("g1m6z3l9q577dltj73ym8fu0068peupqg468r5xe")32 liquidityAddr = address("g1lepzz7lehwfnwng0ddxaglz2kzmsf6pyy5f6tw")33 ecosystemAddr = address("g1p4cylgemajmw93e9a0nr0fmph60gee2yhkn2kz")34 communityAddr = address("g1dpklfykp2mrhcwccek54scv43kamtxlszya2z8")35 teamAddr = address("g15ejerz2n0xn5x94zezhquvwvp98m3jgkcwdrya")36)3738var (39 token *grc20.Token40 ledger *grc20.PrivateLedger41 seq seqid.ID42 inited bool43 open bool44 admin address45 mover map[address]bool46 payer map[address]bool47)4849// Init mints the fixed supply once. The caller is the deploy wallet and receives the public-sale FISH. No coins.50func Init(cur realm) {51 caller := requireUser(cur)52 takeExact(0)53 if inited {54 panic("fishtoken: init")55 }56 if !distinct(caller, seasonAddr, liquidityAddr, ecosystemAddr, communityAddr, teamAddr) {57 panic("fishtoken: addr")58 }59 tok, led := grc20.NewToken("FISH", "FISH", 6, seq.Next(), cur)60 credit(led, seasonAddr, seasonWhole*unit)61 credit(led, liquidityAddr, liquidityWhole*unit)62 credit(led, ecosystemAddr, ecosystemWhole*unit)63 credit(led, communityAddr, communityWhole*unit)64 credit(led, teamAddr, teamWhole*unit)65 credit(led, caller, saleWhole*unit)66 if tok.TotalSupply() != supplyWhole*unit {67 panic("fishtoken: supply")68 }69 token = tok70 ledger = led71 admin = caller72 open = false73 mover = map[address]bool{74 caller: true,75 seasonAddr: true,76 liquidityAddr: true,77 ecosystemAddr: true,78 communityAddr: true,79 teamAddr: true,80 }81 payer = map[address]bool{}82 inited = true83 chain.Emit("Init", "admin", caller.String(), "supply", strconv.FormatInt(supplyWhole*unit, 10))84}8586// SetTransfers opens or closes ordinary wallet sends. The deploy wallet only. No coins.87func SetTransfers(cur realm, next bool) {88 caller := requireUser(cur)89 takeExact(0)90 requireReady()91 requireAdmin(caller)92 open = next93 chain.Emit("SetTransfers", "open", strconv.FormatBool(next))94}9596// GrantMover lets that wallet send FISH while transfers are closed. The deploy wallet only. No coins.97func GrantMover(cur realm, who address) {98 caller := requireUser(cur)99 takeExact(0)100 requireReady()101 requireAdmin(caller)102 if !who.IsValid() {103 panic("fishtoken: addr")104 }105 mover[who] = true106 chain.Emit("GrantMover", "who", who.String())107}108109// RevokeMover removes that wallet from the closed-transfer list. The deploy wallet only. No coins.110func RevokeMover(cur realm, who address) {111 caller := requireUser(cur)112 takeExact(0)113 requireReady()114 requireAdmin(caller)115 if !who.IsValid() {116 panic("fishtoken: addr")117 }118 delete(mover, who)119 chain.Emit("RevokeMover", "who", who.String())120}121122// GrantDistributor lets that realm pay FISH to users while transfers are closed. The deploy wallet only. No coins.123func GrantDistributor(cur realm, who address) {124 caller := requireUser(cur)125 takeExact(0)126 requireReady()127 requireAdmin(caller)128 if !who.IsValid() {129 panic("fishtoken: addr")130 }131 payer[who] = true132 chain.Emit("GrantDistributor", "who", who.String())133}134135// RevokeDistributor removes that reward realm. The deploy wallet only. No coins.136func RevokeDistributor(cur realm, who address) {137 caller := requireUser(cur)138 takeExact(0)139 requireReady()140 requireAdmin(caller)141 if !who.IsValid() {142 panic("fishtoken: addr")143 }144 delete(payer, who)145 chain.Emit("RevokeDistributor", "who", who.String())146}147148// TransferAdmin hands the switch to next. Balances stay where they are. No coins.149func TransferAdmin(cur realm, next address) {150 caller := requireUser(cur)151 takeExact(0)152 requireReady()153 requireAdmin(caller)154 if !next.IsValid() {155 panic("fishtoken: admin")156 }157 admin = next158 mover[next] = true159 chain.Emit("TransferAdmin", "next", next.String())160}161162// Transfer sends FISH from the caller to to.163// A closed ledger rejects an ordinary wallet. Listed wallets and granted reward realms may still send.164func Transfer(cur realm, to address, amount int64) {165 caller := cur.Previous().Address()166 if cur.Previous().IsUserCall() {167 takeExact(0)168 }169 requireReady()170 requireAmount(amount, to)171 if !mayMove(cur, caller, caller) {172 panic("fishtoken: closed")173 }174 must(ledger.CallerTeller().Transfer(0, cur, to, amount))175 chain.Emit("Send", "to", to.String(), "amount", strconv.FormatInt(amount, 10))176}177178// Approve sets the spender allowance. It does not move FISH and does not open trading. No coins from an EOA.179func Approve(cur realm, spender address, amount int64) {180 if cur.Previous().IsUserCall() {181 takeExact(0)182 } else if !payer[cur.Previous().Address()] {183 panic("fishtoken: closed")184 }185 requireReady()186 if amount < 0 || !spender.IsValid() {187 panic("fishtoken: amount")188 }189 must(ledger.CallerTeller().Approve(0, cur, spender, amount))190 chain.Emit("Approve", "spender", spender.String(), "amount", strconv.FormatInt(amount, 10))191}192193// TransferFrom sends FISH using the caller's allowance.194// While closed, only a granted reward realm may pull, and only from its own balance or from a listed wallet.195func TransferFrom(cur realm, owner, to address, amount int64) {196 caller := cur.Previous().Address()197 if cur.Previous().IsUserCall() {198 takeExact(0)199 }200 requireReady()201 requireAmount(amount, to)202 if !mayMove(cur, caller, owner) {203 panic("fishtoken: closed")204 }205 must(ledger.CallerTeller().TransferFrom(0, cur, owner, to, amount))206 chain.Emit("SendFrom", "owner", owner.String(), "to", to.String(), "amount", strconv.FormatInt(amount, 10))207}208209func TransfersOpen() bool { return open }210func IsMover(who address) bool {211 return mover[who]212}213func IsDistributor(who address) bool { return payer[who] }214func Version() string { return "fishtoken-1" }215func Admin() address { return admin }216217func Name() string {218 requireReady()219 return token.GetName()220}221222func Symbol() string {223 requireReady()224 return token.GetSymbol()225}226227func Decimals() int {228 requireReady()229 return token.GetDecimals()230}231232func TotalSupply() int64 {233 requireReady()234 return token.TotalSupply()235}236237func BalanceOf(who address) int64 {238 requireReady()239 return token.BalanceOf(who)240}241242func Allowance(owner, spender address) int64 {243 requireReady()244 return token.Allowance(owner, spender)245}246247func TokenID() string {248 requireReady()249 return token.ID()250}251252func Render(path string) string {253 if path != "" {254 return "# FISH\n\nUnknown path.\n"255 }256 if !inited {257 return "# FISH\n\nNot opened.\n"258 }259 state := "closed"260 if open {261 state = "open"262 }263 return "# FISH\n\nFixed supply. Transfers are " + state + ". A closed ledger does not fund a pool and does not trade. Listed wallets and granted reward realms can still send. Public-sale FISH was minted to the deploy wallet.\n"264}265266func mayMove(cur realm, caller, from address) bool {267 if open {268 return true269 }270 if cur.Previous().IsUserCall() {271 return from == caller && mover[caller]272 }273 if !payer[caller] {274 return false275 }276 return from == caller || mover[from]277}278279func requireAdmin(caller address) {280 if caller != admin {281 panic("fishtoken: admin")282 }283}284285func requireAmount(amount int64, to address) {286 if amount <= 0 || !to.IsValid() {287 panic("fishtoken: amount")288 }289}290291func credit(led *grc20.PrivateLedger, who address, amount int64) {292 if err := led.Mint(who, amount); err != nil {293 panic(err)294 }295}296297func must(err error) {298 if err != nil {299 panic(err)300 }301}302303func requireReady() {304 if !inited || token == nil || ledger == nil {305 panic("fishtoken: init")306 }307}308309func requireUser(cur realm) address {310 if !cur.Previous().IsUserCall() {311 panic("fishtoken: eoa")312 }313 return cur.Previous().Address()314}315316func takeExact(n int64) {317 sent := unsafe.OriginSend()318 if n == 0 {319 if len(sent) != 0 {320 panic("fishtoken: payment")321 }322 return323 }324 panic("fishtoken: payment")325}326327func distinct(xs ...address) bool {328 for i, a := range xs {329 if !a.IsValid() {330 return false331 }332 for j := 0; j < i; j++ {333 if xs[j] == a {334 return false335 }336 }337 }338 return true339}340Admin() string
Allowance(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(who string) int64
Decimals() int
GrantDistributor(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}, who string)
GrantMover(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}, who string)
Init(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})
IsDistributor(who string) bool
IsMover(who string) bool
Name() string
Render(path string) string
RevokeDistributor(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}, who string)
RevokeMover(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}, who string)
SetTransfers(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}, next bool)
Symbol() string
TokenID() string
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)
TransferAdmin(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}, next string)
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}, owner string, to string, amount int64)
TransfersOpen() bool
Version() 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.