coinio.gno
5.55 Kb · 142 lines
1// Package coinio is the chain-facing coin plumbing that every
2// value-holding realm on gno.land repeats: verified payment receipt,
3// disciplined payout, and reserve-protected surplus sweeping. It was
4// extracted from two audited, pearl-1-validated realms (vault and
5// bounties) whose implementations of these mechanics were
6// line-identical.
7//
8// The package is PURE and STATELESS: it holds no balances, no roles,
9// and no configuration; all state stays in the consuming realm (and
10// its accounting package, e.g. feeledger). It emits no events —
11// consumers emit their own. Every function either succeeds or panics,
12// aborting the transaction: these are payment guards, and no failure
13// here has a meaningful recovery path.
14//
15// CAPABILITY CONTRACT: pure packages cannot declare crossing
16// functions (a first `realm` parameter), so every coin-moving function
17// here uses the canonical secondary-parameter shape `(_ int, rlm
18// realm, ...)` — the same pattern as chain treasury packages. Pass 0
19// and your crossing entrypoint's own live `cur` (forwarded through
20// non-crossing calls keeps it current). Each function asserts
21// rlm.IsCurrent() before acting, so a stale, stored, or Previous()
22// realm value fails closed (the designation-forgery guard secondary
23// realm parameters require). Coins can only move FROM rlm.Address() —
24// the calling realm itself — so no consumer can spend another realm's
25// funds through this package.
26//
27// AUTHORIZATION is the consumer's responsibility: coinio decides HOW
28// coins move, never WHO may move them. Gate your entrypoints before
29// calling in.
30//
31// ORDERING CONTRACT (the one invariant coinio cannot enforce): debit
32// your own accounting BEFORE calling Payout or Sweep
33// (checks-effects-interactions). A panic inside coinio aborts the
34// whole transaction, reverting your debit with it — that is what makes
35// the debit-first order safe.
36package coinio
37
38import (
39 "chain"
40 "chain/banker"
41 "chain/runtime/unsafe"
42)
43
44// Receive verifies the canonical receipt-guaranteed payment shape and
45// returns the payer and amount:
46//
47// - the caller of the consuming realm's entrypoint is an EOA via
48// MsgCall (IsUserCall) — the only shape where the chain guarantees
49// the -send envelope landed at the realm address before the body
50// ran (realms and maketx-run are rejected);
51// - the envelope is exactly one coin of the given denom;
52// - the amount is positive.
53//
54// Receive is a READ of the transaction's send envelope, NOT a
55// consumption: a stateless pure package cannot mark an envelope spent,
56// so calling Receive N times in one transaction reports the same
57// envelope N times. CALL IT AT MOST ONCE PER TRANSACTION and credit
58// its result at most once — a consumer that credits per call mints
59// unbacked liabilities from a single -send.
60//
61// Call it first (and once) in any payable crossing entrypoint:
62//
63// func Deposit(cur realm) {
64// from, amount := coinio.Receive(0, cur, "ugnot")
65// // credit `from` with `amount` in your accounting
66// }
67func Receive(_ int, rlm realm, denom string) (from address, amount int64) {
68 if !rlm.IsCurrent() {
69 panic("coinio: realm capability is not current")
70 }
71 if denom == "" {
72 panic("coinio: empty denom")
73 }
74 if !rlm.Previous().IsUserCall() {
75 panic("coinio: payment must be a direct EOA call with -send (realms and maketx-run are rejected)")
76 }
77 sent := unsafe.OriginSend()
78 if len(sent) != 1 || sent[0].Denom != denom {
79 panic("coinio: send exactly one coin type: " + denom)
80 }
81 if sent[0].Amount <= 0 {
82 panic("coinio: amount must be positive")
83 }
84 return rlm.Previous().Address(), sent[0].Amount
85}
86
87// Payout sends amount of denom from the calling realm's own address to
88// `to`. DEBIT YOUR ACCOUNTING FIRST — a panic here (or anywhere later
89// in the transaction) reverts the debit together with the send.
90func Payout(_ int, rlm realm, to address, denom string, amount int64) {
91 if !rlm.IsCurrent() {
92 panic("coinio: realm capability is not current")
93 }
94 var zero address
95 if to == zero {
96 panic("coinio: empty payout address")
97 }
98 if denom == "" {
99 panic("coinio: empty denom")
100 }
101 if amount <= 0 {
102 panic("coinio: amount must be positive")
103 }
104 bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)
105 bk.SendCoins(rlm.Address(), to, chain.Coins{chain.NewCoin(denom, amount)})
106}
107
108// Sweep sends the surplus of a single denomination — everything the
109// calling realm holds above `reserve` — to `to`, and returns the swept
110// amount. Pass your total liabilities as the reserve for the denom
111// your accounting tracks, and 0 for foreign denominations. Panics if
112// there is no positive surplus, so reserved funds are untouchable by
113// construction. One denomination per call keeps the operation gas-
114// bounded regardless of how many junk denoms third parties force-send.
115func Sweep(_ int, rlm realm, to address, denom string, reserve int64) int64 {
116 if !rlm.IsCurrent() {
117 panic("coinio: realm capability is not current")
118 }
119 var zero address
120 if to == zero {
121 panic("coinio: empty sweep address")
122 }
123 if denom == "" {
124 panic("coinio: empty denom")
125 }
126 if reserve < 0 {
127 panic("coinio: negative reserve")
128 }
129 bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)
130 surplus := bk.GetCoin(rlm.Address(), denom) - reserve
131 if surplus <= 0 {
132 panic("coinio: no surplus to sweep for " + denom)
133 }
134 bk.SendCoins(rlm.Address(), to, chain.Coins{chain.NewCoin(denom, surplus)})
135 return surplus
136}
137
138// HeldAt returns the amount of denom held at addr. Read-only; usable
139// from views and Render without a realm capability.
140func HeldAt(addr address, denom string) int64 {
141 return banker.NewReadonlyBanker().GetCoin(addr, denom)
142}