Transaction
106B6898BCB6B3…2282903A8165
Block 351,458 · index 0 · indexed
Summary
- Hash
- 106B6898BCB6B3ACE45AF04F9B61063E8B066736272272961B9F2282903A8165
- Block
- 351,458
- Size
- 30116 bytes
- Gas used
- 40,329,970 / 250,000,000
- Fee
- 250000 ugnot
- Status
- success
Messages
- Attached funds
- 12000000 ugnot
Arguments · 9
- #1upkeep
- #2gnomod.toml
- #3module = "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upkeep" gno = "0.9"
- #4upkeep.gno
- #5// Package upkeep pays people to run the ecosystem's permissionless // valves. The portfolio's realms deliberately expose maintenance // entrypoints that anyone may call — subscriptions.Expire frees a lapsed // subscription slot, timelock_guardian.Execute fires a matured timelocked // action — because no slot's liveness may depend on an interested party // showing up. This realm adds the missing economics: funders finance a // reward pot, and whoever triggers a valve THROUGH this realm is credited // a bounded reward, claimable by pull. // // THE REALM-TO-REALM BOUNDARY, PRECISELY: // // which calls which : upkeep -> subscriptions.Expire(cross, id) // upkeep -> timelock_guardian.Execute(cross, id) // why it is necessary : the reward must be conditioned on the valve // actually firing. Only making the call from // inside this realm ties "the valve fired" and // "the reward is credited" into one atomic // transaction; observing from outside cannot. // caller identity : downstream sees cur.Previous() = THIS realm, // not the poking EOA. Both valves are // permissionless BY DESIGN and use the caller // identity for nothing, so the intermediary // changes no authorization outcome. This realm // is not a deputy for any downstream authority // — it holds none to confuse. // authorization boundary: upstream, anyone may poke (the reward is the // only thing at stake and the pot is the only // source). Downstream, each valve enforces its // own STATE conditions (grace elapsed, delay // matured) exactly as it would for a direct // caller. // ordering : downstream call FIRST, reward accounting // AFTER. A downstream abort therefore reverts // the whole transaction before any pot or // ledger mutation exists. // failure behavior : any downstream panic (not expirable, too // early, already executed, unknown id...) // aborts this transaction. No partial state, // no reward, by VM atomicity — not by cleanup // code. // atomicity assumptions : a Gno transaction is all-or-nothing across // realm boundaries; there is no catch/recover // anywhere on this path (and none may be added // — recovery would break exactly this // guarantee). // value movement : none crosses the boundary. Both valves move // no coins; the poke transaction must carry no // coins (subscriptions' assertNoSend reads the // ORIGIN envelope unconditionally — measured, // not assumed). Rewards move only inside this // realm's ledger, funded by explicit Fund // transactions. // downstream rejection : reward denied automatically — the abort is // the denial. // downstream trust : neither valve trusts nor validates the // caller; both validate state. This realm // symmetrically does not trust the downstream // REPLY beyond "it did not abort". // replay : enforced downstream. A second Expire on the // same subscription aborts ("subscription is // expired"); a second Execute aborts ("action // already executed"). A poker cannot be paid // twice for one valve event. // adversarial callers : an EOA or realm poking with bogus ids, // premature targets, or replays hits a // downstream abort and pays its own gas. The // remaining economic edge — manufacturing // expirable state to farm rewards — differs // per task. sub_expire: every farmed // subscription permanently locks a sub-record // storage deposit of roughly 30,000ugnot at // the observed 100ugnot/byte rate (Expire // flips status; it does not free the record), // which exceeds MaxReward before price and // gas — loss-making at any legal setting. // timelock_execute: a farmed Action record is // small and its deposit can sit below the // cap, so farming resistance there rests on // the admin keeping the setting below the // measured cycle cost. Funders trust the cap // AND the admin's reward policy, not the cap // alone. The pot is a donation either way — // farming can drain it, never third-party // balances. // // Rewards default to 0 per task; the admin sets them within the // compile-time MaxReward. The pot only moves down via successful pokes // and only up via Fund. Conservation: Held == pot + UsersTotal // (+ out-of-band surplus, recoverable above that reserve by SweepDenom). // The ledger's fee cap is 0 — no fee exists anywhere in this realm. package upkeep import ( "chain" "chain/runtime/unsafe" "strconv" "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio" "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger" subs "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions" guardian "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian" ) // Denom is the only asset this realm accepts. const Denom = "ugnot" // Task identifiers — the two supported valves. const ( TaskSubExpire = "sub_expire" TaskTimelockExecute = "timelock_execute" ) // MaxReward is the compile-time ceiling on the per-poke reward: // 20,000ugnot (0.02 GNOT). It bounds what any single poke can extract // from the pot. For sub_expire it also defeats farming outright: one // manufactured expirable subscription permanently locks a sub-record // deposit of ~30,000ugnot at the observed 100ugnot/byte rate — already // above the cap before price and gas. For timelock_execute the farmed // Action record is smaller and its deposit can sit below the cap, so // there the admin's setting, kept below the measured farm-cycle cost, // is what deters farming. Funders trust the cap and the admin's // policy together; the pot they fund is an explicit donation either // way. const MaxReward = int64(20000) var ( admin address // may set rewards, stage 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 pot int64 // funded, not-yet-awarded ugnot rewards = map[string]int64{ TaskSubExpire: 0, TaskTimelockExecute: 0, } ledger = feeledger.MustNew(0) // lifetime counters, rendered for operators pokes int64 funded int64 ) func init() { admin = unsafe.OriginCaller() sweeper = admin self = unsafe.CurrentRealm().Address() } // --- funding --- // Fund adds the attached coins to the reward pot. Anyone may fund; // funding is a donation to ecosystem maintenance and is not refundable. func Fund(cur realm) { funder, amount := coinio.Receive(0, cur, Denom) newPot, ok := checkedAdd(pot, amount) if !ok { panic("pot would overflow") } pot = newPot funded += amount chain.Emit("Funded", "from", funder.String(), "amount", itoa(amount), "pot", itoa(pot)) } // --- the pokes: the realm-to-realm calls --- // PokeExpire triggers subscriptions.Expire(subID) through this realm and // credits the caller the sub_expire reward. The downstream realm decides // whether the subscription is expirable; its abort is the authorization. // The transaction must attach no coins (the downstream realm checks the // origin envelope). func PokeExpire(cur realm, subID int64) { assertNoSend() caller := cur.Previous().Address() reward := requireReward(TaskSubExpire) // Downstream first: an abort here reverts everything below. subs.Expire(cross(cur), subID) award(caller, reward, TaskSubExpire, itoa(subID)) } // PokeExecute triggers timelock_guardian.Execute(actionID) through this // realm and credits the caller the timelock_execute reward. The guardian // decides whether the action is executable; its abort is the // authorization. func PokeExecute(cur realm, actionID string) { assertNoSend() caller := cur.Previous().Address() reward := requireReward(TaskTimelockExecute) guardian.Execute(cross(cur), actionID) award(caller, reward, TaskTimelockExecute, actionID) } // award moves reward from the pot to the caller's claimable balance. // Callers have already established that the pot covers it. func award(caller address, reward int64, task, target string) { pot -= reward ledger.MustDeposit(caller.String(), reward, 0) pokes++ chain.Emit("Poked", "task", task, "target", target, "caller", caller.String(), "reward", itoa(reward), "pot", itoa(pot), ) } // requireReward returns the configured reward for a task, refusing when // the task is unknown, unrewarded, or the pot cannot cover it. Refusal // on an empty pot is deliberate: both valves remain directly callable on // their own realms, so an unrewarded detour through this one is only a // gas trap for the caller. func requireReward(task string) int64 { r, ok := rewards[task] if !ok { panic("unknown task: " + task) } if r <= 0 { panic("no reward configured for " + task) } if pot < r { panic("reward pot cannot cover " + task + ": pot " + itoa(pot) + ", reward " + itoa(r)) } return r } // --- claims --- // Claim sends amount ugnot of the caller's earned balance back 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 back 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 --- // SetReward configures the per-poke reward for a task, bounded by // MaxReward. Admin only. Zero disables the task. func SetReward(cur realm, task string, amount int64) { assertNoSend() assertAdmin(cur.Previous().Address()) if _, ok := rewards[task]; !ok { panic("unknown task: " + task) } if amount < 0 || amount > MaxReward { panic("reward must be in [0, " + itoa(MaxReward) + "]") } old := rewards[task] rewards[task] = amount chain.Emit("RewardChanged", "task", task, "old", itoa(old), "new", itoa(amount)) } // SweepDenom recovers out-of-band coins to the sweeper. For the pot // denom the reserve is pot + earned balances — both structurally // unreachable. Sweeper only. func SweepDenom(cur realm, denom string) { assertNoSend() if cur.Previous().Address() != sweeper { panic("sweeper only") } reserve := int64(0) if denom == Denom { reserve = pot + 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 Pot() int64 { return pot } func RewardFor(task string) int64 { r, ok := rewards[task] if !ok { panic("unknown task: " + task) } return r } func BalanceOf(a address) int64 { return ledger.BalanceOf(a.String()) } func UsersTotal() int64 { return ledger.UsersTotal() } func Pokes() int64 { return pokes } func Funded() int64 { return funded } func Address() address { return self } func Held() int64 { return coinio.HeldAt(self, Denom) } // --- render --- func Render(path string) string { if path != "" { return "unknown page; try the realm root" } out := "# upkeep\n\n" out += "Rewards for running the ecosystem's permissionless valves. " + "Fund the pot; poke a valve through this realm; claim what you " + "earn. A poke succeeds only when the downstream realm accepts " + "the valve call in the same transaction.\n\n" out += "- pot: " + itoa(pot) + Denom + "\n" out += "- earned, unclaimed: " + itoa(ledger.UsersTotal()) + Denom + "\n" out += "- lifetime pokes: " + itoa(pokes) + "\n" out += "- lifetime funding: " + itoa(funded) + Denom + "\n\n" out += "## rewards per poke\n\n" out += "- `" + TaskSubExpire + "` (subscriptions.Expire): " + itoa(rewards[TaskSubExpire]) + Denom + "\n" out += "- `" + TaskTimelockExecute + "` (timelock_guardian.Execute): " + itoa(rewards[TaskTimelockExecute]) + Denom + "\n" out += "\n(cap per poke: " + itoa(MaxReward) + Denom + ", compile-time)\n" return out } // --- internals --- 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 checkedAdd(a, b int64) (int64, bool) { if b > 0 && a > int64(^uint64(0)>>1)-b { return 0, false } return a + b, true } func itoa(n int64) string { return strconv.FormatInt(n, 10) }
- #6upkeep_test.gno
- #7package upkeep import ( "chain" "chain/banker" "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" subs "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions" guardian "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian" ) var ( testAdmin = testutils.TestAddress("ukadmin") funder = testutils.TestAddress("ukfunder") poker = testutils.TestAddress("ukpoker") mallory = testutils.TestAddress("ukmallory") provider = testutils.TestAddress("ukprov") subber = testutils.TestAddress("uksubber") towner = testutils.TestAddress("uktowner") ) // 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 advances // the clock 5s per height AND wipes the origin context. func init() { admin = testAdmin sweeper = testAdmin self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upkeep") } 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 realm's monetary invariant: everything the realm // holds is either unspent pot or earned-but-unclaimed balance. func conserved(t *testing.T) { t.Helper() if Held() != Pot()+UsersTotal() { t.Fatalf("conservation violated: Held=%d pot=%d users=%d", Held(), Pot(), UsersTotal()) } } // syncHeld re-issues the realm's expected holdings. The harness banker // resets between tests while realm globals (pot, ledger) persist, so // each money-touching test starts by making the banker match the books. func syncHeld() { want := Pot() + UsersTotal() have := coinsOf(Address()) if want > have { testing.IssueCoins(Address(), ugnot(want-have)) } } func fundAs(cur realm, from address, amount int64) { testing.SetRealm(testing.NewUserRealm(from)) testing.SetOriginCaller(from) testing.SetOriginSend(ugnot(amount)) testing.IssueCoins(Address(), ugnot(amount)) Fund(cross(cur)) } func adminSet(cur realm, task string, amount int64) { testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SetReward(cross(cur), task, amount) } // expirableSub drives the REAL subscriptions realm to a state where one // subscription is past its grace window, and returns its id. This is // the downstream fixture for the realm-to-realm tests: nothing is // mocked. func expirableSub(cur realm, t *testing.T) int64 { t.Helper() testing.SetRealm(testing.NewUserRealm(provider)) testing.SetOriginCaller(provider) testing.SetOriginSend(nil) planID := subs.CreatePlan(cross(cur), "upkeep-fixture", "d", 1, 10, 1000) testing.SetRealm(testing.NewUserRealm(subber)) testing.SetOriginCaller(subber) testing.SetOriginSend(ugnot(1)) testing.IssueCoins(subs.Address(), ugnot(1)) subID := subs.Subscribe(cross(cur), planID) // paidThrough = h+10; expirable at paidThrough+10. Skip well past. testing.SkipHeights(25) return subID } // maturedAction drives the REAL guardian realm to a matured timelocked // action and returns its id. var targetSeq int func maturedAction(cur realm, t *testing.T) string { t.Helper() targetSeq++ name := "uktarget" + string(rune('a'+targetSeq%26)) testing.SetRealm(testing.NewUserRealm(towner)) testing.SetOriginCaller(towner) testing.SetOriginSend(nil) guardian.RegisterTarget(cross(cur), name, guardian.MinDelayFloor, "") testing.SetRealm(testing.NewUserRealm(towner)) testing.SetOriginCaller(towner) testing.SetOriginSend(nil) id := guardian.Schedule(cross(cur), name, "poke-fixture", guardian.MinDelayFloor) testing.SkipHeights(13) // 65s at 5s/height >= the 60s floor return id } func TestInitialState(t *testing.T) { if Admin() != testAdmin || Pot() != 0 || Pokes() != 0 || Funded() != 0 { t.Fatal("unexpected initial state") } if RewardFor(TaskSubExpire) != 0 || RewardFor(TaskTimelockExecute) != 0 { t.Fatal("rewards must default to 0") } conserved(t) } func TestFundAndRewardConfig(cur realm, t *testing.T) { fundAs(cur, funder, 100000) if Pot() != 100000 || Funded() != 100000 { t.Fatalf("pot=%d funded=%d", Pot(), Funded()) } conserved(t) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "admin only", func() { SetReward(cross(cur), TaskSubExpire, 100) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "reward must be in", func() { SetReward(cross(cur), TaskSubExpire, MaxReward+1) }) uassert.AbortsContains(t, cur, "unknown task", func() { SetReward(cross(cur), "nosuch", 1) }) adminSet(cur, TaskSubExpire, 5000) adminSet(cur, TaskTimelockExecute, 7000) if RewardFor(TaskSubExpire) != 5000 || RewardFor(TaskTimelockExecute) != 7000 { t.Fatal("rewards not set") } } // The realm-to-realm happy path: poke the REAL subscriptions realm. func TestPokeExpire(cur realm, t *testing.T) { syncHeld() subID := expirableSub(cur, t) // Downstream state before: still Active. _, _, _, _, _, _, status := subs.SubInfo(subID) if status != "active" { t.Fatalf("fixture status %q", status) } potBefore, balBefore := Pot(), BalanceOf(poker) testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(nil) PokeExpire(cross(cur), subID) // Downstream state changed by OUR realm's crossing call... _, _, _, _, _, _, status = subs.SubInfo(subID) if status != "expired" { t.Fatalf("downstream not expired: %q", status) } // ...and the reward moved pot -> caller balance atomically. if Pot() != potBefore-5000 || BalanceOf(poker) != balBefore+5000 { t.Fatalf("reward accounting wrong: pot %d->%d bal %d->%d", potBefore, Pot(), balBefore, BalanceOf(poker)) } if Pokes() != 1 { t.Fatalf("pokes=%d", Pokes()) } conserved(t) } // Replay: the downstream realm is the replay guard; a second poke on the // same subscription aborts THERE and no reward is paid HERE. func TestPokeExpireReplay(cur realm, t *testing.T) { syncHeld() potBefore, balBefore := Pot(), BalanceOf(mallory) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "subscription is expired", func() { PokeExpire(cross(cur), 0) // the sub expired in TestPokeExpire }) if Pot() != potBefore || BalanceOf(mallory) != balBefore || Pokes() != 1 { t.Fatal("aborted poke mutated reward state") } conserved(t) } // A premature poke aborts downstream ("not expirable") and pays nothing. func TestPokeExpirePremature(cur realm, t *testing.T) { syncHeld() testing.SetRealm(testing.NewUserRealm(provider)) testing.SetOriginCaller(provider) testing.SetOriginSend(nil) planID := subs.CreatePlan(cross(cur), "fresh", "d", 1, 5000, 1000) testing.SetRealm(testing.NewUserRealm(subber)) testing.SetOriginCaller(subber) testing.SetOriginSend(ugnot(1)) testing.IssueCoins(subs.Address(), ugnot(1)) freshID := subs.Subscribe(cross(cur), planID) potBefore := Pot() testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "not expirable", func() { PokeExpire(cross(cur), freshID) }) uassert.AbortsContains(t, cur, "unknown subscription id", func() { PokeExpire(cross(cur), 99999) }) if Pot() != potBefore { t.Fatal("failed pokes touched the pot") } conserved(t) } // The second realm-to-realm integration: the REAL guardian realm. func TestPokeExecute(cur realm, t *testing.T) { syncHeld() id := maturedAction(cur, t) potBefore, balBefore := Pot(), BalanceOf(poker) testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(nil) PokeExecute(cross(cur), id) if Pot() != potBefore-7000 || BalanceOf(poker) != balBefore+7000 { t.Fatal("timelock reward accounting wrong") } // Replay refused downstream. testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "already executed", func() { PokeExecute(cross(cur), id) }) uassert.AbortsContains(t, cur, "action not found", func() { PokeExecute(cross(cur), "tl-nosuch") }) // Premature: schedule a new one, poke before maturity. testing.SetRealm(testing.NewUserRealm(towner)) testing.SetOriginCaller(towner) testing.SetOriginSend(nil) guardian.RegisterTarget(cross(cur), "ukearly", guardian.MinDelayFloor, "") testing.SetRealm(testing.NewUserRealm(towner)) testing.SetOriginCaller(towner) testing.SetOriginSend(nil) early := guardian.Schedule(cross(cur), "ukearly", "x", guardian.MinDelayFloor) testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "too early", func() { PokeExecute(cross(cur), early) }) conserved(t) } // Pot exhaustion refuses BEFORE the downstream call: no valve fires // unrewarded through this realm. Entering here the pot is deterministic: // 100000 funded minus the 5000 and 7000 awarded above = 88000, so four // MaxReward pokes leave 8000 < MaxReward and the next must refuse. func TestPotExhaustion(cur realm, t *testing.T) { syncHeld() adminSet(cur, TaskSubExpire, MaxReward) // Drain by REAL pokes until the pot no longer covers the reward. for i := 0; Pot() >= MaxReward; i++ { if i >= 10 { t.Fatalf("fixture arithmetic broken: pot=%d after %d pokes", Pot(), i) } id := expirableSub(cur, t) testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(nil) PokeExpire(cross(cur), id) } short := expirableSub(cur, t) potBefore := Pot() testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "reward pot cannot cover", func() { PokeExpire(cross(cur), short) }) // Downstream MUST be untouched by the refused poke. _, _, _, _, _, _, status := subs.SubInfo(short) if status != "active" { t.Fatal("refused poke reached the downstream realm") } if Pot() != potBefore { t.Fatal("refused poke touched the pot") } conserved(t) adminSet(cur, TaskSubExpire, 5000) // restore for the later tests } // The refusal boundary, deterministic: unknown task, zero reward, and // pot-short all refuse before any downstream effect. func TestPokeRefusals(cur realm, t *testing.T) { adminSet(cur, TaskSubExpire, 0) testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "no reward configured", func() { PokeExpire(cross(cur), 0) }) adminSet(cur, TaskSubExpire, 5000) testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(ugnot(5)) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { PokeExpire(cross(cur), 0) }) testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(ugnot(5)) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { PokeExecute(cross(cur), "x") }) } func TestClaims(cur realm, t *testing.T) { syncHeld() earned := BalanceOf(poker) if earned == 0 { t.Fatal("fixture: poker should have earnings") } before := coinsOf(poker) testing.SetRealm(testing.NewUserRealm(poker)) testing.SetOriginCaller(poker) testing.SetOriginSend(nil) ClaimAll(cross(cur)) if coinsOf(poker)-before != earned || BalanceOf(poker) != 0 { t.Fatal("claim did not move the earned balance") } testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "nothing to claim", func() { ClaimAll(cross(cur)) }) uassert.AbortsContains(t, cur, "insufficient balance", func() { Claim(cross(cur), 1) }) conserved(t) } 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 := Pot() + UsersTotal() got := coinsOf(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SweepDenom(cross(cur), Denom) if coinsOf(testAdmin)-got != 77 { t.Fatal("sweep amount wrong") } if Pot()+UsersTotal() != reserveBefore { t.Fatal("sweep touched the reserve") } 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), funder) 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(funder)) testing.SetOriginCaller(funder) testing.SetOriginSend(nil) AcceptAdmin(cross(cur)) if Admin() != funder { t.Fatal("authority did not move") } // Former admin locked out; sweeper moved with admin. testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "admin only", func() { SetReward(cross(cur), TaskSubExpire, 1) }) uassert.AbortsWithMessage(t, cur, "sweeper only", func() { SweepDenom(cross(cur), Denom) }) // Hand back. testing.SetRealm(testing.NewUserRealm(funder)) testing.SetOriginCaller(funder) testing.SetOriginSend(nil) TransferAdmin(cross(cur), testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) AcceptAdmin(cross(cur)) } func TestRender(t *testing.T) { out := Render("") if !strings.Contains(out, "sub_expire") || !strings.Contains(out, "timelock_execute") { t.Fatal("render missing task lines") } if Render("x") == Render("") { t.Fatal("unknown path fell through") } }
- #8/gno.MemPackageType
- #9 MPUserAll
Result log
msg:0,success:true,log:,events:[]