hlfees.gno
9.48 Kb · 248 lines
1// Package hlfees is the bridge's fee realm (bridge/PLAN.md §5.5): the routes pay every fee here — GNOT as coins,
2// BUBBLE through the token realm — and only the treasurer a decision names may withdraw, to itself. The treasurer
3// at launch is compiled in below; a decision made with the builder changes it, 7 days after it is made — the
4// governor's own through Govern until it yields on r/hlmailbox, a GovDAO proposal's after (M1k).
5package hlfees
6
7import (
8 "chain"
9 "chain/banker"
10 "strconv"
11 "strings"
12 "time"
13
14 "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0"
15 "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble"
16 "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlmailbox"
17 "gno.land/r/gov/dao"
18)
19
20const (
21 namespace = "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/"
22 pkgPath = namespace + "hlfees" // must match gnomod.toml
23
24 // launchTreasurer is the operator's named key (§5.5, §11.12): a PLACEHOLDER — the deployer's hotkey — until
25 // the launch notice names it; GovDAO can name the chain's own address instead at any vote.
26 launchTreasurer = address("g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp")
27)
28
29// launchGovernor is the governor (M1k): the mailbox's, compiled here too so a member reads it in this realm's
30// served source; init refuses a deploy with another. bridge/local/e2e.sh swaps it for gnodev's test1.
31const launchGovernor = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6")
32
33var (
34 treasurer = launchTreasurer
35 ledger decisions.Ledger
36)
37
38// init refuses a deploy whose compiled governor is not the mailbox's (M1k): the bridge's realms are governed by
39// one key until it yields, or by GovDAO from the first block, never a mix.
40func init() {
41 if launchGovernor != hlmailbox.Governor() {
42 panic("hlfees: the compiled governor " + string(launchGovernor) + " is not the mailbox's " + string(hlmailbox.Governor()))
43 }
44}
45
46type (
47 treasurerParams struct{ to address }
48 cancelParams struct{ id int64 }
49)
50
51func now() int64 { return time.Now().Unix() }
52
53func itoa(n int64) string { return strconv.FormatInt(n, 10) }
54
55// Address is this realm's address: where the routes pay the fees.
56func Address() address { return chain.PackageAddress(pkgPath) }
57
58// Treasurer is the one address that may withdraw.
59func Treasurer() address { return treasurer }
60
61// Balances are the fees held: ugnot and BUBBLE base units.
62func Balances() (ugnot, bubbles int64) {
63 return banker.NewReadonlyBanker().GetCoin(Address(), "ugnot"), bubble.BalanceOf(Address())
64}
65
66// Withdraw pays everything held to the treasurer, who alone may call it.
67func Withdraw(cur realm) {
68 if !cur.IsCurrent() {
69 panic("hlfees: that realm value is not live; pass your own cur")
70 }
71 if cur.Previous().Address() != treasurer {
72 panic("hlfees: only the treasurer withdraws")
73 }
74 ugnot, bubbles := Balances()
75 if ugnot > 0 {
76 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(Address(), treasurer, chain.Coins{{"ugnot", ugnot}})
77 }
78 if bubbles > 0 {
79 bubble.Transfer(cross(cur), treasurer, bubbles)
80 }
81 chain.Emit("Withdrawn", "to", string(treasurer), "ugnot", itoa(ugnot), "bubble", itoa(bubbles))
82}
83
84// ------------------------------------------------------------ builders --
85
86// Request is what a builder returns (M1k): the proposal GovDAO's proxy executes after the yield (Proposal), and
87// the same decision the governor runs directly before it (Govern); see hlgnot's.
88type Request struct {
89 proposal dao.ProposalRequest
90 text string
91 decide func(cur realm) error
92}
93
94// Proposal is the request as a GovDAO proposal: `dao.MustCreateProposal(cross(cur), req.Proposal())`.
95func (r Request) Proposal() dao.ProposalRequest { return r.proposal }
96
97// Description is the proposal's: the parameters as the builder wrote them.
98func (r Request) Description() string { return r.text }
99
100// who names a previous realm in a refusal: its pkgpath, or a user's address.
101func who(path string, a address) string {
102 if path != "" {
103 return path
104 }
105 return string(a)
106}
107
108// mustBeGovernor is a decision's identity check (M1k), read from the closure's own live cur: the governor's own
109// call until it yields on r/hlmailbox, gno.land/r/gov/dao's proxy exactly after (or from the first block, when no
110// governor was compiled); see hlgnot's.
111func mustBeGovernor(cur realm) {
112 prev := cur.Previous()
113 if decisions.DAOGoverns(launchGovernor, hlmailbox.Yielded()) {
114 if p := prev.PkgPath(); p != decisions.DAOPath {
115 panic("hlfees: only " + decisions.DAOPath + " executes a decision, not " + who(prev.PkgPath(), prev.Address()))
116 }
117 return
118 }
119 if !prev.IsUser() || prev.Address() != launchGovernor {
120 panic("hlfees: until the governor yields, " + string(launchGovernor) + " alone decides, by Govern from its own key: not " + who(prev.PkgPath(), prev.Address()) + " (a GovDAO proposal is refused until the yield)")
121 }
122}
123
124func mustBeLive() {
125 if !hlmailbox.Live() {
126 panic("hlfees: not launched: before the Launch decision only cancel is proposed")
127 }
128}
129
130func propose(cur realm, kind, text string, apply func(made, t int64)) Request {
131 if !cur.IsCurrent() {
132 panic("hlfees: that realm value is not live; pass your own cur")
133 }
134 made := now()
135 cb := func(cur realm) error {
136 mustBeGovernor(cur)
137 t := now()
138 if err := decisions.Expired(made, t); err != nil {
139 return err
140 }
141 apply(made, t)
142 return nil
143 }
144 return Request{proposal: dao.NewProposalRequest("hlfees: "+kind, text, dao.NewSimpleExecutor(0, cur, cb, text)), text: text, decide: cb}
145}
146
147// Govern is the governor's way in before the yield (M1k): the same closure GovDAO's proxy runs after it, called
148// non-crossing in this frame so it reads the governor from cur.Previous(); refused for anyone else and for
149// everyone once the governor yielded on r/hlmailbox (or when none was compiled); see hlgnot's.
150func Govern(cur realm, r Request) {
151 if !cur.IsCurrent() {
152 panic("hlfees: that realm value is not live; pass your own cur")
153 }
154 if r.decide == nil {
155 panic("hlfees: not a builder's request")
156 }
157 if yielded := hlmailbox.Yielded(); decisions.DAOGoverns(launchGovernor, yielded) {
158 if launchGovernor == "" {
159 panic("hlfees: no governor was compiled, " + decisions.DAOPath + " governs from the first block: a decision is a GovDAO proposal, never Govern")
160 }
161 panic("hlfees: the governor yielded at height " + itoa(yielded) + ", " + decisions.DAOPath + " governs since: a decision is a GovDAO proposal, never Govern")
162 }
163 mustBeGovernor(cur)
164 if err := r.decide(cur); err != nil {
165 panic("hlfees: " + err.Error())
166 }
167}
168
169// NewSetTreasurerRequest names the address that may withdraw, 7 days after the decision is made.
170func NewSetTreasurerRequest(cur realm, to address) Request {
171 mustBeLive()
172 if !to.IsValid() {
173 panic("hlfees: not an address: " + string(to))
174 }
175 if to == treasurer {
176 panic("hlfees: " + string(to) + " is the treasurer already")
177 }
178 text := "set treasurer to " + string(to) + " (now " + string(treasurer) + ")"
179 return propose(cur, "set treasurer", text, func(made, t int64) {
180 mustBeLive()
181 d := ledger.Record("set treasurer", text, treasurerParams{to}, made, t, t+decisions.Delay)
182 d.Note = "queued 7 days"
183 chain.Emit("Decision", "id", itoa(d.ID), "kind", "set treasurer", "state", "queued")
184 })
185}
186
187// NewCancelRequest cancels a pending decision of this realm, at once.
188func NewCancelRequest(cur realm, id int64) Request {
189 d := ledger.Get(id)
190 if d == nil || !d.Pending() {
191 panic("hlfees: decision " + itoa(id) + " is not pending")
192 }
193 text := "cancel decision " + itoa(id) + " (" + d.Text + ")"
194 return propose(cur, "cancel", text, func(made, t int64) {
195 if _, err := ledger.Cancel(id, t); err != nil {
196 panic("hlfees: " + err.Error())
197 }
198 c := ledger.Record("cancel", text, cancelParams{id}, made, t, t)
199 ledger.Apply(c, t, "at once: cancelled")
200 })
201}
202
203// Execute applies a queued decision whose 7 days have passed; anyone may call it.
204func Execute(cur realm, id int64) {
205 if !cur.IsCurrent() {
206 panic("hlfees: that realm value is not live; pass your own cur")
207 }
208 t := now()
209 d, err := ledger.Due(id, t)
210 if err != nil {
211 panic("hlfees: " + err.Error())
212 }
213 p, ok := d.Params.(treasurerParams)
214 if !ok {
215 panic("hlfees: decision " + itoa(id) + " has nothing to apply")
216 }
217 treasurer = p.to
218 ledger.Apply(d, t, "applied after the delay")
219 chain.Emit("TreasurerSet", "address", string(treasurer))
220}
221
222// --------------------------------------------------------------- pages --
223
224// Render: "" the page; "json" the figures; "decisions" and "decisions.json" the decisions.
225func Render(path string) string {
226 ugnot, bubbles := Balances()
227 switch path {
228 case "":
229 var b strings.Builder
230 b.WriteString("# The bridge's fees\n\n")
231 b.WriteString("Every fee the bridge takes lands here: GNOT as coins, BUBBLE as the token. Only the treasurer a decision names withdraws, to itself.\n\n")
232 b.WriteString("- held: " + itoa(ugnot) + " ugnot, " + itoa(bubbles) + " BUBBLE base units\n")
233 b.WriteString("- treasurer: " + string(treasurer) + "\n")
234 b.WriteString("- " + decisions.GovernorLine(launchGovernor, hlmailbox.Yielded()) + "\n")
235 b.WriteString("- " + decisions.Clocks() + "\n")
236 b.WriteString("\n## Decisions\n\n" + ledger.Page())
237 return b.String()
238 case "json":
239 return "{\"address\":" + strconv.Quote(string(Address())) + ",\"treasurer\":" + strconv.Quote(string(treasurer)) +
240 "," + decisions.GovernorJSON(launchGovernor, hlmailbox.Yielded()) +
241 ",\"ugnot\":" + itoa(ugnot) + ",\"bubble\":" + itoa(bubbles) + ",\"clocks\":" + decisions.ClocksJSON() + ",\"decisions\":" + itoa(ledger.Last()) + "}"
242 case "decisions":
243 return "# Decisions\n\n" + ledger.Page()
244 case "decisions.json":
245 return ledger.JSON()
246 }
247 return "unknown path; see the page"
248}