shoaltoken.gno
6.47 Kb · 257 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/g10ragdr3cjqkael8rslpw4y6rlelhev4h0fswcx/shoaltoken.
11// Init is a separate call. It does not register this token.
12package shoaltoken
13
14import (
15 "chain"
16 "chain/runtime/unsafe"
17 "strconv"
18
19 "gno.land/p/nt/grc20/v0"
20 "gno.land/p/nt/ownable/v0"
21)
22
23const (
24 unit int64 = 1_000_000
25 supplyWhole int64 = 100_000_000
26 seasonWhole int64 = 43_000_000
27 liquidityWhole int64 = 20_000_000
28 ecosystemWhole int64 = 15_000_000
29 communityWhole int64 = 12_000_000
30 teamWhole int64 = 10_000_000
31 maxBuyTaxBps int64 = 500
32 maxSellTaxBps int64 = 1000
33 defaultBuyTax int64 = 200
34 defaultSellTax int64 = 400
35)
36
37var (
38 token *grc20.Token
39 ledger *grc20.PrivateLedger
40 admin *ownable.Ownable
41 inited bool
42 sealed bool
43 buyTaxBps int64 = defaultBuyTax
44 sellTaxBps int64 = defaultSellTax
45)
46
47// Init mints the fixed supply once and seals the ledger. The five treasuries must be distinct.
48func Init(cur realm, season, liquidity, ecosystem, community, team address) {
49 if !cur.IsCurrent() {
50 panic("shoaltoken: realm")
51 }
52 if !cur.Previous().IsUserCall() {
53 panic("shoaltoken: eoa")
54 }
55 takeExact(0)
56 if inited {
57 panic("shoaltoken: init")
58 }
59 if !distinct(season, liquidity, ecosystem, community, team) {
60 panic("shoaltoken: addr")
61 }
62 tok, led := grc20.NewToken("Shoal FISH", "FISH", 6, 0, cur)
63 credit(led, season, seasonWhole*unit)
64 credit(led, liquidity, liquidityWhole*unit)
65 credit(led, ecosystem, ecosystemWhole*unit)
66 credit(led, community, communityWhole*unit)
67 credit(led, team, teamWhole*unit)
68 if tok.TotalSupply() != supplyWhole*unit {
69 panic("shoaltoken: supply")
70 }
71 token = tok
72 ledger = led
73 admin = ownable.NewWithAddress(cur.Previous().Address())
74 inited = true
75 sealed = true
76 chain.Emit(
77 "Init",
78 "token", tok.ID(),
79 "admin", cur.Previous().Address().String(),
80 "supply", strconv.FormatInt(supplyWhole*unit, 10),
81 )
82}
83
84// Transfer moves FISH from the caller to to. The amount is not taxed.
85func Transfer(cur realm, to address, amount int64) {
86 requireUser(cur)
87 takeExact(0)
88 requireReady()
89 must(ledger.CallerTeller().Transfer(0, cur, to, amount))
90 chain.Emit("Send", "to", to.String(), "amount", strconv.FormatInt(amount, 10))
91}
92
93// Approve lets spender move up to amount from the caller. This replaces the previous allowance.
94func Approve(cur realm, spender address, amount int64) {
95 requireUser(cur)
96 takeExact(0)
97 requireReady()
98 must(ledger.CallerTeller().Approve(0, cur, spender, amount))
99 chain.Emit("Approve", "spender", spender.String(), "amount", strconv.FormatInt(amount, 10))
100}
101
102// TransferFrom moves amount from owner to to, using the caller's allowance. The amount is not taxed.
103func TransferFrom(cur realm, owner, to address, amount int64) {
104 requireUser(cur)
105 takeExact(0)
106 requireReady()
107 must(ledger.CallerTeller().TransferFrom(0, cur, owner, to, amount))
108 chain.Emit("SendFrom", "owner", owner.String(), "to", to.String(), "amount", strconv.FormatInt(amount, 10))
109}
110
111// SetBuyTaxBps stores the buy tax for a later pool. The cap is 500 bps. This does not tax wallets.
112func SetBuyTaxBps(cur realm, next int64) {
113 caller := requireUser(cur)
114 takeExact(0)
115 requireReady()
116 if !admin.OwnedBy(caller) {
117 panic("shoaltoken: admin")
118 }
119 if next < 0 || next > maxBuyTaxBps {
120 panic("shoaltoken: tax")
121 }
122 buyTaxBps = next
123 chain.Emit("SetBuyTaxBps", "bps", strconv.FormatInt(next, 10))
124}
125
126// SetSellTaxBps stores the sell tax for a later pool. The cap is 1000 bps. This does not tax wallets.
127func SetSellTaxBps(cur realm, next int64) {
128 caller := requireUser(cur)
129 takeExact(0)
130 requireReady()
131 if !admin.OwnedBy(caller) {
132 panic("shoaltoken: admin")
133 }
134 if next < 0 || next > maxSellTaxBps {
135 panic("shoaltoken: tax")
136 }
137 sellTaxBps = next
138 chain.Emit("SetSellTaxBps", "bps", strconv.FormatInt(next, 10))
139}
140
141// TransferAdmin hands the tax hook to next. Balances stay where they are.
142func TransferAdmin(cur realm, next address) {
143 requireUser(cur)
144 takeExact(0)
145 requireReady()
146 if !next.IsValid() {
147 panic("shoaltoken: admin")
148 }
149 if err := admin.TransferOwnership(0, cur, next); err != nil {
150 panic("shoaltoken: admin")
151 }
152 chain.Emit("TransferAdmin", "next", next.String())
153}
154
155func Name() string {
156 requireReady()
157 return token.GetName()
158}
159
160func Symbol() string {
161 requireReady()
162 return token.GetSymbol()
163}
164
165func Decimals() int {
166 requireReady()
167 return token.GetDecimals()
168}
169
170func TotalSupply() int64 {
171 requireReady()
172 return token.TotalSupply()
173}
174
175func BalanceOf(who address) int64 {
176 requireReady()
177 return token.BalanceOf(who)
178}
179
180func Allowance(owner, spender address) int64 {
181 requireReady()
182 return token.Allowance(owner, spender)
183}
184
185func TokenID() string {
186 requireReady()
187 return token.ID()
188}
189
190func BuyTaxBps() int64 { return buyTaxBps }
191func SellTaxBps() int64 { return sellTaxBps }
192func Version() string { return "shoaltoken-1" }
193func Sealed() bool { return sealed }
194
195func Admin() address {
196 if admin == nil {
197 return address("")
198 }
199 return admin.Owner()
200}
201
202func Render(path string) string {
203 if path != "" {
204 return "# Shoal FISH\n\nUnknown path.\n"
205 }
206 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"
207}
208
209func credit(led *grc20.PrivateLedger, who address, amount int64) {
210 if err := led.Mint(who, amount); err != nil {
211 panic(err)
212 }
213}
214
215func must(err error) {
216 if err != nil {
217 panic(err)
218 }
219}
220
221func requireReady() {
222 if !inited || !sealed || token == nil || ledger == nil || admin == nil {
223 panic("shoaltoken: init")
224 }
225}
226
227func requireUser(cur realm) address {
228 if !cur.Previous().IsUserCall() {
229 panic("shoaltoken: eoa")
230 }
231 return cur.Previous().Address()
232}
233
234func takeExact(n int64) {
235 sent := unsafe.OriginSend()
236 if n == 0 {
237 if len(sent) != 0 {
238 panic("shoaltoken: payment")
239 }
240 return
241 }
242 panic("shoaltoken: payment")
243}
244
245func distinct(xs ...address) bool {
246 for i, a := range xs {
247 if !a.IsValid() {
248 return false
249 }
250 for j := 0; j < i; j++ {
251 if xs[j] == a {
252 return false
253 }
254 }
255 }
256 return true
257}