const MaxPayees
MaxPayees bounds the ledger so gas stays predictable.
Package pullpayment is the escrow ledger behind the pull-payment pattern, as a pure, reusable package.
gno.land/p/moul/x/daily/pullpayment/v0Pull-payment escrow ledger — New, Credit, CreditMany, Withdraw,
Forfeit, Balance, TotalOwed, TotalWithdrawn, Payees, Iterate,
Consistent, MaxPayees.
1import "gno.land/p/moul/x/daily/pullpayment/v0"
2
3l := pullpayment.New()
4l.CreditMany([]string{"alice", "bob"}, []int64{500, 300})
5l.TotalOwed() // 800 — what the realm must keep in reserve
6
7amt, err := l.Withdraw("bob") // 300; the balance is ALREADY zeroed
8// ...the caller transfers `amt` only now
The classic Solidity answer to reentrancy: never push value to an address, credit it and let the recipient withdraw. A push hands control to the recipient in the middle of your state transition, and a hostile recipient re-enters before you have finished updating.
This package is the bookkeeping half only — it moves no coins. The realm
holding the funds transfers after calling Withdraw, which is exactly the
ordering the pattern demands: checks, effects, then interactions. The balance
is already deleted when control leaves, so a reentrant Withdraw returns
ErrNothing and TotalWithdrawn is not double-counted. That property has its own
test.
Other guarantees, each tested:
CreditMany is all-or-nothing. A batch with one bad entry applies none of
itself — a ledger half-agreeing with the funds it guards is worse than a
rejected call.Consistent() is exported: TotalOwed always equals the sum of the balances.Payees comes back sorted, never in map order, so a Render built from it
cannot differ between nodes.
Live demo: r/moul/x/daily/pullpaymentdemo
· render it at /r/moul/x/daily/pullpaymentdemo/v0.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.
Package pullpayment is the escrow ledger behind the pull-payment pattern, as a pure, reusable package.
The pattern is the classic Solidity answer to reentrancy: never push value to an address, credit it and let the recipient withdraw. A push sends control to the recipient in the middle of your state transition, and a malicious recipient re-enters before you have finished updating. Pull inverts that — the recipient calls in, and their own withdrawal is the only state being touched.
This package is the BOOKKEEPING half only: who is owed what, and the checks-effects-interactions ordering that makes a withdrawal safe. It moves no coins. The realm that holds the funds performs the transfer AFTER calling Withdraw, which is exactly the ordering the pattern demands — the balance is already zeroed when the transfer happens, so a reentrant call finds nothing left to take.
Iteration is over sorted addresses, never a built-in map range: gno map iteration order is unspecified and a Render built from one can differ between nodes, which is a consensus bug rather than a cosmetic one.
A live demo of this package is at r/moul/x/daily/pullpaymentdemo(/r/moul/x/daily/pullpaymentdemo/v0).
Ledger records what each address is owed.
Balance returns what payee is currently owed; zero when nothing.
Consistent reports whether TotalOwed equals the sum of the balances. Always true through the public API; exported so callers can assert the invariant.
Count returns how many payees are owed something.
Credit records that payee is owed amount more. Amounts accumulate: crediting twice owes the sum.
CreditMany credits several payees, applying nothing unless every entry is valid — a partial split would leave the ledger disagreeing with the funds.
Forfeit drops a payee's credit without paying it, returning what was dropped.
IsEmpty reports whether nothing is owed to anyone.
Iterate calls fn for each payee in sorted order. Returning true stops.
Payees returns every address with an outstanding balance, sorted.
TotalOwed returns the sum of every outstanding balance — what the holding realm must keep in reserve.
TotalWithdrawn returns the lifetime sum of successful withdrawals.
Withdraw zeroes payee's balance and returns what was owed.
The caller transfers the returned amount AFTER this call. That ordering is the point of the pattern: the credit is already gone from the ledger when control passes to the recipient, so a reentrant Withdraw returns ErrNothing.