fishseason.gno
6.29 Kb · 275 lines
1// Package fishseason is the Season 1 paying board.
2// The open is 2026-10-16 00:00:00 UTC. The deploy wallet sets the end, posts
3// up to 20 winners, and seals the board. Pay then pulls FISH from the season
4// wallet to those addresses. This realm does not watch the sea and does not
5// treat a fishlive Settle as a win. fishlive is not called.
6// The stage script rewrites the fishtoken import to the deploy wallet's package.
7// A kill still pays no FISH by itself. fishseason-1.
8package fishseason
9
10import (
11 "chain"
12 "chain/runtime/unsafe"
13 "strconv"
14 "time"
15
16 fish "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/fishtoken"
17)
18
19const (
20 unit int64 = 1_000_000
21 places = 20
22 startUnix int64 = 1_792_108_800
23)
24
25var seasonAddr = address("g1m6z3l9q577dltj73ym8fu0068peupqg468r5xe")
26
27// Whole FISH for places 1 through 20. The sum is 100,000.
28var prizeWhole = [places]int64{
29 16_000, 12_500, 10_000, 8_000, 7_000,
30 6_000, 5_500, 5_000, 4_500, 4_000,
31 3_500, 3_000, 2_800, 2_500, 2_200,
32 2_000, 1_800, 1_500, 1_200, 1_000,
33}
34
35var (
36 inited bool
37 admin address
38 self address
39 endUnix int64
40 sealed bool
41 winner [places]address
42 paid [places]bool
43)
44
45// clockUnix stays 0 on chain. The test in this package sets it. No call writes it.
46var clockUnix int64
47
48// Init records the deploy wallet as admin. It does not move FISH. No coins.
49func Init(cur realm) {
50 caller := requireUser(cur)
51 takeExact(0)
52 if inited {
53 panic("fishseason: init")
54 }
55 admin = caller
56 self = cur.Address()
57 inited = true
58 chain.Emit("Init", "admin", admin.String())
59}
60
61// SetEnd stores the close, as Unix seconds. The deploy wallet only, and only before seal. No coins.
62func SetEnd(cur realm, unix int64) {
63 caller := requireUser(cur)
64 takeExact(0)
65 requireReady()
66 requireAdmin(caller)
67 if sealed || unix <= startUnix {
68 panic("fishseason: end")
69 }
70 endUnix = unix
71 chain.Emit("End", "unix", strconv.FormatInt(unix, 10))
72}
73
74// SetPlace posts one winner. Places are 1 through 20. The same address cannot take two places. No coins.
75func SetPlace(cur realm, place int, who address) {
76 caller := requireUser(cur)
77 takeExact(0)
78 requireReady()
79 requireAdmin(caller)
80 if sealed {
81 panic("fishseason: seal")
82 }
83 slot := slotOf(place)
84 if !who.IsValid() || who == seasonAddr || who == cur.Address() {
85 panic("fishseason: addr")
86 }
87 for i := 0; i < places; i++ {
88 if i != slot && winner[i] == who {
89 panic("fishseason: dup")
90 }
91 }
92 winner[slot] = who
93 chain.Emit("Place", "place", strconv.Itoa(place), "who", who.String())
94}
95
96// ClearPlace removes one unsealed place. No coins.
97func ClearPlace(cur realm, place int) {
98 caller := requireUser(cur)
99 takeExact(0)
100 requireReady()
101 requireAdmin(caller)
102 if sealed {
103 panic("fishseason: seal")
104 }
105 winner[slotOf(place)] = address("")
106 chain.Emit("Clear", "place", strconv.Itoa(place))
107}
108
109// Seal freezes the end and the places. At least one place must be posted. No coins.
110func Seal(cur realm) {
111 caller := requireUser(cur)
112 takeExact(0)
113 requireReady()
114 requireAdmin(caller)
115 if sealed || endUnix == 0 || filled() < 1 {
116 panic("fishseason: seal")
117 }
118 sealed = true
119 chain.Emit("Seal", "end", strconv.FormatInt(endUnix, 10))
120}
121
122// Pay sends each unpaid posted prize from the season wallet to that winner.
123// The board must be sealed and the close must have passed. No coins.
124// fishtoken must list this realm as a distributor, and the season wallet must
125// have approved this realm for the FISH. A second pay of the same place does nothing.
126func Pay(cur realm) {
127 requireUser(cur)
128 takeExact(0)
129 requireReady()
130 if !sealed {
131 panic("fishseason: seal")
132 }
133 if nowUnix() < endUnix {
134 panic("fishseason: early")
135 }
136 due := Due()
137 if due == 0 {
138 panic("fishseason: paid")
139 }
140 if !fish.IsDistributor(cur.Address()) {
141 panic("fishseason: grant")
142 }
143 if fish.Allowance(seasonAddr, cur.Address()) < due {
144 panic("fishseason: allowance")
145 }
146 n := 0
147 for i := 0; i < places; i++ {
148 if paid[i] || !winner[i].IsValid() {
149 continue
150 }
151 who := winner[i]
152 amt := prizeWhole[i] * unit
153 paid[i] = true
154 n++
155 fish.TransferFrom(cross(cur), seasonAddr, who, amt)
156 chain.Emit("Pay", "place", strconv.Itoa(i+1), "who", who.String(), "amount", strconv.FormatInt(amt, 10))
157 }
158 if n == 0 {
159 panic("fishseason: paid")
160 }
161}
162
163// TransferAdmin hands the board to next. Posted places stay. No coins.
164func TransferAdmin(cur realm, next address) {
165 caller := requireUser(cur)
166 takeExact(0)
167 requireReady()
168 requireAdmin(caller)
169 if !next.IsValid() || next == admin {
170 panic("fishseason: admin")
171 }
172 admin = next
173 chain.Emit("Admin", "next", next.String())
174}
175
176func Version() string { return "fishseason-1" }
177
178func Admin() address { return admin }
179
180func Realm() address { return self }
181
182func Start() int64 { return startUnix }
183
184func End() int64 { return endUnix }
185
186func Sealed() bool { return sealed }
187
188func SeasonWallet() address { return seasonAddr }
189
190func PlaceOf(place int) address { return winner[slotOf(place)] }
191
192func PrizeOf(place int) int64 { return prizeWhole[slotOf(place)] * unit }
193
194func PaidOf(place int) bool { return paid[slotOf(place)] }
195
196func Due() int64 {
197 var sum int64
198 for i := 0; i < places; i++ {
199 if !paid[i] && winner[i].IsValid() {
200 sum += prizeWhole[i] * unit
201 }
202 }
203 return sum
204}
205
206func Render(path string) string {
207 if path != "" {
208 return "# Season 1\n\nUnknown path.\n"
209 }
210 if !inited {
211 return "# Season 1\n\nNot opened.\n"
212 }
213 state := "open"
214 if sealed {
215 state = "sealed"
216 }
217 return "# Season 1\n\nStart " + strconv.FormatInt(startUnix, 10) +
218 ". End " + strconv.FormatInt(endUnix, 10) +
219 ". Board " + state +
220 ". Due " + strconv.FormatInt(Due(), 10) + " base units.\n"
221}
222
223func nowUnix() int64 {
224 if clockUnix != 0 {
225 return clockUnix
226 }
227 return time.Now().Unix()
228}
229
230func slotOf(place int) int {
231 if place < 1 || place > places {
232 panic("fishseason: place")
233 }
234 return place - 1
235}
236
237func filled() int {
238 n := 0
239 for i := 0; i < places; i++ {
240 if winner[i].IsValid() {
241 n++
242 }
243 }
244 return n
245}
246
247func requireReady() {
248 if !inited {
249 panic("fishseason: init")
250 }
251}
252
253func requireAdmin(caller address) {
254 if caller != admin {
255 panic("fishseason: admin")
256 }
257}
258
259func requireUser(cur realm) address {
260 if !cur.Previous().IsUserCall() {
261 panic("fishseason: eoa")
262 }
263 return cur.Previous().Address()
264}
265
266func takeExact(n int64) {
267 sent := unsafe.OriginSend()
268 if n == 0 {
269 if len(sent) != 0 {
270 panic("fishseason: payment")
271 }
272 return
273 }
274 panic("fishseason: payment")
275}