fishcoin.gno
7.99 Kb · 310 lines
1// Package shoaltoken is the Shoal FISH ledger. Init mints a fixed supply and
2// no later call can mint. Wallet Transfer, Approve, and TransferFrom move the
3// full amount. BuyTaxBps and SellTaxBps are stored for a later pool realm.
4// They do not tax a wallet transfer, a cheer, a season reward, or a community claim.
5//
6// The symbol is FISH, the same letters as the fish NFT collection. A wallet
7// that keys only on the symbol can confuse the two. The chain identity is
8// TokenID: the realm path, the symbol, and the sequence.
9//
10// Deployed on onyx-1 at gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/shoal/shoaltoken.
11// An earlier copy remains at gno.land/r/g10ragdr3cjqkael8rslpw4y6rlelhev4h0fswcx/shoaltoken.
12// Init is a separate call. It does not register this token.
13package fishcoin
14
15import (
16 "chain"
17 "chain/runtime/unsafe"
18 "strconv"
19
20 "gno.land/p/nt/grc20/v0"
21 "gno.land/p/nt/ownable/v0"
22)
23
24const (
25 unit int64 = 1_000_000
26 supplyWhole int64 = 100_000_000
27 seasonWhole int64 = 43_000_000
28 liquidityWhole int64 = 20_000_000
29 ecosystemWhole int64 = 15_000_000
30 communityWhole int64 = 12_000_000
31 teamWhole int64 = 10_000_000
32 maxBuyTaxBps int64 = 500
33 maxSellTaxBps int64 = 1000
34 defaultBuyTax int64 = 200
35 defaultSellTax int64 = 400
36)
37
38var (
39 token *grc20.Token
40 ledger *grc20.PrivateLedger
41 admin *ownable.Ownable
42 inited bool
43 sealed bool
44 spender address
45 spenderSet bool
46 buyTaxBps int64 = defaultBuyTax
47 sellTaxBps int64 = defaultSellTax
48)
49
50// Init mints the fixed supply once and seals the ledger. The five treasuries must be distinct.
51func Init(cur realm, season, liquidity, ecosystem, community, team address) {
52 if !cur.IsCurrent() {
53 panic("shoaltoken: realm")
54 }
55 if !cur.Previous().IsUserCall() {
56 panic("shoaltoken: eoa")
57 }
58 takeExact(0)
59 if inited {
60 panic("shoaltoken: init")
61 }
62 if !distinct(season, liquidity, ecosystem, community, team) {
63 panic("shoaltoken: addr")
64 }
65 tok, led := grc20.NewToken("Shoal FISH", "FISH", 6, 0, cur)
66 credit(led, season, seasonWhole*unit)
67 credit(led, liquidity, liquidityWhole*unit)
68 credit(led, ecosystem, ecosystemWhole*unit)
69 credit(led, community, communityWhole*unit)
70 credit(led, team, teamWhole*unit)
71 if tok.TotalSupply() != supplyWhole*unit {
72 panic("shoaltoken: supply")
73 }
74 token = tok
75 ledger = led
76 admin = ownable.NewWithAddress(cur.Previous().Address())
77 inited = true
78 sealed = true
79 chain.Emit(
80 "Init",
81 "token", tok.ID(),
82 "admin", cur.Previous().Address().String(),
83 "supply", strconv.FormatInt(supplyWhole*unit, 10),
84 )
85}
86
87// Transfer moves FISH from the caller to to. The amount is not taxed.
88func Transfer(cur realm, to address, amount int64) {
89 requireUser(cur)
90 takeExact(0)
91 requireReady()
92 must(ledger.CallerTeller().Transfer(0, cur, to, amount))
93 chain.Emit("Send", "to", to.String(), "amount", strconv.FormatInt(amount, 10))
94}
95
96// Approve lets spender move up to amount from the caller. This replaces the previous allowance.
97func Approve(cur realm, spender address, amount int64) {
98 requireUser(cur)
99 takeExact(0)
100 requireReady()
101 must(ledger.CallerTeller().Approve(0, cur, spender, amount))
102 chain.Emit("Approve", "spender", spender.String(), "amount", strconv.FormatInt(amount, 10))
103}
104
105// TransferFrom moves amount from owner to to, using the caller's allowance. The amount is not taxed.
106func TransferFrom(cur realm, owner, to address, amount int64) {
107 requireUser(cur)
108 takeExact(0)
109 requireReady()
110 must(ledger.CallerTeller().TransferFrom(0, cur, owner, to, amount))
111 chain.Emit("SendFrom", "owner", owner.String(), "to", to.String(), "amount", strconv.FormatInt(amount, 10))
112}
113
114// SetBuyTaxBps stores the buy tax for a later pool. The cap is 500 bps. This does not tax wallets.
115func SetBuyTaxBps(cur realm, next int64) {
116 caller := requireUser(cur)
117 takeExact(0)
118 requireReady()
119 if !admin.OwnedBy(caller) {
120 panic("shoaltoken: admin")
121 }
122 if next < 0 || next > maxBuyTaxBps {
123 panic("shoaltoken: tax")
124 }
125 buyTaxBps = next
126 chain.Emit("SetBuyTaxBps", "bps", strconv.FormatInt(next, 10))
127}
128
129// SetSellTaxBps stores the sell tax for a later pool. The cap is 1000 bps. This does not tax wallets.
130func SetSellTaxBps(cur realm, next int64) {
131 caller := requireUser(cur)
132 takeExact(0)
133 requireReady()
134 if !admin.OwnedBy(caller) {
135 panic("shoaltoken: admin")
136 }
137 if next < 0 || next > maxSellTaxBps {
138 panic("shoaltoken: tax")
139 }
140 sellTaxBps = next
141 chain.Emit("SetSellTaxBps", "bps", strconv.FormatInt(next, 10))
142}
143
144// TransferAdmin hands the tax hook to next. Balances stay where they are.
145func TransferAdmin(cur realm, next address) {
146 requireUser(cur)
147 takeExact(0)
148 requireReady()
149 if !next.IsValid() {
150 panic("shoaltoken: admin")
151 }
152 if err := admin.TransferOwnership(0, cur, next); err != nil {
153 panic("shoaltoken: admin")
154 }
155 chain.Emit("TransferAdmin", "next", next.String())
156}
157
158func Name() string {
159 requireReady()
160 return token.GetName()
161}
162
163func Symbol() string {
164 requireReady()
165 return token.GetSymbol()
166}
167
168func Decimals() int {
169 requireReady()
170 return token.GetDecimals()
171}
172
173func TotalSupply() int64 {
174 requireReady()
175 return token.TotalSupply()
176}
177
178func BalanceOf(who address) int64 {
179 requireReady()
180 return token.BalanceOf(who)
181}
182
183func Allowance(owner, spender address) int64 {
184 requireReady()
185 return token.Allowance(owner, spender)
186}
187
188func TokenID() string {
189 requireReady()
190 return token.ID()
191}
192
193func BuyTaxBps() int64 { return buyTaxBps }
194func SellTaxBps() int64 { return sellTaxBps }
195func Version() string { return "shoaltoken-1" }
196func Sealed() bool { return sealed }
197
198// Spender is the one realm allowed to Draw. It is empty until SetSpender.
199func Spender() address { return spender }
200
201// SetSpender records the fish realm once. A later call cannot replace it.
202func SetSpender(cur realm, who address) {
203 caller := requireUser(cur)
204 takeExact(0)
205 requireReady()
206 if !admin.OwnedBy(caller) {
207 panic("shoaltoken: admin")
208 }
209 if spenderSet || !who.IsValid() {
210 panic("shoaltoken: spender")
211 }
212 spender = who
213 spenderSet = true
214 chain.Emit("SetSpender", "who", who.String())
215}
216
217// Draw moves min(amount, balance, allowance) from owner to to.
218// Only the recorded spender may call it. The owner must have approved that spender.
219// A short balance or allowance moves nothing and does not panic. This does not mint.
220func Draw(cur realm, owner, to address, amount int64) int64 {
221 if !cur.IsCurrent() {
222 panic("shoaltoken: realm")
223 }
224 if !spenderSet || cur.Previous().Address() != spender {
225 panic("shoaltoken: spender")
226 }
227 takeExact(0)
228 requireReady()
229 if amount <= 0 || !owner.IsValid() || !to.IsValid() {
230 return 0
231 }
232 bal := token.BalanceOf(owner)
233 if amount > bal {
234 amount = bal
235 }
236 allow := token.Allowance(owner, spender)
237 if amount > allow {
238 amount = allow
239 }
240 if amount <= 0 {
241 return 0
242 }
243 must(ledger.CallerTeller().TransferFrom(0, cur, owner, to, amount))
244 chain.Emit("Draw", "owner", owner.String(), "to", to.String(), "amount", strconv.FormatInt(amount, 10))
245 return amount
246}
247
248func Admin() address {
249 if admin == nil {
250 return address("")
251 }
252 return admin.Owner()
253}
254
255func Render(path string) string {
256 if path != "" {
257 return "# Shoal FISH\n\nUnknown path.\n"
258 }
259 return "# Shoal FISH\n\nFixed supply. Wallet transfers are not taxed.\n\nThe symbol FISH shares its letters with the fish NFT collection. TokenID is the chain identity.\n"
260}
261
262func credit(led *grc20.PrivateLedger, who address, amount int64) {
263 if err := led.Mint(who, amount); err != nil {
264 panic(err)
265 }
266}
267
268func must(err error) {
269 if err != nil {
270 panic(err)
271 }
272}
273
274func requireReady() {
275 if !inited || !sealed || token == nil || ledger == nil || admin == nil {
276 panic("shoaltoken: init")
277 }
278}
279
280func requireUser(cur realm) address {
281 if !cur.Previous().IsUserCall() {
282 panic("shoaltoken: eoa")
283 }
284 return cur.Previous().Address()
285}
286
287func takeExact(n int64) {
288 sent := unsafe.OriginSend()
289 if n == 0 {
290 if len(sent) != 0 {
291 panic("shoaltoken: payment")
292 }
293 return
294 }
295 panic("shoaltoken: payment")
296}
297
298func distinct(xs ...address) bool {
299 for i, a := range xs {
300 if !a.IsValid() {
301 return false
302 }
303 for j := 0; j < i; j++ {
304 if xs[j] == a {
305 return false
306 }
307 }
308 }
309 return true
310}