A weighted split of an integer amount that adds up: Split, Share,
Total.
1import"gno.land/p/moul/x/games/prorata/v0"23prorata.Split(100,[]int64{1,2,3})// [17 33 50], and it sums to exactly 1004prorata.Split(10,[]int64{1,1,1})// [4 3 3], the dust goes to the lowest index5prorata.Share(100,1,3)// 33, floored: "what would I get"
Every payout in a game is this operation: a pot, a roster, a weight each. Written
the obvious way, amount*weight/total per claimant, it is wrong in two ways that
only show up once there is real money in the contract.
The shares do not add up. Each one is rounded down, so their sum is short by
up to one unit per claimant. That dust either sits in the realm forever or lands
on whoever the loop happened to pay last, which is a rule nobody chose. Split
distributes by largest remainder: floor every share, then hand the leftover
units out one at a time to the claimants whose truncated fraction was largest,
ties going to the lower index. The result sums to exactly the amount, and the
rule is deterministic, so every node computes the same split and a Render of it
does not change between calls.
amount*weight overflows an int64 long before either factor does, which
turns a large payout negative. Nothing here ever computes that product: it splits
the amount into whole multiples of the total and what is left over, so
amount*weight/total becomes q*weight + r*weight/total with r already
smaller than the total. A pot of 10^18 ugnot across billion-unit weights is
exact, and what genuinely cannot fit returns ErrOverflow rather than wrapping.
Two smaller decisions, each with a test:
A zero weight is paid nothing, including no remainder unit. Somebody with
no claim is not handed dust.
An empty roster, or one where every weight is zero, is an error, not an
empty split. Silently returning nothing would strand the amount, and only the
caller knows where it should go instead.
Nothing here reads the chain and nothing holds coins: the caller owns the roster
and the custody.
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.
Overview
Package prorata splits an integer amount across weighted claimants without leaking or inventing a unit.
Every payout in a game is this operation: a pot, a roster, and a weight each. Written the obvious way, `amount*weight/total` per claimant, it is wrong in two ways that only show up with real money in the contract. The shares are each rounded down, so their sum is short of the amount by up to one unit per claimant, and that dust either accumulates in the contract forever or gets handed to whoever the code happens to pay last. And `amount*weight` overflows an int64 long before either factor does, which turns a large payout negative.
Split fixes both. It distributes by largest remainder: floor every share, then hand the leftover units out one at a time to the claimants whose truncated fraction was largest, ties going to the lower index. The result sums to EXACTLY the amount, every time, and the rule is deterministic, so every node computes the same split and a Render of it does not change between calls.
Nothing here reads the chain and nothing holds coins: the caller owns the roster and the custody. A game built on this package is at r/moul/x/games/lastwords(/r/moul/x/games/lastwords/v0).
1var( 2// ErrNegativeAmount is returned when the amount to split is negative. 3ErrNegativeAmount=errors.New("prorata: amount must not be negative") 4// ErrNegativeWeight is returned when any weight is negative. 5ErrNegativeWeight=errors.New("prorata: weights must not be negative") 6// ErrNoWeight is returned when there is nobody to pay, or every weight is 7// zero. The caller must decide where the amount goes instead: silently 8// returning an empty split would strand it. 9ErrNoWeight=errors.New("prorata: total weight is zero, nothing to split across")10// ErrOverflow is returned when the arithmetic does not fit in an int64.11ErrOverflow=errors.New("prorata: weights overflow int64")12)
Share returns what one weight is owed out of amount, rounded down. It is the "what would I get" query, and deliberately does NOT account for the remainder: use Split when the shares have to add up.