Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}