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

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}