fishtoken.gno
8.57 Kb · 339 lines
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 cannot
4// 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 deploy
6// 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 fishtoken
9
10import (
11 "chain"
12 "chain/runtime/unsafe"
13 "strconv"
14
15 "gno.land/p/nt/grc20/v0"
16 "gno.land/p/nt/seqid/v0"
17)
18
19const (
20 unit int64 = 1_000_000
21 supplyWhole int64 = 100_000_000
22 seasonWhole int64 = 43_000_000
23 liquidityWhole int64 = 20_000_000
24 ecosystemWhole int64 = 15_000_000
25 communityWhole int64 = 12_000_000
26 teamWhole int64 = 5_000_000
27 saleWhole int64 = 5_000_000
28)
29
30var (
31 seasonAddr = address("g1m6z3l9q577dltj73ym8fu0068peupqg468r5xe")
32 liquidityAddr = address("g1lepzz7lehwfnwng0ddxaglz2kzmsf6pyy5f6tw")
33 ecosystemAddr = address("g1p4cylgemajmw93e9a0nr0fmph60gee2yhkn2kz")
34 communityAddr = address("g1dpklfykp2mrhcwccek54scv43kamtxlszya2z8")
35 teamAddr = address("g15ejerz2n0xn5x94zezhquvwvp98m3jgkcwdrya")
36)
37
38var (
39 token *grc20.Token
40 ledger *grc20.PrivateLedger
41 seq seqid.ID
42 inited bool
43 open bool
44 admin address
45 mover map[address]bool
46 payer map[address]bool
47)
48
49// 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 = tok
70 ledger = led
71 admin = caller
72 open = false
73 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 = true
83 chain.Emit("Init", "admin", caller.String(), "supply", strconv.FormatInt(supplyWhole*unit, 10))
84}
85
86// 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 = next
93 chain.Emit("SetTransfers", "open", strconv.FormatBool(next))
94}
95
96// 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] = true
106 chain.Emit("GrantMover", "who", who.String())
107}
108
109// 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}
121
122// 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] = true
132 chain.Emit("GrantDistributor", "who", who.String())
133}
134
135// 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}
147
148// 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 = next
158 mover[next] = true
159 chain.Emit("TransferAdmin", "next", next.String())
160}
161
162// 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}
177
178// 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}
192
193// 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}
208
209func 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 }
216
217func Name() string {
218 requireReady()
219 return token.GetName()
220}
221
222func Symbol() string {
223 requireReady()
224 return token.GetSymbol()
225}
226
227func Decimals() int {
228 requireReady()
229 return token.GetDecimals()
230}
231
232func TotalSupply() int64 {
233 requireReady()
234 return token.TotalSupply()
235}
236
237func BalanceOf(who address) int64 {
238 requireReady()
239 return token.BalanceOf(who)
240}
241
242func Allowance(owner, spender address) int64 {
243 requireReady()
244 return token.Allowance(owner, spender)
245}
246
247func TokenID() string {
248 requireReady()
249 return token.ID()
250}
251
252func 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}
265
266func mayMove(cur realm, caller, from address) bool {
267 if open {
268 return true
269 }
270 if cur.Previous().IsUserCall() {
271 return from == caller && mover[caller]
272 }
273 if !payer[caller] {
274 return false
275 }
276 return from == caller || mover[from]
277}
278
279func requireAdmin(caller address) {
280 if caller != admin {
281 panic("fishtoken: admin")
282 }
283}
284
285func requireAmount(amount int64, to address) {
286 if amount <= 0 || !to.IsValid() {
287 panic("fishtoken: amount")
288 }
289}
290
291func credit(led *grc20.PrivateLedger, who address, amount int64) {
292 if err := led.Mint(who, amount); err != nil {
293 panic(err)
294 }
295}
296
297func must(err error) {
298 if err != nil {
299 panic(err)
300 }
301}
302
303func requireReady() {
304 if !inited || token == nil || ledger == nil {
305 panic("fishtoken: init")
306 }
307}
308
309func requireUser(cur realm) address {
310 if !cur.Previous().IsUserCall() {
311 panic("fishtoken: eoa")
312 }
313 return cur.Previous().Address()
314}
315
316func takeExact(n int64) {
317 sent := unsafe.OriginSend()
318 if n == 0 {
319 if len(sent) != 0 {
320 panic("fishtoken: payment")
321 }
322 return
323 }
324 panic("fishtoken: payment")
325}
326
327func distinct(xs ...address) bool {
328 for i, a := range xs {
329 if !a.IsValid() {
330 return false
331 }
332 for j := 0; j < i; j++ {
333 if xs[j] == a {
334 return false
335 }
336 }
337 }
338 return true
339}