Transaction

7FBAAF1B7121B4…E1953A62B1FB

Block 337,132 · index 1 · RPC

Summary

Hash
7FBAAF1B7121B4BADDA4A1BB65932BDB80ED32551466A80C618CE1953A62B1FB
Block
337,132
Size
34908 bytes
Gas used
46,434,354 / 250,000,000
Fee
250000 ugnot
Status
success

Messages

Attached funds
12000000 ugnot

Arguments · 9

  1. #1revshare
  2. #2gnomod.toml
  3. #3module = "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/revshare" gno = "0.9"
  4. #4revshare.gno
  5. #5// Package revshare is a team revenue realm that IS a subscription // provider. The live subscriptions realm supports realm providers but // warns, in its own frozen header, that a realm provider "must expose // its own crossing path to Claim, or what it earns is stranded." // revshare is that crossing path, plus the one thing a team needs on // top of it: pulled revenue is split among weighted members into // pull-claimable balances. // // THE ECONOMIC COMPOSITION, PRECISELY: // // components : subscriptions (live realm — the revenue // machine and the upstream custodian), // feeledger (member balance accounting, // fee cap 0), coinio (payouts, held-balance // reads, reserve-protected sweep). // value boundaries : B1 subscriber EOA -> subscriptions (plan // price, origin envelope; governed by // subscriptions' own H == U + F). B2 // subscriptions -> revshare (banker send: // ClaimAll pays the caller, and the caller // is this realm). B3 revshare -> member EOA // (coinio.Payout on Claim/ClaimAll). // who owns which state : subscriptions owns plans, subs and the // provider's claimable balance (revshare's // RECEIVABLE); revshare owns the member // table, weights, and the member balances in // its own ledger. No state is shared; the // only coupling is the crossing calls and // the coins that move over B2. // who controls funds : upstream, only a claim by this realm can // move its receivable (provider-keyed // ledger). Here, member balances move only // to their owner (pull claims); the admin // can NEVER touch earned balances — sweep // reserves them, weight changes affect only // FUTURE distributions. // identity propagation : downstream sees cur.Previous() = THIS // realm on every crossing call, so the plan // provider and the claim beneficiary are the // realm address by construction — no admin // or member identity ever reaches the // downstream realm. // conservation : Held == UsersTotal + S (surplus above the // Liabilities() reserve, recoverable only by // SweepDenom). Distribution is exact: shares // are floor(amount*w/W) via the // overflow-free split (A/W)*w + ((A%W)*w)/W, // and the remainder goes to the // highest-weight member (ties: lowest // address) — fee_split's deterministic dust // policy, so no residual pool exists. // Cross-boundary: lifetime Pulled equals the // sum of all distributions, and the // downstream receivable is NOT part of Held. // ordering : Pull refuses BEFORE the downstream call // (no members configured = refuse), claims // downstream, MEASURES the arrival as a // held-balance delta, then distributes // exactly what arrived. State-after-call: // the only local mutations happen after the // boundary, on measured coins. // downstream abort : "nothing to claim" (or any downstream // panic) aborts the whole Pull — no local // state exists yet to corrupt, by ordering // AND by VM atomicity. There is no recover // anywhere in this realm, and none may be // added. // replay : a second Pull finds a zero downstream // balance and aborts there. Distribution // credits are driven by the measured delta, // so a replayed Pull cannot double-count // even in principle. // trust : revshare does not trust the downstream // reply beyond "it did not abort" — it // distributes the measured balance delta, // not a reported amount. Downstream // validates nothing about this caller; the // provider ledger is keyed by address. // terminal states : plans retire downstream (provider-only, // exposed here admin-gated); members can be // removed (earned balances survive removal // and stay claimable); the realm itself has // no terminal state — a team that walks away // leaves only pull-claimable balances. // adversarial callers : Pull is permissionless — it can only move // the receivable into member balances at the // fixed weights, so a stranger's Pull is a // free favor. Plan creation/retirement is // admin-gated (the downstream per-provider // plan quota is a griefable resource). // Members trust the admin for FUTURE weights // only, never for earned balances. All // entrypoints refuse coin-carrying // transactions (the downstream assertNoSend // reads the origin envelope; ours matches). // // One team per deploy: subscriptions keeps ONE claimable balance per // provider address, so a multi-team router behind one realm address // could not attribute revenue at the boundary. This is a measured // constraint of the downstream API, not a choice. package revshare import ( "chain" "chain/runtime/unsafe" "strconv" "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio" "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger" subs "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions" ) // Denom is the only asset this realm accounts. const Denom = "ugnot" // MaxMembers bounds the distribution loop; MaxWeight bounds a single // weight. Together they cap totalWeight at 200,000, which makes the // remainder step of the split ((A%W)*w) provably overflow-free for // any int64 coin amount — the conservation math cannot trap. const ( MaxMembers = 20 MaxWeight = int64(10000) ) type member struct { addr address weight int64 } var ( admin address // manages members, plans; stages a successor pendingAdmin address // staged by TransferAdmin, completes via AcceptAdmin sweeper address // may sweep out-of-band surplus self address // this realm's address, captured at deploy members []member // insertion-ordered; bounded by MaxMembers totalWeight int64 ledger = feeledger.MustNew(0) planIDs []int64 // plans created through this realm (ops/Render) pulled int64 // lifetime revenue pulled over boundary B2 ) func init() { admin = unsafe.OriginCaller() sweeper = admin self = unsafe.CurrentRealm().Address() } // --- membership (admin-gated; future distributions only) --- // AddMember adds a weighted member. Weight changes never touch earned // balances; they shape future pulls only. func AddMember(cur realm, a address, weight int64) { assertNoSend() assertAdmin(cur.Previous().Address()) var zero address if a == zero { panic("member must not be the zero address") } if weight < 1 || weight > MaxWeight { panic("weight must be in [1, " + itoa(MaxWeight) + "]") } if len(members) >= MaxMembers { panic("member table is full (" + itoa(int64(MaxMembers)) + ")") } if indexOf(a) >= 0 { panic("already a member") } members = append(members, member{addr: a, weight: weight}) totalWeight += weight chain.Emit("MemberAdded", "addr", a.String(), "weight", itoa(weight)) } // SetWeight changes a member's weight for future distributions. func SetWeight(cur realm, a address, weight int64) { assertNoSend() assertAdmin(cur.Previous().Address()) if weight < 1 || weight > MaxWeight { panic("weight must be in [1, " + itoa(MaxWeight) + "]") } i := indexOf(a) if i < 0 { panic("not a member") } old := members[i].weight totalWeight += weight - old members[i].weight = weight chain.Emit("WeightSet", "addr", a.String(), "old", itoa(old), "new", itoa(weight)) } // RemoveMember removes a member from future distributions. The // member's earned balance is untouched and stays claimable forever. func RemoveMember(cur realm, a address) { assertNoSend() assertAdmin(cur.Previous().Address()) i := indexOf(a) if i < 0 { panic("not a member") } totalWeight -= members[i].weight members = append(members[:i], members[i+1:]...) chain.Emit("MemberRemoved", "addr", a.String()) } // --- the provider surface: crossing paths into subscriptions --- // CreatePlan creates a subscription plan THROUGH this realm, making // the realm the plan's provider downstream. Admin only: the // per-provider plan quota downstream is a griefable resource. func CreatePlan(cur realm, title, description string, price, periodBlocks, maxFeeBps int64) int64 { assertNoSend() assertAdmin(cur.Previous().Address()) id := subs.CreatePlan(cross(cur), title, description, price, periodBlocks, maxFeeBps) planIDs = append(planIDs, id) chain.Emit("PlanCreated", "planId", itoa(id), "price", itoa(price), "periodBlocks", itoa(periodBlocks)) return id } // RetirePlan retires one of this realm's plans downstream. Admin only. func RetirePlan(cur realm, planID int64) { assertNoSend() assertAdmin(cur.Previous().Address()) subs.RetirePlan(cross(cur), planID) chain.Emit("PlanRetired", "planId", itoa(planID)) } // Pull claims this realm's entire accrued provider balance from the // subscriptions realm and distributes it to the members by weight. // Permissionless: pulling can only move the receivable into member // balances at the fixed weights, so anyone may crank it. Refuses // BEFORE the downstream call when no member could receive the funds. func Pull(cur realm) int64 { assertNoSend() if totalWeight == 0 { panic("no members configured to receive revenue") } before := coinio.HeldAt(self, Denom) subs.ClaimAll(cross(cur)) amount := coinio.HeldAt(self, Denom) - before if amount <= 0 { panic("downstream claim delivered nothing") } distribute(amount) pulled += amount chain.Emit("Pulled", "amount", itoa(amount), "pulledTotal", itoa(pulled)) return amount } // distribute splits amount by member weight. Shares are computed as // (A/W)*w + ((A%W)*w)/W — floor(A*w/W) without any multiplication // that can overflow. The remainder (< number of members) goes to the // highest-weight member, lowest address on ties: deterministic, and // it leaves the realm with no undistributed pool at rest. func distribute(amount int64) { q := amount / totalWeight r := amount % totalWeight distributed := int64(0) for _, m := range members { share := q*m.weight + (r*m.weight)/totalWeight if share > 0 { ledger.MustDeposit(m.addr.String(), share, 0) distributed += share } } if dust := amount - distributed; dust > 0 { ledger.MustDeposit(dustRecipient().String(), dust, 0) } } func dustRecipient() address { best := members[0] for _, m := range members[1:] { if m.weight > best.weight || (m.weight == best.weight && m.addr.String() < best.addr.String()) { best = m } } return best.addr } // --- member claims --- // Claim sends amount ugnot of the caller's earned balance to the // caller. func Claim(cur realm, amount int64) { assertNoSend() caller := cur.Previous().Address() if err := ledger.Withdraw(caller.String(), amount); err != nil { panic(err) } coinio.Payout(0, cur, caller, Denom, amount) chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount)) } // ClaimAll sends the caller's entire earned balance to the caller. func ClaimAll(cur realm) { assertNoSend() caller := cur.Previous().Address() amount, err := ledger.WithdrawAll(caller.String()) if err != nil { panic(err) } if amount == 0 { panic("nothing to claim") } coinio.Payout(0, cur, caller, Denom, amount) chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount)) } // --- administration --- // SweepDenom recovers out-of-band coins to the sweeper. For the // accounting denom the reserve is the full ledger liability — member // balances are structurally unsweepable. Sweeper only. func SweepDenom(cur realm, denom string) { assertNoSend() if cur.Previous().Address() != sweeper { panic("sweeper only") } reserve := int64(0) if denom == Denom { reserve = ledger.Liabilities() } swept := coinio.Sweep(0, cur, sweeper, denom, reserve) chain.Emit("SurplusSwept", "to", sweeper.String(), "coins", itoa(swept)+denom) } // TransferAdmin stages a two-step admin handover. func TransferAdmin(cur realm, successor address) { assertNoSend() assertAdmin(cur.Previous().Address()) var zero address if successor == zero { panic("successor must not be the zero address") } pendingAdmin = successor chain.Emit("AdminTransferStaged", "from", admin.String(), "to", successor.String()) } // AcceptAdmin completes the handover; only the staged successor may. // The sweeper role moves with the admin. func AcceptAdmin(cur realm) { assertNoSend() caller := cur.Previous().Address() if caller != pendingAdmin { panic("only the staged successor may accept") } old := admin admin = caller sweeper = caller var zero address pendingAdmin = zero chain.Emit("AdminTransferred", "from", old.String(), "to", admin.String()) } // --- views --- func Admin() address { return admin } func PendingAdmin() address { return pendingAdmin } func MemberCount() int64 { return int64(len(members)) } func TotalWeight() int64 { return totalWeight } func MemberWeight(a address) int64 { i := indexOf(a) if i < 0 { panic("not a member") } return members[i].weight } func BalanceOf(a address) int64 { return ledger.BalanceOf(a.String()) } func UsersTotal() int64 { return ledger.UsersTotal() } func Pulled() int64 { return pulled } func NumPlans() int64 { return int64(len(planIDs)) } func PlanID(i int64) int64 { if i < 0 || i >= int64(len(planIDs)) { panic("plan index out of range") } return planIDs[i] } func Address() address { return self } func Held() int64 { return coinio.HeldAt(self, Denom) } // Receivable reads this realm's accrued, not-yet-pulled provider // balance inside the subscriptions realm — the other side of value // boundary B2. It is deliberately NOT part of Held or of the local // conservation equation. func Receivable() int64 { return subs.BalanceOf(self) } // --- render --- func Render(path string) string { if path != "" { return "unknown page; try the realm root" } out := "# revshare\n\n" out += "A team revenue realm: it is the PROVIDER of its " + "subscription plans, pulls accrued revenue across the realm " + "boundary, and splits it among weighted members into " + "pull-claimable balances.\n\n" out += "- receivable (in subscriptions): " + itoa(Receivable()) + Denom + "\n" out += "- held here: " + itoa(Held()) + Denom + "\n" out += "- earned, unclaimed: " + itoa(ledger.UsersTotal()) + Denom + "\n" out += "- lifetime pulled: " + itoa(pulled) + Denom + "\n" out += "- plans created here: " + itoa(int64(len(planIDs))) + "\n\n" out += "## members\n\n" if len(members) == 0 { out += "(none configured)\n" return out } out += "| member | weight | unclaimed |\n|---|---|---|\n" for _, m := range members { out += "| " + m.addr.String() + " | " + itoa(m.weight) + " | " + itoa(ledger.BalanceOf(m.addr.String())) + Denom + " |\n" } out += "\n(total weight " + itoa(totalWeight) + "; rounding dust " + "goes to the highest-weight member)\n" return out } // --- internals --- func indexOf(a address) int { for i, m := range members { if m.addr == a { return i } } return -1 } func assertNoSend() { if len(unsafe.OriginSend()) != 0 { panic("this function does not accept coins") } } func assertAdmin(caller address) { if caller != admin { panic("admin only") } } func itoa(n int64) string { return strconv.FormatInt(n, 10) }
  6. #6revshare_test.gno
  7. #7package revshare import ( "chain" "chain/banker" "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" subs "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions" ) var ( testAdmin = testutils.TestAddress("rsadmin") alice = testutils.TestAddress("rsalice") bob = testutils.TestAddress("rsbob") subber = testutils.TestAddress("rssubber") subber2 = testutils.TestAddress("rssubber2") mallory = testutils.TestAddress("rsmallory") ) // Harness facts, carried from the sibling suites: init runs with an // empty origin (real deploys capture the signer); realm globals persist // across tests while the banker resets; SetRealm binds to the CALLING // frame only, so arming is inlined at every site; SkipHeights wipes the // origin context. NEW for a two-realm money suite: the banker reset // hits BOTH realms, so held-balance sync must restore revshare's // backing AND the downstream subscriptions realm's backing, or the // first cross-boundary banker send machine-panics the run. func init() { admin = testAdmin sweeper = testAdmin self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/revshare") } func ugnot(n int64) chain.Coins { return chain.NewCoins(chain.NewCoin(Denom, n)) } func coinsOf(a address) int64 { return banker.NewReadonlyBanker().GetCoin(a, Denom) } // conserved asserts the local conservation equation at rest: // everything this realm holds is members' earned-but-unclaimed value. // (Out-of-band surplus tests manage their own expectations.) func conserved(t *testing.T) { t.Helper() if Held() != UsersTotal() { t.Fatalf("conservation violated: Held=%d users=%d", Held(), UsersTotal()) } } // syncHeld restores BOTH realms' banker backing to match their // persistent ledgers after the harness banker reset. func syncHeld() { if want, have := UsersTotal(), coinsOf(Address()); want > have { testing.IssueCoins(Address(), ugnot(want-have)) } if want, have := subs.Liabilities(), coinsOf(subs.Address()); want > have { testing.IssueCoins(subs.Address(), ugnot(want-have)) } } // SetRealm binds to the CALLING frame only, so arming is inlined at // every call site — a helper that arms and returns is silently // ineffective (the sibling suites' documented trap). // subscribe drives the REAL subscriptions realm: who pays exactly // price into planID. Nothing is mocked. func subscribe(cur realm, t *testing.T, who address, planID, price int64) int64 { t.Helper() testing.SetRealm(testing.NewUserRealm(who)) testing.SetOriginCaller(who) testing.SetOriginSend(ugnot(price)) testing.IssueCoins(subs.Address(), ugnot(price)) return subs.Subscribe(cross(cur), planID) } func TestInitialState(t *testing.T) { if Admin() != testAdmin || MemberCount() != 0 || TotalWeight() != 0 || Pulled() != 0 || NumPlans() != 0 { t.Fatal("unexpected initial state") } conserved(t) } // The no-members refusal must fire BEFORE any downstream call. func TestPullNoMembers(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "no members configured to receive revenue", func() { Pull(cross(cur)) }) } func TestMembership(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "admin only", func() { AddMember(cross(cur), alice, 3) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) var zero address uassert.AbortsContains(t, cur, "zero address", func() { AddMember(cross(cur), zero, 1) }) uassert.AbortsContains(t, cur, "weight must be in", func() { AddMember(cross(cur), alice, 0) }) uassert.AbortsContains(t, cur, "weight must be in", func() { AddMember(cross(cur), alice, MaxWeight+1) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) AddMember(cross(cur), alice, 3) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) AddMember(cross(cur), bob, 1) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "already a member", func() { AddMember(cross(cur), alice, 5) }) uassert.AbortsWithMessage(t, cur, "not a member", func() { SetWeight(cross(cur), mallory, 2) }) uassert.AbortsContains(t, cur, "weight must be in", func() { SetWeight(cross(cur), alice, 0) }) if MemberCount() != 2 || TotalWeight() != 4 || MemberWeight(alice) != 3 || MemberWeight(bob) != 1 { t.Fatal("membership state wrong") } } // The provider flow across all three value boundaries, against the // REAL subscriptions realm. func TestProviderFlowAndPull(cur realm, t *testing.T) { syncHeld() // Plan creation is admin-gated HERE (quota griefing) before the // downstream call. testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "admin only", func() { CreatePlan(cross(cur), "team", "d", 1000, 10, 1000) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) planID := CreatePlan(cross(cur), "team", "d", 1000, 10, 1000) if NumPlans() != 1 || PlanID(0) != planID { t.Fatal("plan bookkeeping wrong") } // Downstream truth: the PROVIDER of the plan is this realm. provider, _, price, _, _, status, _ := subs.PlanInfo(planID) if provider != Address() || price != 1000 || status != "active" { t.Fatalf("downstream plan wrong: provider=%v price=%d status=%q", provider, price, status) } // Boundary B1: subscriber pays the subscriptions realm directly. subID := subscribe(cur, t, subber, planID, 1000) if Receivable() != 1000 { t.Fatalf("receivable=%d, want 1000", Receivable()) } if Held() != 0 { t.Fatal("nothing should be held here before Pull") } _ = subID // Boundary B2: anyone may crank the pull (permissionless). heldBefore := Held() testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) got := Pull(cross(cur)) if got != 1000 || Held()-heldBefore != 1000 || Receivable() != 0 { t.Fatalf("pull moved wrong value: got=%d held=%d receivable=%d", got, Held(), Receivable()) } // 3:1 split of 1000 -> 750 / 250, no dust. if BalanceOf(alice) != 750 || BalanceOf(bob) != 250 || BalanceOf(mallory) != 0 { t.Fatalf("split wrong: alice=%d bob=%d mallory=%d", BalanceOf(alice), BalanceOf(bob), BalanceOf(mallory)) } if Pulled() != 1000 || UsersTotal() != 1000 { t.Fatal("lifetime accounting wrong") } conserved(t) } // Replay: the downstream realm holds the replay guard — an empty // receivable aborts there and no local state changes. func TestPullReplay(cur realm, t *testing.T) { syncHeld() usersBefore, pulledBefore := UsersTotal(), Pulled() testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "nothing to claim", func() { Pull(cross(cur)) }) if UsersTotal() != usersBefore || Pulled() != pulledBefore { t.Fatal("aborted pull mutated state") } conserved(t) } // Rounding: dust goes to the highest-weight member, deterministically, // and the sum always equals the pulled amount exactly. func TestRoundingDust(cur realm, t *testing.T) { syncHeld() testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) planID := CreatePlan(cross(cur), "odd", "d", 1001, 10, 1000) subscribe(cur, t, subber2, planID, 1001) a0, b0 := BalanceOf(alice), BalanceOf(bob) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) Pull(cross(cur)) // 1001 at 3:1 -> alice floor 750 + dust 1 = 751, bob 250. if BalanceOf(alice)-a0 != 751 || BalanceOf(bob)-b0 != 250 { t.Fatalf("dust policy wrong: alice+%d bob+%d", BalanceOf(alice)-a0, BalanceOf(bob)-b0) } conserved(t) } // Minimum values: a 1ugnot pull where every floor share is zero still // conserves — the whole amount is dust to the highest weight. func TestMinimumPull(cur realm, t *testing.T) { syncHeld() testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) planID := CreatePlan(cross(cur), "min", "d", 1, 10, 1000) subscribe(cur, t, subber, planID, 1) a0, b0 := BalanceOf(alice), BalanceOf(bob) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) got := Pull(cross(cur)) if got != 1 || BalanceOf(alice)-a0 != 1 || BalanceOf(bob)-b0 != 0 { t.Fatal("minimum pull mishandled") } conserved(t) } // Weight changes shape FUTURE pulls only; earned balances never move. func TestWeightChangeFutureOnly(cur realm, t *testing.T) { syncHeld() a0, b0 := BalanceOf(alice), BalanceOf(bob) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SetWeight(cross(cur), bob, 3) if BalanceOf(alice) != a0 || BalanceOf(bob) != b0 { t.Fatal("weight change touched earned balances") } if TotalWeight() != 6 { t.Fatalf("totalWeight=%d, want 6", TotalWeight()) } testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) planID := CreatePlan(cross(cur), "even", "d", 600, 10, 1000) subscribe(cur, t, subber2, planID, 600) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) Pull(cross(cur)) // 600 at 3:3 -> 300 each. if BalanceOf(alice)-a0 != 300 || BalanceOf(bob)-b0 != 300 { t.Fatal("new weights not applied to new revenue") } conserved(t) } // Boundary B3: members pull their earnings out; strangers get nothing. func TestClaims(cur realm, t *testing.T) { syncHeld() earned := BalanceOf(alice) if earned == 0 { t.Fatal("fixture: alice should have earnings") } before := coinsOf(alice) testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(nil) ClaimAll(cross(cur)) if coinsOf(alice)-before != earned || BalanceOf(alice) != 0 { t.Fatal("claim did not move the earned balance") } bobBefore, bobCoins := BalanceOf(bob), coinsOf(bob) testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(nil) Claim(cross(cur), 100) if BalanceOf(bob) != bobBefore-100 || coinsOf(bob)-bobCoins != 100 { t.Fatal("partial claim wrong") } testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "insufficient balance", func() { Claim(cross(cur), BalanceOf(bob)+1) }) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "nothing to claim", func() { ClaimAll(cross(cur)) }) conserved(t) } // Removal never touches earned value: the ex-member keeps claiming. func TestRemoveMemberKeepsBalance(cur realm, t *testing.T) { syncHeld() bobBal := BalanceOf(bob) if bobBal == 0 { t.Fatal("fixture: bob should still have a balance") } testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) RemoveMember(cross(cur), bob) if MemberCount() != 1 || TotalWeight() != 3 || BalanceOf(bob) != bobBal { t.Fatal("removal corrupted state") } // Future revenue goes entirely to the remaining member. testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) planID := CreatePlan(cross(cur), "solo", "d", 500, 10, 1000) subscribe(cur, t, subber, planID, 500) a0 := BalanceOf(alice) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) Pull(cross(cur)) if BalanceOf(alice)-a0 != 500 || BalanceOf(bob) != bobBal { t.Fatal("post-removal distribution wrong") } // The ex-member's balance remains claimable. before := coinsOf(bob) testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(nil) ClaimAll(cross(cur)) if coinsOf(bob)-before != bobBal { t.Fatal("ex-member could not claim") } conserved(t) } // Plan retirement crosses the boundary admin-gated here, provider-only // downstream. func TestRetirePlan(cur realm, t *testing.T) { syncHeld() planID := PlanID(0) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "admin only", func() { RetirePlan(cross(cur), planID) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) RetirePlan(cross(cur), planID) _, _, _, _, _, status, _ := subs.PlanInfo(planID) if status != "retired" { t.Fatalf("downstream not retired: %q", status) } // Downstream refuses new subscriptions to a retired plan. testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(ugnot(1000)) testing.IssueCoins(subs.Address(), ugnot(1000)) uassert.AbortsContains(t, cur, "plan is not active", func() { subs.Subscribe(cross(cur), planID) }) } // Every entrypoint refuses coin-carrying transactions. func TestStraySend(cur realm, t *testing.T) { for _, f := range []func(){ func() { Pull(cross(cur)) }, func() { ClaimAll(cross(cur)) }, func() { AddMember(cross(cur), mallory, 1) }, func() { CreatePlan(cross(cur), "x", "d", 1000, 10, 1000) }, func() { SweepDenom(cross(cur), Denom) }, } { testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(ugnot(5)) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", f) } } // Out-of-band value: the reserve protects member balances; foreign // denominations sweep in full. func TestSweepReserve(cur realm, t *testing.T) { syncHeld() testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "no surplus to sweep", func() { SweepDenom(cross(cur), Denom) }) testing.IssueCoins(Address(), ugnot(77)) reserveBefore := UsersTotal() got := coinsOf(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SweepDenom(cross(cur), Denom) if coinsOf(testAdmin)-got != 77 || UsersTotal() != reserveBefore { t.Fatal("sweep wrong or touched the reserve") } testing.IssueCoins(Address(), chain.NewCoins(chain.NewCoin("foo", 9))) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SweepDenom(cross(cur), "foo") if banker.NewReadonlyBanker().GetCoin(testAdmin, "foo") != 9 { t.Fatal("foreign denom not swept in full") } testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "sweeper only", func() { SweepDenom(cross(cur), Denom) }) conserved(t) } func TestAdminHandover(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) TransferAdmin(cross(cur), alice) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "only the staged successor may accept", func() { AcceptAdmin(cross(cur)) }) testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(nil) AcceptAdmin(cross(cur)) if Admin() != alice { t.Fatal("authority did not move") } // Former admin locked out of members, plans and sweep. testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "admin only", func() { AddMember(cross(cur), mallory, 1) }) uassert.AbortsWithMessage(t, cur, "sweeper only", func() { SweepDenom(cross(cur), Denom) }) // Hand back. testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(nil) TransferAdmin(cross(cur), testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) AcceptAdmin(cross(cur)) } func TestMemberTableBounds(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) for i := MemberCount(); i < int64(MaxMembers); i++ { testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) AddMember(cross(cur), testutils.TestAddress("rsfill"+string(rune('a'+i))), 1) } testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "member table is full", func() { AddMember(cross(cur), testutils.TestAddress("rsoverflow"), 1) }) } func TestRender(t *testing.T) { out := Render("") if !strings.Contains(out, "receivable") || !strings.Contains(out, "| member |") { t.Fatal("render missing sections") } if Render("x") == Render("") { t.Fatal("unknown path fell through") } }
  8. #8/gno.MemPackageType
  9. #9 MPUserAll

Result log

msg:0,success:true,log:,events:[]