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.

pullpayment.gno

5.27 Kb · 180 lines
  1// Package pullpayment is the escrow ledger behind the pull-payment pattern, as
  2// a pure, reusable package.
  3//
  4// The pattern is the classic Solidity answer to reentrancy: never push value to
  5// an address, credit it and let the recipient withdraw. A push sends control to
  6// the recipient in the middle of your state transition, and a malicious
  7// recipient re-enters before you have finished updating. Pull inverts that —
  8// the recipient calls in, and their own withdrawal is the only state being
  9// touched.
 10//
 11// This package is the BOOKKEEPING half only: who is owed what, and the
 12// checks-effects-interactions ordering that makes a withdrawal safe. It moves
 13// no coins. The realm that holds the funds performs the transfer AFTER calling
 14// Withdraw, which is exactly the ordering the pattern demands — the balance is
 15// already zeroed when the transfer happens, so a reentrant call finds nothing
 16// left to take.
 17//
 18// Iteration is over sorted addresses, never a built-in map range: gno map
 19// iteration order is unspecified and a Render built from one can differ between
 20// nodes, which is a consensus bug rather than a cosmetic one.
 21//
 22// A live demo of this package is at
 23// [r/moul/x/daily/pullpaymentdemo](/r/nym-moultest123/x/daily/pullpaymentdemo/v0).
 24package pullpayment
 25
 26import (
 27	"errors"
 28	"sort"
 29)
 30
 31// MaxPayees bounds the ledger so gas stays predictable.
 32const MaxPayees = 4096
 33
 34var (
 35	ErrBadAmount = errors.New("pullpayment: amount must be positive")
 36	ErrFull      = errors.New("pullpayment: too many payees")
 37	ErrNothing   = errors.New("pullpayment: nothing to withdraw")
 38	ErrOverflow  = errors.New("pullpayment: credit would overflow")
 39)
 40
 41const maxInt64 = int64(9223372036854775807)
 42
 43// Ledger records what each address is owed.
 44type Ledger struct {
 45	owed      map[string]int64
 46	total     int64
 47	withdrawn int64
 48}
 49
 50// New returns an empty Ledger.
 51func New() *Ledger { return &Ledger{owed: map[string]int64{}} }
 52
 53// Credit records that payee is owed amount more. Amounts accumulate: crediting
 54// twice owes the sum.
 55func (l *Ledger) Credit(payee string, amount int64) error {
 56	if amount <= 0 {
 57		return ErrBadAmount
 58	}
 59	cur, seen := l.owed[payee]
 60	if !seen && len(l.owed) >= MaxPayees {
 61		return ErrFull
 62	}
 63	if cur > maxInt64-amount || l.total > maxInt64-amount {
 64		return ErrOverflow
 65	}
 66	l.owed[payee] = cur + amount
 67	l.total += amount
 68	return nil
 69}
 70
 71// Balance returns what payee is currently owed; zero when nothing.
 72func (l *Ledger) Balance(payee string) int64 { return l.owed[payee] }
 73
 74// Withdraw zeroes payee's balance and returns what was owed.
 75//
 76// The caller transfers the returned amount AFTER this call. That ordering is
 77// the point of the pattern: the credit is already gone from the ledger when
 78// control passes to the recipient, so a reentrant Withdraw returns ErrNothing.
 79func (l *Ledger) Withdraw(payee string) (int64, error) {
 80	amount, ok := l.owed[payee]
 81	if !ok || amount == 0 {
 82		return 0, ErrNothing
 83	}
 84	delete(l.owed, payee) // effects before interactions
 85	l.total -= amount
 86	l.withdrawn += amount
 87	return amount, nil
 88}
 89
 90// Forfeit drops a payee's credit without paying it, returning what was dropped.
 91func (l *Ledger) Forfeit(payee string) (int64, error) {
 92	amount, ok := l.owed[payee]
 93	if !ok || amount == 0 {
 94		return 0, ErrNothing
 95	}
 96	delete(l.owed, payee)
 97	l.total -= amount
 98	return amount, nil
 99}
100
101// TotalOwed returns the sum of every outstanding balance — what the holding
102// realm must keep in reserve.
103func (l *Ledger) TotalOwed() int64 { return l.total }
104
105// TotalWithdrawn returns the lifetime sum of successful withdrawals.
106func (l *Ledger) TotalWithdrawn() int64 { return l.withdrawn }
107
108// Payees returns every address with an outstanding balance, sorted.
109func (l *Ledger) Payees() []string {
110	out := make([]string, 0, len(l.owed))
111	for p := range l.owed {
112		out = append(out, p)
113	}
114	sort.Strings(out)
115	return out
116}
117
118// Count returns how many payees are owed something.
119func (l *Ledger) Count() int { return len(l.owed) }
120
121// IsEmpty reports whether nothing is owed to anyone.
122func (l *Ledger) IsEmpty() bool { return len(l.owed) == 0 }
123
124// Iterate calls fn for each payee in sorted order. Returning true stops.
125func (l *Ledger) Iterate(fn func(payee string, amount int64) bool) {
126	for _, p := range l.Payees() {
127		if fn(p, l.owed[p]) {
128			return
129		}
130	}
131}
132
133// CreditMany credits several payees, applying nothing unless every entry is
134// valid — a partial split would leave the ledger disagreeing with the funds.
135func (l *Ledger) CreditMany(payees []string, amounts []int64) error {
136	if len(payees) != len(amounts) {
137		return ErrBadAmount
138	}
139	// Validate first.
140	for _, a := range amounts {
141		if a <= 0 {
142			return ErrBadAmount
143		}
144	}
145	probe := len(l.owed)
146	for _, p := range payees {
147		if _, seen := l.owed[p]; !seen {
148			probe++
149		}
150	}
151	if probe > MaxPayees {
152		return ErrFull
153	}
154	var sum int64
155	for _, a := range amounts {
156		if sum > maxInt64-a {
157			return ErrOverflow
158		}
159		sum += a
160	}
161	if l.total > maxInt64-sum {
162		return ErrOverflow
163	}
164	// Then apply.
165	for i, p := range payees {
166		l.owed[p] += amounts[i]
167	}
168	l.total += sum
169	return nil
170}
171
172// Consistent reports whether TotalOwed equals the sum of the balances. Always
173// true through the public API; exported so callers can assert the invariant.
174func (l *Ledger) Consistent() bool {
175	var sum int64
176	for _, a := range l.owed {
177		sum += a
178	}
179	return sum == l.total
180}