PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
vesting
Namespace
g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Files
3 (gnomod.toml)
Exported functions
37
Module
gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting
gno
0.9

Files (3)

  • gnomod.tomltoml
  • vesting.gnogno
  • vesting_test.gnogno
vesting_test.gnogno
1package vesting23import (4	"strings"5	"testing"67	"chain"8	"chain/banker"9	"chain/runtime"1011	"gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/duebook"12	"gno.land/p/nt/testutils/v0"13	"gno.land/p/nt/uassert/v0"14)1516var (17	testAdmin = testutils.TestAddress("vestadmin")18	funder1   = testutils.TestAddress("vfunder1")19	funder2   = testutils.TestAddress("vfunder2")20	bene1     = testutils.TestAddress("vbene1")21	bene2     = testutils.TestAddress("vbene2")22	mallory   = testutils.TestAddress("vmallory")23	stranger  = testutils.TestAddress("vstranger")24)2526// HARNESS NOTES, carried from the duebook_demo and grants suites and27// re-verified here:28//29//   - testing.SetRealm records the override per FRAME INDEX. A helper that30//     stages a caller and RETURNS leaves the test frame untouched, so the31//     previous staging silently wins and every caller looks like the same32//     account — which hides exactly the authorization bugs these tests33//     exist to find. Every caller below is therefore staged INLINE in the34//     frame that makes the call. The staging helpers (fundAs, callAs) are35//     safe only because they make their call from inside their own frame.36//37//   - Realm globals PERSIST across tests in the test VM, while banker38//     state resets per test. The monetary invariant is therefore asserted39//     in DELTA form after re-seeding carried liabilities (begin()).40//41//   - A panic caught by uassert does NOT roll back realm globals here,42//     whereas on-chain it aborts and reverts the whole transaction. Every43//     abort asserted below happens strictly BEFORE this realm writes any44//     state, so the distinction never bites. The one path where it could —45//     a book.Schedule failure partway through Fund's loop — is unreachable46//     because Fund pre-checks capacity, and is not asserted here.47//48//   - Tests must not assume a fixed starting height, schedule id, or49//     tranche id. They read all three and reason in deltas.50func init() {51	admin = testAdmin52	feeRecipient = testAdmin53	self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting")54}5556// --- harness ---5758type baseline struct{ held, escrow, users, fees int64 }5960func begin() baseline {61	// Disarm any OriginSend a previous test left staged. The envelope is62	// process-global, not per-frame, and a leftover one makes the first63	// non-Fund entrypoint of this test abort in rejectStraySend — which64	// looks exactly like the guard working and is really the harness65	// leaking. Each test opens with an empty envelope, as a fresh tx does.66	testing.SetOriginSend(nil)67	if l := Liabilities(); l > 0 {68		testing.IssueCoins(Address(), ugnot(l))69	}70	return baseline{held: Held(), escrow: EscrowTotal(), users: UsersTotal(), fees: FeesAccrued()}71}7273// conserved asserts ΔH == ΔE + ΔU + ΔF + extraSurplus.74func (b baseline) conserved(t *testing.T, extraSurplus int64) {75	t.Helper()76	dH := Held() - b.held77	dE := EscrowTotal() - b.escrow78	dU := UsersTotal() - b.users79	dF := FeesAccrued() - b.fees80	if dH != dE+dU+dF+extraSurplus {81		t.Fatalf("conservation violated: dH=%d dE=%d dU=%d dF=%d dS=%d", dH, dE, dU, dF, extraSurplus)82	}83	// NOT a test — an identity. Liabilities() is DEFINED as escrowTotal +84	// ledger.Liabilities(), and ledger.Liabilities() as usersTotal +85	// feesAccrued, so both sides are the same expression and this can only86	// fail on integer overflow. Kept deliberately, as an overflow tripwire87	// and as executable documentation of the definition; the real88	// conservation check is the dH comparison above, which is against the89	// banker's actual balance and can genuinely fail.90	if Liabilities() != EscrowTotal()+UsersTotal()+FeesAccrued() {91		t.Fatalf("Liabilities() = %d, want E+U+F", Liabilities())92	}93}9495// pendingSum recomputes the sum of amounts over pending tranches — the96// application invariant E == Σ pending.97func pendingSum() int64 {98	sum := int64(0)99	tranches.Iterate("", "", func(_ string, v any) bool {100		if t := v.(*tranche); t.status == StatusPending {101			sum += t.amount102		}103		return false104	})105	return sum106}107108func pendingCount() int {109	n := 0110	tranches.Iterate("", "", func(_ string, v any) bool {111		if v.(*tranche).status == StatusPending {112			n++113		}114		return false115	})116	return n117}118119// appInvariant asserts BOTH stores agree: escrowTotal tracks the pending120// tranches, and duebook holds exactly one open deferral per pending121// tranche. The second half is the cross-store check that matters most —122// a tranche this realm thinks is pending but duebook has consumed would123// be unreleasable escrow, and the reverse would be a double-spend.124func appInvariant(t *testing.T) {125	t.Helper()126	if got := pendingSum(); got != EscrowTotal() {127		t.Fatalf("app invariant violated: escrowTotal=%d, sum pending=%d", EscrowTotal(), got)128	}129	if got := book.OpenCount(); got != pendingCount() {130		t.Fatalf("store divergence: duebook open=%d, pending tranches=%d", got, pendingCount())131	}132	assertOpenCounts(t)133	// Per-schedule bookkeeping must add up to the schedule total.134	for id := int64(1); id <= NumSchedules(); id++ {135		s := mustSchedule(id)136		pend := int64(0)137		for _, did := range s.trancheIDs {138			if tranches.Get(padU(did)).(*tranche).status == StatusPending {139				pend += tranches.Get(padU(did)).(*tranche).amount140			}141		}142		if pend+s.releasedGross+s.revokedTotal != s.total {143			t.Fatalf("schedule %d: pending=%d + released=%d + revoked=%d != total=%d",144				id, pend, s.releasedGross, s.revokedTotal, s.total)145		}146	}147}148149// assertOpenCounts recomputes every funder's pending-tranche count from the150// tranche records and compares it to the openByFunder counter. This is the151// THIRD cross-store check, and it earns its place: the counter gates Fund,152// so a counter that drifts high locks an honest funder out of a realm that153// has room, and one that drifts low silently defeats the per-funder cap.154// It also asserts the zero-entry removal, because a tree that accumulates a155// dead 0 per address that ever funded anything is an unbounded growth path.156func assertOpenCounts(t *testing.T) {157	t.Helper()158159	want := map[string]int64{}160	tranches.Iterate("", "", func(_ string, v any) bool {161		tr := v.(*tranche)162		if tr.status == StatusPending {163			want[mustSchedule(tr.scheduleID).funder.String()]++164		}165		return false166	})167168	got := map[string]int64{}169	total := int64(0)170	openByFunder.Iterate("", "", func(k string, v any) bool {171		n := v.(int64)172		if n == 0 {173			t.Fatalf("openByFunder holds a zero entry for %s; it must be removed at zero", k)174		}175		got[k] = n176		total += n177		return false178	})179180	if len(got) != len(want) {181		t.Fatalf("openByFunder tracks %d funder(s), recomputed %d", len(got), len(want))182	}183	for k, w := range want {184		if got[k] != w {185			t.Fatalf("funder %s: counter says %d, tranche records say %d", k, got[k], w)186		}187	}188	if total != int64(pendingCount()) {189		t.Fatalf("openByFunder totals %d, pending tranches %d", total, pendingCount())190	}191	if got := FundersWithOpenTranches(); got != len(want) {192		t.Fatalf("FundersWithOpenTranches()=%d, recomputed %d", got, len(want))193	}194}195196func coinsOf(addr address) int64 {197	return banker.NewReadonlyBanker().GetCoin(addr, Denom)198}199200func ugnot(n int64) chain.Coins {201	return chain.NewCoins(chain.NewCoin(Denom, n))202}203204// fundAs performs a chain-faithful funded Fund. Must be called from the205// test function's own frame.206//207// The OriginSend envelope is NOT per-frame: whatever was last staged stays208// visible to every later call in the process. Fund is the one entrypoint209// that accepts coins, so leaving the envelope armed makes the very next210// Release/Revoke abort in rejectStraySend instead of reaching the logic211// under test. Clear it on the way out — a real transaction ends here.212func fundAs(cur realm, from, beneficiary address, total int64, title, memo, spec string, revocable bool) int64 {213	testing.SetRealm(testing.NewUserRealm(from))214	testing.SetOriginCaller(from)215	testing.SetOriginSend(ugnot(total))216	testing.IssueCoins(Address(), ugnot(total))217	id := Fund(cross(cur), beneficiary, title, memo, spec, revocable, MaxFeeBps)218	testing.SetOriginSend(nil)219	return id220}221222// trancheIDsOf returns a schedule's tranche ids as int64, in order.223func trancheIDsOf(id int64) []int64 {224	s := mustSchedule(id)225	out := []int64{}226	for _, did := range s.trancheIDs {227		out = append(out, int64(did))228	}229	return out230}231232// --- tests ---233234func TestInitialConfig(t *testing.T) {235	if Admin() != testAdmin || FeeRecipient() != testAdmin {236		t.Fatal("seeded roles mismatch")237	}238	var zero address239	if PendingAdmin() != zero {240		t.Fatal("pendingAdmin should start zero")241	}242	if Address() == zero {243		t.Fatal("realm address is zero")244	}245	// Reference duebook's own constant, NOT a hardcoded 10000. With the246	// literal, both operands were compile-time constants and the branch was247	// dead — it could not fail even if duebook were redeployed with a lower248	// limit, and duebook.MustNew would then panic at realm init instead,249	// turning a caught test failure into a deploy-time abort.250	if MaxOpenTranches > duebook.MaxOpenLimit {251		t.Fatalf("MaxOpenTranches %d exceeds duebook.MaxOpenLimit %d",252			MaxOpenTranches, duebook.MaxOpenLimit)253	}254	if MaxOpenPerFunder > MaxOpenTranches {255		t.Fatalf("MaxOpenPerFunder %d exceeds the whole-realm cap %d",256			MaxOpenPerFunder, MaxOpenTranches)257	}258	if book.MinDelay() != MinDelayBlocks || book.MaxDelay() != MaxDelayBlocks {259		t.Fatal("book bounds do not match the declared constants")260	}261}262263func TestFundHappyPath(cur realm, t *testing.T) {264	b := begin()265	h := runtime.ChainHeight()266267	id := fundAs(cur, funder1, bene1, 1000, "engineer grant", "a memo", "10:200,20:300,30:500", true)268269	funder, beneficiary, title, total, snapBps, revocable,270		createdAt, numTranches, pending, releasedGross, revokedTotal := ScheduleInfo(id)271	if funder != funder1 || beneficiary != bene1 || title != "engineer grant" {272		t.Fatalf("identity fields wrong: %v %v %q", funder, beneficiary, title)273	}274	if total != 1000 || snapBps != 0 || !revocable {275		t.Fatalf("terms wrong: total=%d bps=%d revocable=%v", total, snapBps, revocable)276	}277	if createdAt != h || numTranches != 3 || pending != 3 || releasedGross != 0 || revokedTotal != 0 {278		t.Fatalf("bookkeeping wrong: createdAt=%d n=%d pending=%d rel=%d rev=%d",279			createdAt, numTranches, pending, releasedGross, revokedTotal)280	}281	if Memo(id) != "a memo" {282		t.Fatal("memo mismatch")283	}284285	// Tranche records carry the right amounts and absolute due heights.286	ids := trancheIDsOf(id)287	wantAmounts := []int64{200, 300, 500}288	wantDue := []int64{h + 10, h + 20, h + 30}289	for i, tid := range ids {290		sID, seq, amount, dueAt, status, settledAt := TrancheInfo(tid)291		if sID != id || seq != int64(i+1) || amount != wantAmounts[i] ||292			dueAt != wantDue[i] || status != StatusPending || settledAt != 0 {293			t.Fatalf("tranche %d wrong: sched=%d seq=%d amount=%d due=%d status=%s settled=%d",294				tid, sID, seq, amount, dueAt, status, settledAt)295		}296	}297298	appInvariant(t)299	b.conserved(t, 0)300}301302func TestFundRejectsEnvelope(cur realm, t *testing.T) {303	b := begin()304305	// Not a direct EOA call (coinio.Receive guard).306	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/attacker"))307	testing.SetOriginSend(ugnot(100))308	uassert.AbortsContains(t, cur, "direct EOA call", func() {309		Fund(cross(cur), bene1, "evil", "", "10:100", false, MaxFeeBps)310	})311312	// No coins at all.313	testing.SetRealm(testing.NewUserRealm(mallory))314	testing.SetOriginCaller(mallory)315	testing.SetOriginSend(nil)316	uassert.AbortsContains(t, cur, "exactly one coin type", func() {317		Fund(cross(cur), bene1, "no coins", "", "10:100", false, MaxFeeBps)318	})319320	// Wrong denomination.321	testing.SetRealm(testing.NewUserRealm(mallory))322	testing.SetOriginCaller(mallory)323	testing.SetOriginSend(chain.NewCoins(chain.NewCoin("foocoin", 100)))324	uassert.AbortsContains(t, cur, "exactly one coin type", func() {325		Fund(cross(cur), bene1, "wrong denom", "", "10:100", false, MaxFeeBps)326	})327328	appInvariant(t)329	b.conserved(t, 0)330}331332func TestFundRejectsTerms(cur realm, t *testing.T) {333	b := begin()334	var zero address335336	// Each case stages inline, because SetRealm is per-frame.337	cases := []struct {338		name, want  string339		beneficiary address340		title, memo string341		spec        string342		sent        int64343	}{344		{"zero beneficiary", "empty beneficiary", zero, "t", "", "10:100", 100},345		// A balance credited to the realm's own address is claimable by346		// nobody and still counted in Liabilities(), so it is stranded347		// AND it shrinks SweepDenom's reach. Released by any third348		// party, so the funder cannot undo it.349		{"self beneficiary", "must not be this realm's own address", self, "t", "", "10:100", 100},350		{"empty title", "title must be", bene1, "", "", "10:100", 100},351		{"long title", "title must be", bene1, strings.Repeat("a", MaxTitleLen+1), "", "10:100", 100},352		{"bad title charset", "letters, digits", bene1, "bad!title", "", "10:100", 100},353		{"long memo", "memo exceeds", bene1, "t", strings.Repeat("m", MaxMemoLen+1), "10:100", 100},354		{"empty spec", "at least one tranche", bene1, "t", "", "", 100},355		{"long spec", "spec exceeds", bene1, "t", "", strings.Repeat("9", MaxSpecLen+1), 100},356		{"missing colon", "expected", bene1, "t", "", "10-100", 100},357		{"delay not integer", "delay is not an integer", bene1, "t", "", "ten:100", 100},358		{"amount not integer", "amount is not an integer", bene1, "t", "", "10:lots", 100},359		{"delay zero", "outside", bene1, "t", "", "0:100", 100},360		{"delay too large", "outside", bene1, "t", "", "100000001:100", 100},361		{"delays equal", "strictly increasing", bene1, "t", "", "10:50,10:50", 100},362		{"delays decreasing", "strictly increasing", bene1, "t", "", "20:50,10:50", 100},363		{"zero amount", "amount must be positive", bene1, "t", "", "10:0", 100},364		{"negative amount", "amount must be positive", bene1, "t", "", "10:-5", 100},365		{"underfunded", "they must be equal", bene1, "t", "", "10:200", 100},366		{"overfunded", "they must be equal", bene1, "t", "", "10:50", 100},367	}368369	// A cross-call abort is NOT catchable with recover() in the calling370	// realm — that is what uassert.AbortsContains is for. Staging stays in371	// this frame, immediately before each call.372	for _, c := range cases {373		spec := c.spec374		ben := c.beneficiary375		title := c.title376		memo := c.memo377378		testing.SetRealm(testing.NewUserRealm(mallory))379		testing.SetOriginCaller(mallory)380		testing.SetOriginSend(ugnot(c.sent))381		uassert.AbortsContains(t, cur, c.want, func() {382			Fund(cross(cur), ben, title, memo, spec, false, MaxFeeBps)383		}, c.name)384	}385386	// Too many tranches: MaxTranches+1 entries with increasing delays.387	parts := []string{}388	for i := 1; i <= MaxTranches+1; i++ {389		parts = append(parts, itoa(int64(i))+":1")390	}391	testing.SetRealm(testing.NewUserRealm(mallory))392	testing.SetOriginCaller(mallory)393	testing.SetOriginSend(ugnot(int64(MaxTranches + 1)))394	uassert.AbortsContains(t, cur, "at most "+itoa(MaxTranches)+" tranches", func() {395		Fund(cross(cur), bene1, "many", "", strings.Join(parts, ","), false, MaxFeeBps)396	})397398	appInvariant(t)399	b.conserved(t, 0)400}401402func TestReleaseBeforeDueAborts(cur realm, t *testing.T) {403	b := begin()404	id := fundAs(cur, funder1, bene1, 100, "not yet", "", "5:100", false)405	tid := trancheIDsOf(id)[0]406407	testing.SetRealm(testing.NewUserRealm(stranger))408	testing.SetOriginCaller(stranger)409	uassert.AbortsContains(t, cur, "not due yet", func() { Release(cross(cur), tid) })410411	// Nothing moved.412	if BalanceOf(bene1) != 0 {413		t.Fatal("beneficiary credited before the due height")414	}415	appInvariant(t)416	b.conserved(t, 0)417}418419// TestReleaseIsPermissionlessAndPaysTheBeneficiary is the central420// authorization property: a stranger may trigger the release, and doing so421// credits the RECORDED beneficiary, never the caller.422func TestReleaseIsPermissionlessAndPaysTheBeneficiary(cur realm, t *testing.T) {423	b := begin()424	id := fundAs(cur, funder1, bene1, 400, "payable", "", "1:400", false)425	tid := trancheIDsOf(id)[0]426427	beneBefore := BalanceOf(bene1)428	malloryBefore := BalanceOf(mallory)429	testing.SkipHeights(1)430431	// Mallory, an unrelated account, triggers the release.432	testing.SetRealm(testing.NewUserRealm(mallory))433	testing.SetOriginCaller(mallory)434	Release(cross(cur), tid)435436	if got := BalanceOf(bene1) - beneBefore; got != 400 {437		t.Fatalf("beneficiary credited %d, want 400", got)438	}439	if got := BalanceOf(mallory) - malloryBefore; got != 0 {440		t.Fatalf("the caller was credited %d; release must never pay the caller", got)441	}442	_, _, _, _, status, settledAt := TrancheInfo(tid)443	if status != StatusReleased || settledAt == 0 {444		t.Fatalf("tranche not marked released: status=%s settledAt=%d", status, settledAt)445	}446	appInvariant(t)447	b.conserved(t, 0)448}449450// TestDoubleReleaseIsImpossible is the property duebook exists to provide.451// The second attempt must fail because the deferral no longer exists — not452// because this realm re-checked a status flag.453func TestDoubleReleaseIsImpossible(cur realm, t *testing.T) {454	b := begin()455	id := fundAs(cur, funder1, bene1, 700, "once only", "", "1:700", false)456	tid := trancheIDsOf(id)[0]457	testing.SkipHeights(1)458459	testing.SetRealm(testing.NewUserRealm(bene1))460	testing.SetOriginCaller(bene1)461	Release(cross(cur), tid)462	after := BalanceOf(bene1)463464	// Second attempt, same transaction-equivalent frame and a different465	// caller: duebook has consumed the ID and can never return it again.466	testing.SetRealm(testing.NewUserRealm(mallory))467	testing.SetOriginCaller(mallory)468	uassert.AbortsContains(t, cur, "no such open deferral", func() { Release(cross(cur), tid) })469470	// And a third, from the beneficiary.471	testing.SetRealm(testing.NewUserRealm(bene1))472	testing.SetOriginCaller(bene1)473	uassert.AbortsContains(t, cur, "no such open deferral", func() { Release(cross(cur), tid) })474475	if BalanceOf(bene1) != after {476		t.Fatal("a repeat release changed the balance")477	}478	appInvariant(t)479	b.conserved(t, 0)480}481482// TestVestedTrancheCannotBeRevoked is this realm's own check, not483// duebook's: duebook.Cancel would consume a due-but-unclaimed deferral484// happily. Without the refusal a funder could front-run the beneficiary.485func TestVestedTrancheCannotBeRevoked(cur realm, t *testing.T) {486	b := begin()487	id := fundAs(cur, funder1, bene1, 300, "vested now", "", "1:300", true)488	tid := trancheIDsOf(id)[0]489490	testing.SkipHeights(1) // now == dueAt: vested491492	testing.SetRealm(testing.NewUserRealm(funder1))493	testing.SetOriginCaller(funder1)494	uassert.AbortsContains(t, cur, "can never be revoked", func() { Revoke(cross(cur), tid) })495496	// Still releasable to the beneficiary afterwards.497	before := BalanceOf(bene1)498	testing.SetRealm(testing.NewUserRealm(stranger))499	testing.SetOriginCaller(stranger)500	Release(cross(cur), tid)501	if got := BalanceOf(bene1) - before; got != 300 {502		t.Fatalf("beneficiary credited %d after a refused revocation, want 300", got)503	}504	appInvariant(t)505	b.conserved(t, 0)506}507508func TestRevokeAuthorization(cur realm, t *testing.T) {509	b := begin()510	rev := fundAs(cur, funder1, bene1, 500, "revocable one", "", "100:500", true)511	irr := fundAs(cur, funder2, bene2, 600, "irrevocable one", "", "100:600", false)512	revTID := trancheIDsOf(rev)[0]513	irrTID := trancheIDsOf(irr)[0]514515	// A stranger may not revoke.516	testing.SetRealm(testing.NewUserRealm(mallory))517	testing.SetOriginCaller(mallory)518	uassert.AbortsContains(t, cur, "only the funder may revoke", func() { Revoke(cross(cur), revTID) })519520	// Nor may the beneficiary.521	testing.SetRealm(testing.NewUserRealm(bene1))522	testing.SetOriginCaller(bene1)523	uassert.AbortsContains(t, cur, "only the funder may revoke", func() { Revoke(cross(cur), revTID) })524525	// Nor the OTHER schedule's funder.526	testing.SetRealm(testing.NewUserRealm(funder2))527	testing.SetOriginCaller(funder2)528	uassert.AbortsContains(t, cur, "only the funder may revoke", func() { Revoke(cross(cur), revTID) })529530	// An irrevocable schedule refuses even its own funder.531	testing.SetRealm(testing.NewUserRealm(funder2))532	testing.SetOriginCaller(funder2)533	uassert.AbortsContains(t, cur, "is irrevocable", func() { Revoke(cross(cur), irrTID) })534535	// RevokeAll carries the same two checks.536	testing.SetRealm(testing.NewUserRealm(mallory))537	testing.SetOriginCaller(mallory)538	uassert.AbortsContains(t, cur, "only the funder may revoke", func() { RevokeAll(cross(cur), rev) })539	testing.SetRealm(testing.NewUserRealm(funder2))540	testing.SetOriginCaller(funder2)541	uassert.AbortsContains(t, cur, "is irrevocable", func() { RevokeAll(cross(cur), irr) })542543	appInvariant(t)544	b.conserved(t, 0)545}546547func TestRevokeRefundsFunderFeeFree(cur realm, t *testing.T) {548	// A non-zero fee must NOT be charged on a revocation.549	testing.SetRealm(testing.NewUserRealm(testAdmin))550	testing.SetOriginCaller(testAdmin)551	SetFeeBps(cross(cur), 500)552553	b := begin()554	id := fundAs(cur, funder1, bene1, 800, "clawback", "", "1000:800", true)555	tid := trancheIDsOf(id)[0]556557	funderBefore := BalanceOf(funder1)558	feesBefore := FeesAccrued()559560	testing.SetRealm(testing.NewUserRealm(funder1))561	testing.SetOriginCaller(funder1)562	Revoke(cross(cur), tid)563564	if got := BalanceOf(funder1) - funderBefore; got != 800 {565		t.Fatalf("funder refunded %d, want the full 800 fee-free", got)566	}567	if FeesAccrued() != feesBefore {568		t.Fatalf("a revocation accrued a fee: %d -> %d", feesBefore, FeesAccrued())569	}570	_, _, _, _, status, settledAt := TrancheInfo(tid)571	if status != StatusRevoked || settledAt == 0 {572		t.Fatalf("tranche not marked revoked: %s %d", status, settledAt)573	}574575	// A revoked tranche can never be released: duebook consumed the ID.576	testing.SetRealm(testing.NewUserRealm(bene1))577	testing.SetOriginCaller(bene1)578	uassert.AbortsContains(t, cur, "no such open deferral", func() { Release(cross(cur), tid) })579580	appInvariant(t)581	b.conserved(t, 0)582583	testing.SetRealm(testing.NewUserRealm(testAdmin))584	testing.SetOriginCaller(testAdmin)585	SetFeeBps(cross(cur), 0)586}587588func TestRevokeAllSkipsVestedAndSettled(cur realm, t *testing.T) {589	b := begin()590	// Tranche 1 due at +1, the rest far out.591	id := fundAs(cur, funder1, bene1, 1000, "mixed", "", "1:100,500:200,600:300,700:400", true)592	ids := trancheIDsOf(id)593594	testing.SkipHeights(1) // tranche 1 vests595596	// Release tranche 1 so it is settled, not merely vested.597	testing.SetRealm(testing.NewUserRealm(stranger))598	testing.SetOriginCaller(stranger)599	Release(cross(cur), ids[0])600601	funderBefore := BalanceOf(funder1)602	beneBefore := BalanceOf(bene1)603	testing.SetRealm(testing.NewUserRealm(funder1))604	testing.SetOriginCaller(funder1)605	RevokeAll(cross(cur), id)606607	// The three unvested tranches came back; the released one did not.608	if got := BalanceOf(funder1) - funderBefore; got != 900 {609		t.Fatalf("funder refunded %d, want 900 (1000 less the released 100)", got)610	}611	// Assert the DELTA, not the absolute. BalanceOf(bene1) is cumulative612	// across the whole suite and is already far above 100 by this point, so613	// a `< 100` check could not fail however badly RevokeAll behaved. What614	// must hold is that RevokeAll moved nothing out of the beneficiary.615	if got := BalanceOf(bene1) - beneBefore; got != 0 {616		t.Fatalf("RevokeAll changed the beneficiary balance by %d, want 0", got)617	}618	_, _, _, _, st0, _ := TrancheInfo(ids[0])619	if st0 != StatusReleased {620		t.Fatalf("released tranche was altered by RevokeAll: %s", st0)621	}622	for _, tid := range ids[1:] {623		_, _, _, _, st, _ := TrancheInfo(tid)624		if st != StatusRevoked {625			t.Fatalf("tranche %d status %s, want revoked", tid, st)626		}627	}628629	// A second RevokeAll has nothing left to do and must say so rather630	// than report a silent success.631	testing.SetRealm(testing.NewUserRealm(funder1))632	testing.SetOriginCaller(funder1)633	uassert.AbortsContains(t, cur, "no revocable tranches", func() { RevokeAll(cross(cur), id) })634635	appInvariant(t)636	b.conserved(t, 0)637}638639func TestWithdrawPaysOutAndDebitsFirst(cur realm, t *testing.T) {640	b := begin()641	id := fundAs(cur, funder1, bene1, 250, "payout test", "", "1:250", false)642	tid := trancheIDsOf(id)[0]643	testing.SkipHeights(1)644645	testing.SetRealm(testing.NewUserRealm(bene1))646	testing.SetOriginCaller(bene1)647	Release(cross(cur), tid)648649	claimable := BalanceOf(bene1)650	if claimable < 250 {651		t.Fatalf("claimable %d, want at least 250", claimable)652	}653	coinsBefore := coinsOf(bene1)654655	// Partial withdrawal.656	testing.SetRealm(testing.NewUserRealm(bene1))657	testing.SetOriginCaller(bene1)658	testing.SetOriginSend(nil)659	Withdraw(cross(cur), 100)660	if got := coinsOf(bene1) - coinsBefore; got != 100 {661		t.Fatalf("received %d coins, want 100", got)662	}663	if BalanceOf(bene1) != claimable-100 {664		t.Fatalf("ledger not debited: %d", BalanceOf(bene1))665	}666667	// Over-withdrawal is refused.668	testing.SetRealm(testing.NewUserRealm(bene1))669	testing.SetOriginCaller(bene1)670	uassert.AbortsContains(t, cur, "insufficient balance", func() { Withdraw(cross(cur), claimable) })671672	// The rest.673	testing.SetRealm(testing.NewUserRealm(bene1))674	testing.SetOriginCaller(bene1)675	WithdrawAll(cross(cur))676	if BalanceOf(bene1) != 0 {677		t.Fatalf("balance after WithdrawAll = %d", BalanceOf(bene1))678	}679680	// Nothing left to withdraw: feeledger returns (0, nil) for an empty681	// account, so this realm's own guard is what turns that into an abort.682	testing.SetRealm(testing.NewUserRealm(bene1))683	testing.SetOriginCaller(bene1)684	uassert.AbortsContains(t, cur, "nothing to withdraw", func() { WithdrawAll(cross(cur)) })685686	// A stranger with no balance cannot withdraw.687	testing.SetRealm(testing.NewUserRealm(mallory))688	testing.SetOriginCaller(mallory)689	uassert.AbortsContains(t, cur, "insufficient balance", func() { Withdraw(cross(cur), 1) })690691	// Every withdrawal moved H and U by the same amount, so the identity692	// holds with no surplus term: dH == dE + dU + dF.693	appInvariant(t)694	b.conserved(t, 0)695}696697// TestFeeSnapshotAndCeiling covers both halves of the fee race: a fee698// change never reaches an already-funded schedule, and a fee raise699// sequenced ahead of a Fund is refused rather than silently applied.700func TestFeeSnapshotAndCeiling(cur realm, t *testing.T) {701	testing.SetRealm(testing.NewUserRealm(testAdmin))702	testing.SetOriginCaller(testAdmin)703	SetFeeBps(cross(cur), 200)704705	b := begin()706	id := fundAs(cur, funder1, bene1, 1000, "snapshotted", "", "1:1000", false)707	_, _, _, _, snapBps, _, _, _, _, _, _ := ScheduleInfo(id)708	if snapBps != 200 {709		t.Fatalf("snapshot bps = %d, want 200", snapBps)710	}711712	// Admin raises the fee AFTER funding; the schedule keeps 200.713	testing.SetRealm(testing.NewUserRealm(testAdmin))714	testing.SetOriginCaller(testAdmin)715	SetFeeBps(cross(cur), 1000)716717	tid := trancheIDsOf(id)[0]718	testing.SkipHeights(1)719	beneBefore := BalanceOf(bene1)720	feesBefore := FeesAccrued()721722	testing.SetRealm(testing.NewUserRealm(stranger))723	testing.SetOriginCaller(stranger)724	Release(cross(cur), tid)725726	// 200 bps of 1000 == 20, charged at the SNAPSHOT, not the live 1000 bps.727	if got := FeesAccrued() - feesBefore; got != 20 {728		t.Fatalf("fee charged = %d, want 20 (the snapshot)", got)729	}730	if got := BalanceOf(bene1) - beneBefore; got != 980 {731		t.Fatalf("beneficiary credited %d, want 980", got)732	}733734	// The creation-time race: the live fee is now 1000, so a caller who735	// signs for at most 200 must be refused.736	testing.SetRealm(testing.NewUserRealm(funder1))737	testing.SetOriginCaller(funder1)738	testing.SetOriginSend(ugnot(100))739	uassert.AbortsContains(t, cur, "exceeds the caller's maximum", func() {740		Fund(cross(cur), bene1, "raced", "", "10:100", false, 200)741	})742	testing.SetOriginSend(nil) // disarm; the envelope is process-global743744	appInvariant(t)745	b.conserved(t, 0)746747	testing.SetRealm(testing.NewUserRealm(testAdmin))748	testing.SetOriginCaller(testAdmin)749	SetFeeBps(cross(cur), 0)750}751752func TestFeeWithdrawalAuthorization(cur realm, t *testing.T) {753	testing.SetRealm(testing.NewUserRealm(testAdmin))754	testing.SetOriginCaller(testAdmin)755	SetFeeBps(cross(cur), 1000)756757	b := begin()758	id := fundAs(cur, funder1, bene1, 1000, "fees", "", "1:1000", false)759	tid := trancheIDsOf(id)[0]760	testing.SkipHeights(1)761	testing.SetRealm(testing.NewUserRealm(stranger))762	testing.SetOriginCaller(stranger)763	Release(cross(cur), tid)764765	if FeesAccrued() == 0 {766		t.Fatal("no fee accrued at 1000 bps")767	}768769	// A non-recipient cannot withdraw fees or sweep.770	testing.SetRealm(testing.NewUserRealm(mallory))771	testing.SetOriginCaller(mallory)772	uassert.AbortsContains(t, cur, "only the fee recipient", func() { WithdrawFees(cross(cur)) })773	testing.SetRealm(testing.NewUserRealm(mallory))774	testing.SetOriginCaller(mallory)775	uassert.AbortsContains(t, cur, "only the fee recipient", func() { SweepDenom(cross(cur), Denom) })776777	pot := FeesAccrued()778	coinsBefore := coinsOf(testAdmin)779	testing.SetRealm(testing.NewUserRealm(testAdmin))780	testing.SetOriginCaller(testAdmin)781	WithdrawFees(cross(cur))782	if got := coinsOf(testAdmin) - coinsBefore; got != pot {783		t.Fatalf("fee recipient received %d, want %d", got, pot)784	}785	if FeesAccrued() != 0 {786		t.Fatalf("fee pot not drained: %d", FeesAccrued())787	}788789	// Draining twice is refused.790	testing.SetRealm(testing.NewUserRealm(testAdmin))791	testing.SetOriginCaller(testAdmin)792	uassert.AbortsContains(t, cur, "no fees accrued", func() { WithdrawFees(cross(cur)) })793794	appInvariant(t)795	// The withdrawal took 120 out of H *and* out of F, so both sides of the796	// identity moved together and there is no surplus term. dF is only -20797	// because 20 of that 120 was accrued by an earlier test and is inside798	// the baseline: F went 20 -> 120 -> 0. Subtracting `pot` here would be799	// double-counting the very transfer dH already records.800	b.conserved(t, 0)801802	testing.SetRealm(testing.NewUserRealm(testAdmin))803	testing.SetOriginCaller(testAdmin)804	SetFeeBps(cross(cur), 0)805}806807// TestSetFeeRecipientSettlesTheOutgoingRecipient is audit finding Y2: the808// fee pot is a single scalar with no per-recipient accounting, so a bare809// re-point would hand every basis point earned under the old recipient to810// the new one at the admin's sole discretion.811func TestSetFeeRecipientSettlesTheOutgoingRecipient(cur realm, t *testing.T) {812	testing.SetRealm(testing.NewUserRealm(testAdmin))813	testing.SetOriginCaller(testAdmin)814	SetFeeBps(cross(cur), 1000)815816	b := begin()817	id := fundAs(cur, funder1, bene1, 1000, "settle", "", "1:1000", false)818	tid := trancheIDsOf(id)[0]819	testing.SkipHeights(1)820	testing.SetRealm(testing.NewUserRealm(stranger))821	testing.SetOriginCaller(stranger)822	Release(cross(cur), tid)823824	pot := FeesAccrued()825	if pot == 0 {826		t.Fatal("no fee accrued at 1000 bps")827	}828	oldRecipient := FeeRecipient()829	oldClaimBefore := BalanceOf(oldRecipient)830	newClaimBefore := BalanceOf(bene2)831832	testing.SetRealm(testing.NewUserRealm(testAdmin))833	testing.SetOriginCaller(testAdmin)834	SetFeeRecipient(cross(cur), bene2)835836	if FeeRecipient() != bene2 {837		t.Fatalf("recipient is %s, want %s", FeeRecipient(), bene2)838	}839	// The pot moved to the OUTGOING recipient's claimable balance...840	if got := BalanceOf(oldRecipient) - oldClaimBefore; got != pot {841		t.Fatalf("outgoing recipient credited %d, want the whole pot %d", got, pot)842	}843	// ...and F is now empty, so the successor inherits the role, not the844	// earnings. This is the assertion the finding is about.845	if FeesAccrued() != 0 {846		t.Fatalf("fee pot still holds %d after re-pointing", FeesAccrued())847	}848	if got := BalanceOf(bene2) - newClaimBefore; got != 0 {849		t.Fatalf("successor was credited %d of the predecessor's earnings, want 0", got)850	}851	// The successor has no pot to drain.852	testing.SetRealm(testing.NewUserRealm(bene2))853	testing.SetOriginCaller(bene2)854	uassert.AbortsContains(t, cur, "no fees accrued", func() { WithdrawFees(cross(cur)) })855856	// No coins moved: F simply became U. Conservation is untouched.857	appInvariant(t)858	b.conserved(t, 0)859860	// A re-point to the SAME address is a no-op and must not churn the pot.861	testing.SetRealm(testing.NewUserRealm(testAdmin))862	testing.SetOriginCaller(testAdmin)863	SetFeeRecipient(cross(cur), bene2)864	if FeesAccrued() != 0 {865		t.Fatal("self re-point disturbed the pot")866	}867868	// Restore the suite's baseline roles and fee.869	testing.SetRealm(testing.NewUserRealm(testAdmin))870	testing.SetOriginCaller(testAdmin)871	SetFeeRecipient(cross(cur), testAdmin)872	testing.SetRealm(testing.NewUserRealm(testAdmin))873	testing.SetOriginCaller(testAdmin)874	SetFeeBps(cross(cur), 0)875}876877// TestPerFunderCapIsEnforcedAndFreed is audit finding Y1: MaxOpenTranches is878// a whole-realm cap, and without a per-funder bound one account could fill879// it for ~nothing and then reclaim every coin fee-free with RevokeAll.880//881// It also closes the coverage gap the audit named: before this, NO test882// exercised a capacity check at all. The whole-realm cap itself stays883// untested on purpose — reaching it needs 5000 tranches, and the per-funder884// cap is now the binding one for any single account, which is the point.885func TestPerFunderCapIsEnforcedAndFreed(cur realm, t *testing.T) {886	b := begin()887888	// onesSpec builds n tranches of 1ugnot with strictly increasing delays.889	// n == MaxTranches is the largest schedule a single Fund can create.890	onesSpec := func(n int64) string {891		parts := []string{}892		for i := int64(1); i <= n; i++ {893			parts = append(parts, itoa(i)+":1")894		}895		return strings.Join(parts, ",")896	}897898	// Realm globals persist across tests, so funder2 may already hold pending899	// tranches and the headroom need not divide evenly by MaxTranches. Fill900	// with full schedules and top off with the remainder rather than assert an901	// arithmetic coincidence: an earlier draft asserted room%MaxTranches==0 and902	// failed at room=255.903	start := OpenTranchesOfFunder(funder2)904	room := int64(MaxOpenPerFunder) - start905	if room <= 0 {906		t.Fatalf("funder2 starts at %d pending, already at the cap %d", start, MaxOpenPerFunder)907	}908909	ids := []int64{}910	sizes := []int64{}911	for left := room; left > 0; {912		n := int64(MaxTranches)913		if left < n {914			n = left915		}916		ids = append(ids, fundAs(cur, funder2, bene2, n, "fill", "", onesSpec(n), true))917		sizes = append(sizes, n)918		left -= n919	}920	if got := OpenTranchesOfFunder(funder2); got != int64(MaxOpenPerFunder) {921		t.Fatalf("funder2 holds %d pending, want the cap %d", got, MaxOpenPerFunder)922	}923924	// One more tranche — a single one — must be refused.925	testing.SetRealm(testing.NewUserRealm(funder2))926	testing.SetOriginCaller(funder2)927	testing.SetOriginSend(ugnot(1))928	testing.IssueCoins(Address(), ugnot(1))929	uassert.AbortsContains(t, cur, "funder tranche capacity reached", func() {930		Fund(cross(cur), bene2, "one too many", "", "1:1", false, MaxFeeBps)931	})932	testing.SetOriginSend(nil)933934	// The 1ugnot issued for that rejected Fund never entered the escrow, so935	// it is surplus now. Account for it explicitly rather than let it hide936	// inside a tolerance.937	surplus := int64(1)938939	// The cap is PER FUNDER, not global: a different funder is unaffected940	// even though funder2 is at its own ceiling.941	otherID := fundAs(cur, funder1, bene1, 5, "unaffected", "", "1:5", true)942	if OpenTranchesOfFunder(funder1) < 1 {943		t.Fatal("funder1 was blocked by funder2's cap")944	}945946	// Releasing or revoking frees the slot again. Revoke one whole schedule947	// and confirm funder2 gets back exactly that schedule's worth of headroom.948	testing.SetRealm(testing.NewUserRealm(funder2))949	testing.SetOriginCaller(funder2)950	RevokeAll(cross(cur), ids[0])951	if got := OpenTranchesOfFunder(funder2); got != int64(MaxOpenPerFunder)-sizes[0] {952		t.Fatalf("after RevokeAll funder2 holds %d, want %d",953			got, int64(MaxOpenPerFunder)-sizes[0])954	}955	// ...and can fund again.956	ids[0] = fundAs(cur, funder2, bene2, sizes[0], "refill", "", onesSpec(sizes[0]), true)957	if got := OpenTranchesOfFunder(funder2); got != int64(MaxOpenPerFunder) {958		t.Fatalf("after refunding funder2 holds %d, want the cap %d", got, MaxOpenPerFunder)959	}960961	// Clean up: return funder2 to its starting count so later tests are not962	// funding against a full account, and drop the counter entries to prove963	// the zero-removal path runs.964	for _, id := range ids {965		testing.SetRealm(testing.NewUserRealm(funder2))966		testing.SetOriginCaller(funder2)967		RevokeAll(cross(cur), id)968	}969	testing.SetRealm(testing.NewUserRealm(funder1))970	testing.SetOriginCaller(funder1)971	RevokeAll(cross(cur), otherID)972	if got := OpenTranchesOfFunder(funder2); got != start {973		t.Fatalf("funder2 left with %d pending, want the starting %d", got, start)974	}975976	appInvariant(t)977	b.conserved(t, surplus)978}979980// TestSweepCannotTouchEscrow is the reserve property: surplus is981// sweepable, escrowed tranches and claimable balances are not.982func TestSweepCannotTouchEscrow(cur realm, t *testing.T) {983	b := begin()984	fundAs(cur, funder1, bene1, 900, "reserved", "", "1000:900", false)985986	// Force-send 77 ugnot out of band, then sweep. Only the 77 may move.987	testing.IssueCoins(Address(), ugnot(77))988	if got := Surplus(); got != 77 {989		t.Fatalf("surplus = %d, want 77", got)990	}991	liabBefore := Liabilities()992	coinsBefore := coinsOf(testAdmin)993994	testing.SetRealm(testing.NewUserRealm(testAdmin))995	testing.SetOriginCaller(testAdmin)996	SweepDenom(cross(cur), Denom)997998	if got := coinsOf(testAdmin) - coinsBefore; got != 77 {999		t.Fatalf("swept %d, want exactly the 77 surplus", got)1000	}1001	if Liabilities() != liabBefore {1002		t.Fatalf("sweep changed liabilities: %d -> %d", liabBefore, Liabilities())1003	}1004	if Held() < Liabilities() {1005		t.Fatalf("sweep broke solvency: held=%d liabilities=%d", Held(), Liabilities())1006	}10071008	// With no surplus left, sweeping aborts rather than touching reserves.1009	testing.SetRealm(testing.NewUserRealm(testAdmin))1010	testing.SetOriginCaller(testAdmin)1011	uassert.AbortsContains(t, cur, "no surplus to sweep", func() { SweepDenom(cross(cur), Denom) })10121013	appInvariant(t)1014	b.conserved(t, 0)1015}10161017func TestAdminHandoverIsTwoStep(cur realm, t *testing.T) {1018	var zero address10191020	// Only the admin may propose.1021	testing.SetRealm(testing.NewUserRealm(mallory))1022	testing.SetOriginCaller(mallory)1023	uassert.AbortsContains(t, cur, "admin only", func() { ProposeAdmin(cross(cur), mallory) })10241025	testing.SetRealm(testing.NewUserRealm(testAdmin))1026	testing.SetOriginCaller(testAdmin)1027	ProposeAdmin(cross(cur), funder2)1028	if PendingAdmin() != funder2 {1029		t.Fatal("proposal not recorded")1030	}1031	if Admin() != testAdmin {1032		t.Fatal("a proposal must not transfer the role on its own")1033	}10341035	// Only the proposed account may accept.1036	testing.SetRealm(testing.NewUserRealm(mallory))1037	testing.SetOriginCaller(mallory)1038	uassert.AbortsContains(t, cur, "only the proposed admin", func() { AcceptAdmin(cross(cur)) })10391040	testing.SetRealm(testing.NewUserRealm(funder2))1041	testing.SetOriginCaller(funder2)1042	AcceptAdmin(cross(cur))1043	if Admin() != funder2 || PendingAdmin() != zero {1044		t.Fatalf("handover incomplete: admin=%v pending=%v", Admin(), PendingAdmin())1045	}10461047	// Accepting again is refused (the proposal was cleared).1048	testing.SetRealm(testing.NewUserRealm(funder2))1049	testing.SetOriginCaller(funder2)1050	uassert.AbortsContains(t, cur, "only the proposed admin", func() { AcceptAdmin(cross(cur)) })10511052	// The old admin has lost the role.1053	testing.SetRealm(testing.NewUserRealm(testAdmin))1054	testing.SetOriginCaller(testAdmin)1055	uassert.AbortsContains(t, cur, "admin only", func() { SetFeeBps(cross(cur), 1) })10561057	// Hand it back so later tests keep their seeded admin.1058	testing.SetRealm(testing.NewUserRealm(funder2))1059	testing.SetOriginCaller(funder2)1060	ProposeAdmin(cross(cur), testAdmin)1061	testing.SetRealm(testing.NewUserRealm(testAdmin))1062	testing.SetOriginCaller(testAdmin)1063	AcceptAdmin(cross(cur))1064	if Admin() != testAdmin {1065		t.Fatal("failed to restore the seeded admin")1066	}1067}10681069func TestSetFeeBounds(cur realm, t *testing.T) {1070	testing.SetRealm(testing.NewUserRealm(testAdmin))1071	testing.SetOriginCaller(testAdmin)1072	uassert.AbortsContains(t, cur, "out of range", func() { SetFeeBps(cross(cur), MaxFeeBps+1) })1073	testing.SetRealm(testing.NewUserRealm(testAdmin))1074	testing.SetOriginCaller(testAdmin)1075	uassert.AbortsContains(t, cur, "out of range", func() { SetFeeBps(cross(cur), -1) })1076	testing.SetRealm(testing.NewUserRealm(testAdmin))1077	testing.SetOriginCaller(testAdmin)1078	uassert.AbortsContains(t, cur, "empty fee recipient", func() {1079		var zero address1080		SetFeeRecipient(cross(cur), zero)1081	})1082}10831084// TestStraySendRejected: only Fund accepts value. Every other payable-1085// looking entrypoint must refuse an attached envelope rather than absorb1086// it into surplus.1087func TestStraySendRejected(cur realm, t *testing.T) {1088	b := begin()1089	id := fundAs(cur, funder1, bene1, 100, "stray", "", "1000:100", true)1090	tid := trancheIDsOf(id)[0]10911092	const guard = "does not accept coins"10931094	testing.SetRealm(testing.NewUserRealm(funder1))1095	testing.SetOriginCaller(funder1)1096	testing.SetOriginSend(ugnot(5))1097	uassert.AbortsContains(t, cur, guard, func() { Release(cross(cur), tid) }, "Release")10981099	testing.SetRealm(testing.NewUserRealm(funder1))1100	testing.SetOriginCaller(funder1)1101	testing.SetOriginSend(ugnot(5))1102	uassert.AbortsContains(t, cur, guard, func() { Revoke(cross(cur), tid) }, "Revoke")11031104	testing.SetRealm(testing.NewUserRealm(funder1))1105	testing.SetOriginCaller(funder1)1106	testing.SetOriginSend(ugnot(5))1107	uassert.AbortsContains(t, cur, guard, func() { RevokeAll(cross(cur), id) }, "RevokeAll")11081109	testing.SetRealm(testing.NewUserRealm(funder1))1110	testing.SetOriginCaller(funder1)1111	testing.SetOriginSend(ugnot(5))1112	uassert.AbortsContains(t, cur, guard, func() { WithdrawAll(cross(cur)) }, "WithdrawAll")11131114	testing.SetRealm(testing.NewUserRealm(testAdmin))1115	testing.SetOriginCaller(testAdmin)1116	testing.SetOriginSend(ugnot(5))1117	uassert.AbortsContains(t, cur, guard, func() { SetFeeBps(cross(cur), 0) }, "SetFeeBps")11181119	testing.SetOriginSend(nil)1120	appInvariant(t)1121	b.conserved(t, 0)1122}11231124func TestUnknownIDsAbort(cur realm, t *testing.T) {1125	// The views are non-crossing, and this test file is compiled INTO the1126	// realm, so calling one is a same-realm call and its panic is an1127	// ordinary panic — not an abort. PanicsContains is the matching1128	// assertion; AbortsContains cannot see it and the panic escapes.1129	// Off-chain a reader reaches these through vm/qeval, which does cross1130	// the boundary and so surfaces the same text as an abort.1131	uassert.PanicsContains(t, cur, "unknown schedule id", func() { ScheduleInfo(999999) })1132	uassert.PanicsContains(t, cur, "unknown tranche id", func() { TrancheInfo(999999) })1133	uassert.PanicsContains(t, cur, "must be positive", func() { TrancheInfo(0) })1134	uassert.PanicsContains(t, cur, "must be positive", func() { TrancheInfo(-1) })11351136	testing.SetRealm(testing.NewUserRealm(mallory))1137	testing.SetOriginCaller(mallory)1138	// Release consumes before it looks anything up, so an unknown id is1139	// rejected by duebook and carries duebook's wording, not this realm's1140	// "unknown tranche id". That ordering is the replay guard and is not1141	// worth reversing for a nicer message: reading the tranches tree first1142	// would put a lookup ahead of the consume, which is exactly the shape1143	// the consumer contract forbids.1144	uassert.AbortsContains(t, cur, "no such open deferral", func() { Release(cross(cur), 999999) })1145}11461147func TestVestedOfTracksTheRevocationBoundary(cur realm, t *testing.T) {1148	b := begin()1149	id := fundAs(cur, funder1, bene1, 600, "boundary", "", "1:100,2:200,1000:300", false)11501151	vested, unvested := VestedOf(id)1152	if vested != 0 || unvested != 600 {1153		t.Fatalf("at funding: vested=%d unvested=%d, want 0/600", vested, unvested)1154	}11551156	testing.SkipHeights(1)1157	vested, unvested = VestedOf(id)1158	if vested != 100 || unvested != 500 {1159		t.Fatalf("after 1 block: vested=%d unvested=%d, want 100/500", vested, unvested)1160	}11611162	testing.SkipHeights(1)1163	vested, unvested = VestedOf(id)1164	if vested != 300 || unvested != 300 {1165		t.Fatalf("after 2 blocks: vested=%d unvested=%d, want 300/300", vested, unvested)1166	}11671168	// Releasing does not change the vested figure: vested means "no longer1169	// revocable", whether or not it has been paid out.1170	ids := trancheIDsOf(id)1171	testing.SetRealm(testing.NewUserRealm(stranger))1172	testing.SetOriginCaller(stranger)1173	Release(cross(cur), ids[0])1174	vested, unvested = VestedOf(id)1175	if vested != 300 || unvested != 300 {1176		t.Fatalf("after a release: vested=%d unvested=%d, want 300/300", vested, unvested)1177	}11781179	appInvariant(t)1180	b.conserved(t, 0)1181}11821183func TestViewsAndRenderDoNotPanic(cur realm, t *testing.T) {1184	// feeBps is a realm global and persists across tests, so a test that1185	// asserts a fee-free figure must set the fee it wants rather than1186	// inherit whatever the last fee test left — including the case where1187	// that test failed before its own reset line ran.1188	testing.SetRealm(testing.NewUserRealm(testAdmin))1189	testing.SetOriginCaller(testAdmin)1190	SetFeeBps(cross(cur), 0)11911192	b := begin()1193	id := fundAs(cur, funder1, bene1, 300, "renderable", "a *memo* with _markdown_", "1:100,900:200", true)11941195	if out := TranchesOf(id); !strings.Contains(out, StatusPending) {1196		t.Fatalf("TranchesOf missing status: %q", out)1197	}1198	testing.SkipHeights(1)1199	if out := ReleasableNow(); out == "" {1200		t.Fatal("ReleasableNow empty with a due tranche")1201	}1202	if fee, credited := FeeOn(1000); fee != 0 || credited != 1000 {1203		t.Fatalf("FeeOn at 0 bps = %d/%d", fee, credited)1204	}1205	if OpenTrancheCount() < 1 {1206		t.Fatal("OpenTrancheCount is zero with pending tranches")1207	}1208	if Height() == 0 {1209		t.Fatal("Height() is zero")1210	}12111212	// Assert the STATE-DEPENDENT line, not the heading. "E + U + F + S" is1213	// emitted unconditionally as part of the section title, so matching it1214	// proves only that Render ran — it would keep passing while the realm1215	// rendered "conservation: VIOLATED" underneath.1216	if out := Render(""); !strings.Contains(out, "Vesting") ||1217		!strings.Contains(out, "conservation: OK") {1218		t.Fatalf("overview render wrong: %q", head(out, 400))1219	}1220	if out := Render(""); !strings.Contains(out, "per funder") {1221		t.Fatalf("overview render omits the per-funder cap: %q", head(out, 400))1222	}1223	if out := Render(itoa(id)); !strings.Contains(out, "Schedule #"+itoa(id)) ||1224		!strings.Contains(out, "releasable now") {1225		t.Fatalf("detail render wrong: %q", out)1226	}1227	if out := Render("not-a-number"); !strings.Contains(out, "invalid schedule id") {1228		t.Fatalf("bad path render: %q", out)1229	}1230	if out := Render("999999"); !strings.Contains(out, "unknown schedule id") {1231		t.Fatalf("unknown path render: %q", out)1232	}12331234	appInvariant(t)1235	b.conserved(t, 0)1236}12371238func head(s string, n int) string {1239	if len(s) <= n {1240		return s1241	}1242	return s[:n]1243}1244

