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