Functions

  • AcceptAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})

  • Address() string

  • Admin() string

  • BalanceOf(addr string) int64

  • EscrowTotal() int64

  • FeeBps() int64

  • FeeOn(amount int64) (fee int64, credited int64)

  • FeeRecipient() string

  • FeesAccrued() int64

  • Fund(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, beneficiary string, title string, memo string, spec string, revocable bool, maxFeeBps int64) int64

  • FundersWithOpenTranches() int

  • Height() int64

  • Held() int64

  • Liabilities() int64

  • Memo(id int64) string

  • NumSchedules() int64

  • OpenTrancheCount() int

  • OpenTranchesOfFunder(funder string) int64

  • PendingAdmin() string

  • ProposeAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, next string)

  • ReleasableNow() string

  • Release(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, trancheID int64) string

  • Render(path string) string

  • Revoke(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, trancheID int64) string

  • RevokeAll(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, scheduleID int64) string

  • ScheduleInfo(id int64) (funder string, beneficiary string, title string, total int64, feeBps int64, revocable bool, createdAt int64, numTranches int64, pending int64, releasedGross int64, revokedTotal int64)

  • SetFeeBps(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, bps int64)

  • SetFeeRecipient(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, next string)

  • Surplus() int64

  • SweepDenom(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, denom string) string

  • TrancheInfo(trancheID int64) (scheduleID int64, seq int64, amount int64, dueAt int64, status string, settledAt int64)

  • TranchesOf(id int64) string

  • UsersTotal() int64

  • VestedOf(id int64) (vested int64, unvested int64)

  • Withdraw(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, amount int64) string

  • WithdrawAll(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) string

  • WithdrawFees(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) string

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.