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.gnogno
1// Realm vesting escrows native GNOT for a named beneficiary and releases2// it in DISCRETE, INDIVIDUALLY DATED TRANCHES — each payable exactly once,3// at or after its own due height.4//5// # DISCRETE, NOT LINEAR — read this first6//7// Everywhere else in this ecosystem "vesting" means CONTINUOUS LINEAR8// ACCRUAL: a formula of the form amount * elapsed / duration, evaluated9// whenever someone asks. This realm does no such arithmetic. A funder10// states an explicit list of (delay, amount) pairs at funding time, and11// nothing accrues between them: a tranche is worth zero until its due12// height and its full amount from that height on.13//14// That is a deliberate choice, not a simplification:15//16//   - It is the shape real schedules actually have — cliffs, quarterly17//     tranches, milestone payments, a lump sum on a date.18//   - It has no `amount * elapsed` term, so the int64 overflow class that19//     has already shipped a NEGATIVE vested amount in two independent20//     ecosystem packages cannot occur here: that MULTIPLICATIVE class is21//     designed out rather than guarded against. The additive sums that do22//     exist are guarded, not designed out — Fund checks the tranche total23//     and the escrow total with checkedAdd, each with its own panic,24//     before any state changes. Both halves are true and neither replaces25//     the other.26//   - It maps exactly onto duebook, whose exactly-once Claim is what makes27//     a double release structurally impossible rather than merely checked.28//29// There is no default curve to be surprised by: every tranche is stated30// explicitly at creation, or the schedule does not exist.31//32// # COMPOSITION33//34// This realm owns only the schedule state machine. Everything else is35// delegated to primitives already audited and deployed:36//37//	duebook   — the entire schedule: due heights, and exactly-once release38//	coinio    — coin custody: escrow-in, payout, surplus sweep39//	feeledger — claimable balances and the fee pot40//	sanitize  — funder-supplied free text, before it reaches markdown41//42// It is duebook's FIRST PRODUCTIVE CONSUMER. duebook_demo is the43// falsification experiment and holds no coins by design; this realm is the44// first to put value behind the exactly-once claim.45//46// # LIFECYCLE47//48//	Fund (EOA + -send)      : one transaction creates the schedule, escrows49//	                          the exact total, and registers every tranche50//	                          in duebook. There is no draft state and no51//	                          half-funded schedule.52//	Release (ANYONE)        : once a tranche is due, anyone may release it.53//	                          The value is credited to the RECORDED54//	                          BENEFICIARY, never to the caller.55//	Revoke (funder only)    : returns a NOT-YET-DUE tranche to the funder,56//	                          and only if the schedule was created57//	                          revocable. A due tranche can never be revoked.58//	Withdraw / WithdrawAll  : pays out the caller's own claimable balance.59//60// # WHY NOTHING CAN BE TRAPPED, AND WHY THERE IS NO EXPIRY61//62// Tranches are scheduled with duebook's ttl == 0, so they NEVER expire.63// Vested value cannot be lost by a beneficiary who is slow, offline, or64// gone. The sibling realms in this portfolio use a permissionless expiry65// valve to guarantee escrow is never trapped; here the valve is different66// and stronger: Release is PERMISSIONLESS, so every tranche becomes67// claimable-by-anyone the moment it is due, and the schedule drains itself68// without depending on any particular actor staying alive. Combined with69// "a due tranche can never be revoked", every ugnot in escrow has exactly70// one reachable destination and a height after which nobody can block it.71//72// # VESTED MEANS IRREVOCABLE, WITH NO RACE73//74// Revoke refuses any tranche whose due height has arrived, even though75// duebook's Cancel would happily consume it. THAT CHECK IS THIS REALM'S,76// not duebook's. Without it a funder could watch for the due height and77// front-run the beneficiary's Release to claw back value already earned.78// Likewise, `revocable` is fixed at funding and has no setter: an79// irrevocable schedule cannot be made revocable later, which is the only80// thing that makes the promise worth anything.81//82// # AUTHORIZATION83//84// Every identity is derived from the crossing entrypoint's85// cur.Previous().Address(). No function takes a caller identity as a86// parameter, so acting as another account is impossible by construction.87// Release is permissionless *because* it pays the recorded beneficiary:88// the caller's identity is not used to route value, so there is nothing89// for a stranger to gain.90//91// # HOW IT WIRES DUEBOOK CORRECTLY (duebook's five-point consumer contract)92//93//  1. THE CLOCK IS runtime.ChainHeight(), never a call argument. A realm94//     that lets a caller choose `now` has no delay at all.95//  2. HEIGHTS ARE THE ONLY UNIT. No timestamp appears anywhere.96//  3. THE *Book IS NEVER EXPORTED. It is an unexported package var and is97//     never returned across a realm boundary; handing it out would hand out98//     the right to schedule, cancel and claim.99//  4. THE ACTOR IS AUTHORIZED from cur.Previous().Address(); the duebook100//     owner recorded at Schedule is the funder, and duebook checks Cancel101//     against that recorded value, never against an argument.102//  5. THE EFFECT HAPPENS AFTER A SUCCESSFUL Claim, IN THE SAME103//     TRANSACTION.104//105// The deferral PAYLOAD is a human-readable label for events and rendering.106// THIS REALM NEVER PARSES IT. Every authoritative fact about a tranche —107// its amount, its schedule, its beneficiary — comes from this realm's own108// `tranches` tree, keyed by the deferral ID that duebook guarantees is109// never reused.110//111// # MONETARY INVARIANT (conservation)112//113// Let H be ugnot held at this realm's address, E = escrowTotal (the sum of114// amounts over tranches still pending), U the ledger's claimable balances,115// F the fee pot, and S >= 0 out-of-band surplus:116//117//	H == E + U + F + S118//119// Fund raises H and E together (coinio.Receive is the receipt-guaranteed120// shape). Release moves one tranche amount from E into U+F, with feeledger121// guaranteeing credited + fee == amount. Revoke moves a tranche amount from122// E into U, fee-free. Withdrawals debit the ledger BEFORE coinio.Payout123// moves the identical amount out (checks-effects-interactions). Any panic124// aborts the whole transaction. This realm never issues or removes coins.125// Surplus is recoverable only via SweepDenom, which reserves Liabilities().126//127// # ONLY GNOT128//129// Fund rejects any envelope that is not exactly one positive ugnot coin.130// Foreign denominations sit in surplus and are sweepable.131package vesting132133import (134	"chain"135	"chain/runtime"136	"chain/runtime/unsafe"137	"strconv"138	"strings"139140	"gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio"141	"gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/duebook"142	"gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger"143	"gno.land/p/nt/avl/v0"144	"gno.land/p/nt/markdown/sanitize/v0"145)146147// Denom is the only asset this realm escrows.148const Denom = "ugnot"149150// MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%.151const MaxFeeBps = int64(1000)152153// Tranche status values. A tranche is created pending and reaches exactly154// one terminal state; both terminals are frozen.155const (156	StatusPending  = "pending"157	StatusReleased = "released"158	StatusRevoked  = "revoked"159)160161// Bounds. All are compile-time constants: none is admin-mutable, so a162// schedule's shape cannot be changed under its funder or beneficiary.163const (164	// MinDelayBlocks is the floor on how far ahead the FIRST tranche may165	// fall. One block is enough to prove the delay is enforced while166	// keeping the realm exercisable on a live chain.167	MinDelayBlocks = int64(1)168169	// MaxDelayBlocks caps the furthest tranche at roughly 10.8 years at170	// pearl-1's ~3.42s blocks. It also bounds now+delay far inside int64.171	MaxDelayBlocks = int64(100_000_000)172173	// MaxTranches bounds one schedule. It bounds the Fund transaction's174	// work, the RevokeAll loop, and the schedule detail render.175	MaxTranches = 64176177	// MaxOpenTranches caps pending tranches across EVERY schedule at once178	// — it is a whole-realm limit, not a per-schedule one, because it is179	// duebook's open-deferral cap. Fund fails when a new schedule would180	// cross it. Must be <= duebook.MaxOpenLimit.181	MaxOpenTranches = 5000182183	// MaxOpenPerFunder bounds how many tranches ONE funder may hold184	// pending at once, so that MaxOpenTranches cannot be consumed by a185	// single account.186	//187	// Without this bound the whole-realm cap is a denial-of-service188	// surface that costs nothing to use: 79 Fund calls of 64 one-ugnot189	// tranches fill the realm, Fund then aborts for everybody, and the190	// attacker recovers 100% of the escrow fee-free with RevokeAll. Two191	// deliberate choices elsewhere remove every recovery path — ttl is192	// always 0, so duebook's permissionless Expire valve never opens, and193	// MaxOpenTranches is a compile-time constant with no admin setter. The194	// denial would therefore be total and permanent, which is out of all195	// proportion to its cost.196	//197	// 256 leaves one funder four full MaxTranches schedules open at once —198	// generous for the intended use — while exhausting the realm now takes199	// at least ceil(5000/256) = 20 separately funded accounts.200	//201	// That is a SYBIL BOUND and nothing more, so the residual is quantified202	// here rather than left to be inferred from the 20: the cap counts203	// TRANCHES, never value. parseSpec accepts amount == 1, so 256 pending204	// tranches cost 256ugnot and all 20 accounts together escrow about205	// 5000ugnot — every bit of it recoverable fee-free, since revokeOne206	// refunds at 0 bps. The real cost of the realm-wide denial is therefore207	// ~80 Fund transactions' gas and 20 funded addresses, NOT capital.208	// Anyone sizing the residual risk from the "20 accounts" figure alone209	// will size it wrong by orders of magnitude.210	//211	// Bounding the attack by VALUE instead would take a per-tranche minimum212	// amount. That is a change to what the application is — it would price213	// small legitimate tranches out — so it is a reviewer's decision, not a214	// routine fix, and it is deliberately not made here.215	MaxOpenPerFunder = 256216217	// MaxTitleLen bounds a schedule title. Titles are charset-restricted218	// so they are list-safe in Render without escaping.219	MaxTitleLen = 80220221	// MaxMemoLen bounds the free-text memo. It is a BYTE length, not a222	// rune count, because its job is to bound storage.223	MaxMemoLen = 1000224225	// MaxSpecLen bounds the tranche specification string, so parsing cost226	// is bounded before any parsing begins.227	MaxSpecLen = 1400228229	// MaxListed caps how many entries a single view or render returns.230	MaxListed = 50231)232233// tranche is one dated payment. It is this realm's authoritative record;234// the matching duebook deferral carries only the due height and a label.235// Records are kept after they terminate, so history remains answerable236// once duebook has consumed the deferral.237type tranche struct {238	deferralID uint64239	scheduleID int64240	seq        int64 // 1-based position within its schedule241	amount     int64242	dueAt      int64243	status     string244	settledAt  int64 // height of release or revocation; 0 while pending245}246247// schedule is one funded vesting agreement.248type schedule struct {249	id          int64250	funder      address251	beneficiary address252	title       string253	memo        string254	total       int64 // Σ tranche amounts, fixed at funding255	feeBps      int64 // snapshotted at funding, charged at each release256	revocable   bool  // fixed at funding; there is no setter257	createdAt   int64258	trancheIDs  []uint64 // in tranche order259260	pending       int64 // count of tranches still pending261	releasedGross int64 // Σ amounts released (before fee)262	revokedTotal  int64 // Σ amounts returned to the funder263}264265var (266	admin        address // may set fee, fee recipient, and propose a successor267	pendingAdmin address // must accept before the handover takes effect268	feeRecipient address // may withdraw fees and sweep surplus269	feeBps       int64   // fee snapshotted into NEW schedules270271	self           address // this realm's address, captured at deploy272	nextScheduleID int64273	escrowTotal    int64 // == Σ amount over pending tranches (the E term)274275	schedules = avl.NewTree() // padID(scheduleID)  -> *schedule276	tranches  = avl.NewTree() // padU(deferralID)   -> *tranche277278	// openByFunder counts each funder's still-pending tranches, for the279	// MaxOpenPerFunder cap. An entry is REMOVED at zero rather than left280	// holding 0, so the tree tracks live funders and does not accumulate a281	// dead key per address that ever funded anything.282	openByFunder = avl.NewTree() // funder.String() -> int64283284	ledger = feeledger.MustNew(MaxFeeBps)285286	// book is unexported and never returned (duebook consumer contract 3).287	book = duebook.MustNew(MinDelayBlocks, MaxDelayBlocks, MaxOpenTranches)288)289290func init() {291	admin = unsafe.OriginCaller()292	feeRecipient = admin293	self = unsafe.CurrentRealm().Address()294}295296// rejectStraySend aborts if an EOA attached coins to an entrypoint that has297// no use for them. Only Fund accepts value; without this guard a mistyped298// send would be silently absorbed into surplus.299func rejectStraySend(cur realm) {300	if !cur.IsCurrent() {301		// every call site passes the entrypoint's own first cur; this302		// pins that discipline against future call-site drift, which is303		// the house pattern from fee_split/v2. Unexported and with all304		// 11 call sites forwarding a live cur, this is unreachable305		// today — it is here because the artifact is immutable and the306		// guard's answer decides whether coins are rejected or absorbed307		panic("realm capability is not current")308	}309	if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 {310		panic("this entrypoint does not accept coins; only Fund does")311	}312}313314// Fund creates a vesting schedule in a single transaction: it escrows the315// attached GNOT, registers every tranche with duebook, and returns the new316// schedule id. Only direct EOA calls with -send are accepted.317//318// spec lists the tranches as "delay:amount" pairs separated by commas, for319// example "100:500,200:500,300:1000". Delays are in BLOCKS FROM NOW and320// must be STRICTLY INCREASING — two payments at the same height are one321// tranche with the amounts added. Every amount must be positive, and the322// amounts must sum to EXACTLY the attached send: this realm will not323// silently absorb a surplus or create an underfunded schedule.324//325// revocable decides, permanently, whether the funder may reclaim326// not-yet-due tranches. There is no setter; an irrevocable schedule stays327// irrevocable.328//329// maxFeeBps is the ceiling the caller signs for. Funding is rejected if the330// live fee exceeds it, which closes the race where an admin sequences a fee331// raise ahead of this transaction. Pass MaxFeeBps to accept any legal fee.332func Fund(cur realm, beneficiary address, title, memo, spec string, revocable bool, maxFeeBps int64) int64 {333	// Receive first and exactly once per transaction (coinio's contract):334	// it READS the send envelope and cannot mark it spent, so crediting335	// per call would mint unbacked liabilities from a single -send.336	funder, amount := coinio.Receive(0, cur, Denom)337338	if feeBps > maxFeeBps {339		panic("current fee " + itoa(feeBps) + " bps exceeds the caller's maximum " + itoa(maxFeeBps))340	}341	var zero address342	if beneficiary == zero {343		panic("empty beneficiary address")344	}345	// This realm can never call its own Withdraw, so a balance credited to346	// its own address is claimable by nobody while still counting inside347	// Liabilities() — permanently stranded AND permanently shrinking348	// SweepDenom's reach. Unlike an arbitrary typo, self is a uniquely349	// knowable address this realm publishes itself via Address(), so it is350	// the one wrong beneficiary worth spending a comparison on351	if beneficiary == self {352		panic("beneficiary must not be this realm's own address")353	}354	assertValidTitle(title)355	if len(memo) > MaxMemoLen {356		panic("memo exceeds " + itoa(MaxMemoLen) + " bytes")357	}358359	delays, amounts := parseSpec(spec)360	n := int64(len(delays))361362	// The declared total must match the escrow exactly, in both directions.363	declared := int64(0)364	for _, a := range amounts {365		sum, ok := checkedAdd(declared, a)366		if !ok {367			panic("tranche amounts overflow int64")368		}369		declared = sum370	}371	if declared != amount {372		panic("tranche amounts total " + itoa(declared) + Denom +373			" but " + itoa(amount) + Denom + " was sent; they must be equal")374	}375376	// Whole-realm capacity check, ahead of any state change, for a clear377	// message. duebook would refuse with ErrBookFull anyway, and a panic378	// there would abort this transaction wholly — the pre-check only379	// improves the error.380	if int64(book.OpenCount())+n > int64(MaxOpenTranches) {381		panic("realm tranche capacity reached: " + itoa(int64(book.OpenCount())) +382			" pending of " + itoa(int64(MaxOpenTranches)) + ", this schedule needs " + itoa(n))383	}384385	// Per-funder capacity, so one account cannot consume the whole-realm386	// cap above and lock everybody out. See MaxOpenPerFunder.387	if mine := openOf(funder); mine+n > int64(MaxOpenPerFunder) {388		panic("funder tranche capacity reached: " + itoa(mine) + " pending of " +389			itoa(int64(MaxOpenPerFunder)) + ", this schedule needs " + itoa(n))390	}391392	newEscrow, ok := checkedAdd(escrowTotal, amount)393	if !ok {394		panic("escrow overflow")395	}396397	now := runtime.ChainHeight() // the chain's clock (contract 1), heights only (contract 2)398399	nextScheduleID++400	s := &schedule{401		id:          nextScheduleID,402		funder:      funder,403		beneficiary: beneficiary,404		title:       title,405		memo:        memo,406		total:       amount,407		feeBps:      feeBps,408		revocable:   revocable,409		createdAt:   now,410		pending:     n,411	}412413	for i := 0; i < len(delays); i++ {414		seq := int64(i + 1)415		// The payload is a label for humans and events. It is NEVER parsed416		// (see the package doc): the authoritative record is the tranche417		// struct stored under the deferral ID below.418		label := "schedule " + itoa(s.id) + " tranche " + itoa(seq) + "/" + itoa(n)419420		// ttl 0 means the tranche never expires: vested value cannot be421		// lost by a slow or absent beneficiary.422		did, err := book.Schedule(funder.String(), label, now, delays[i], 0)423		if err != nil {424			panic(err)425		}426		d, found := book.Get(did)427		if !found {428			panic("internal: scheduled deferral not found")429		}430		tranches.Set(padU(did), &tranche{431			deferralID: did,432			scheduleID: s.id,433			seq:        seq,434			amount:     amounts[i],435			dueAt:      d.DueAt,436			status:     StatusPending,437		})438		s.trancheIDs = append(s.trancheIDs, did)439440		chain.Emit("TrancheScheduled",441			"schedule", itoa(s.id),442			"tranche", utoa(did),443			"seq", itoa(seq),444			"amount", itoa(amounts[i]),445			"dueAt", itoa(d.DueAt),446		)447	}448449	schedules.Set(padID(s.id), s)450	escrowTotal = newEscrow451	addOpen(funder, n) // commits with escrowTotal: both count the same tranches452453	chain.Emit("ScheduleFunded",454		"schedule", itoa(s.id),455		"funder", funder.String(),456		"beneficiary", beneficiary.String(),457		"total", itoa(amount),458		"tranches", itoa(n),459		"feeBps", itoa(s.feeBps),460		"revocable", btoa(revocable),461	)462	return s.id463}464465// Release pays out a due tranche. ANYONE may call it: the value is credited466// to the schedule's RECORDED BENEFICIARY, never to the caller, so a467// stranger calling this gains nothing and the beneficiary needs no liveness468// of their own. The protocol fee snapshotted at funding is charged here.469//470// Aborts if the tranche does not exist, is not due yet, or has already been471// released or revoked — the last of those is duebook's guarantee, not a472// check written here.473func Release(cur realm, trancheID int64) string {474	rejectStraySend(cur)475476	did := mustID(trancheID)477	now := runtime.ChainHeight()478479	// CONSUME FIRST (duebook consumer contract 5). Claim removes the480	// deferral before returning, so by the time the effect below runs no481	// second Claim of this ID can ever succeed — not in this transaction,482	// and not in any future one. This realm writes no replay guard of its483	// own because duebook's is structural.484	d, err := book.Claim(did, now)485	if err != nil {486		panic(err)487	}488489	// EFFECT SECOND, same transaction.490	t := mustTranche(d.ID)491	if t.status != StatusPending {492		// Unreachable: a successful Claim proves the deferral was open, and493		// this realm terminates the tranche record in the same transaction494		// it consumes the deferral. Kept as a consistency assertion — if it495		// ever fires, the two stores have diverged and the transaction must496		// not proceed.497		panic("internal: claimed a deferral whose tranche is " + t.status)498	}499	s := mustSchedule(t.scheduleID)500501	// feeledger validates before mutating, and guarantees502	// credited + fee == amount.503	credited, fee, err := ledger.Deposit(s.beneficiary.String(), t.amount, s.feeBps)504	if err != nil {505		panic(err)506	}507	escrowTotal -= t.amount // >= 0: escrowTotal == Σ pending amounts >= t.amount508	t.status = StatusReleased509	t.settledAt = now510	s.pending--511	s.releasedGross += t.amount512	addOpen(s.funder, -1) // this tranche no longer counts against the funder513514	chain.Emit("TrancheReleased",515		"schedule", itoa(s.id),516		"tranche", utoa(d.ID),517		"seq", itoa(t.seq),518		"beneficiary", s.beneficiary.String(),519		"credited", itoa(credited),520		"fee", itoa(fee),521		"caller", cur.Previous().Address().String(),522		"height", itoa(now),523	)524	return "released tranche " + utoa(d.ID) + " (" + itoa(credited) + Denom + " to " +525		s.beneficiary.String() + ")"526}527528// Revoke returns a NOT-YET-DUE tranche to the funder's claimable balance,529// fee-free. Only the funder may call it, and only on a schedule created530// revocable.531//532// A tranche whose due height has arrived can never be revoked, even though533// duebook's Cancel would consume it: that refusal is this realm's, and it534// is what stops a funder front-running the beneficiary's Release to claw535// back value already vested.536func Revoke(cur realm, trancheID int64) string {537	rejectStraySend(cur)538539	caller := cur.Previous().Address()540	did := mustID(trancheID)541	t := mustTranche(did)542	s := mustSchedule(t.scheduleID)543544	assertRevocableBy(s, caller)545	now := runtime.ChainHeight()546	if now >= t.dueAt {547		panic("tranche " + utoa(did) + " is due at height " + itoa(t.dueAt) +548			" (now " + itoa(now) + "): a vested tranche can never be revoked")549	}550551	revokeOne(cur, s, t, now)552	return "revoked tranche " + utoa(did) + " (" + itoa(t.amount) + Denom + " to the funder)"553}554555// RevokeAll returns every not-yet-due tranche of a schedule to the funder's556// claimable balance in one transaction, and returns how many were revoked.557// Already-due and already-settled tranches are skipped, not an error.558// Funder only, revocable schedules only. Aborts if nothing was revocable,559// so a no-op cannot be mistaken for success.560func RevokeAll(cur realm, scheduleID int64) string {561	rejectStraySend(cur)562563	caller := cur.Previous().Address()564	s := mustSchedule(scheduleID)565	assertRevocableBy(s, caller)566	now := runtime.ChainHeight()567568	// Bounded by MaxTranches, so the loop cost is bounded at funding time.569	count := int64(0)570	total := int64(0)571	for _, did := range s.trancheIDs {572		v := tranches.Get(padU(did))573		if v == nil {574			panic("internal: schedule references an unknown tranche")575		}576		t := v.(*tranche)577		if t.status != StatusPending || now >= t.dueAt {578			continue579		}580		amount := t.amount581		revokeOne(cur, s, t, now)582		count++583		total += amount // bounded by s.total, which was overflow-checked at funding584	}585	if count == 0 {586		panic("no revocable tranches: every tranche of this schedule is already due or settled")587	}588589	chain.Emit("ScheduleRevoked",590		"schedule", itoa(s.id),591		"tranches", itoa(count),592		"amount", itoa(total),593		"height", itoa(now),594	)595	return "revoked " + itoa(count) + " tranches (" + itoa(total) + Denom + " to the funder)"596}597598// revokeOne cancels one pending, not-yet-due tranche and credits the funder599// fee-free. Callers have already verified authorization, revocability and600// the due-height refusal.601func revokeOne(cur realm, s *schedule, t *tranche, now int64) {602	// CONSUME FIRST. Ownership is checked by duebook against the funder603	// address this realm recorded at Schedule, never against an argument604	// (duebook consumer contract 4).605	d, err := book.Cancel(t.deferralID, s.funder.String())606	if err != nil {607		panic(err)608	}609	_ = d610611	// EFFECT SECOND, same transaction. Refunds are always fee-free.612	if _, _, err := ledger.Deposit(s.funder.String(), t.amount, 0); err != nil {613		panic(err)614	}615	escrowTotal -= t.amount616	t.status = StatusRevoked617	t.settledAt = now618	s.pending--619	s.revokedTotal += t.amount620	addOpen(s.funder, -1) // revoking frees the funder's slot too621622	chain.Emit("TrancheRevoked",623		"schedule", itoa(s.id),624		"tranche", utoa(t.deferralID),625		"seq", itoa(t.seq),626		"funder", s.funder.String(),627		"amount", itoa(t.amount),628		"height", itoa(now),629	)630}631632// Withdraw sends amount ugnot of the caller's claimable balance (released633// tranches for a beneficiary, revocations for a funder) to the caller.634func Withdraw(cur realm, amount int64) string {635	rejectStraySend(cur)636	caller := cur.Previous().Address()637638	// Debit the ledger BEFORE the coins move (coinio's ordering contract).639	if err := ledger.Withdraw(caller.String(), amount); err != nil {640		panic(err)641	}642	coinio.Payout(0, cur, caller, Denom, amount)643644	chain.Emit("Withdrawn", "to", caller.String(), "amount", itoa(amount))645	return "withdrew " + itoa(amount) + Denom646}647648// WithdrawAll sends the caller's entire claimable balance to the caller.649// Aborts if there is nothing to withdraw.650func WithdrawAll(cur realm) string {651	rejectStraySend(cur)652	caller := cur.Previous().Address()653654	amount, err := ledger.WithdrawAll(caller.String())655	if err != nil {656		panic(err)657	}658	if amount == 0 {659		panic("nothing to withdraw")660	}661	coinio.Payout(0, cur, caller, Denom, amount)662663	chain.Emit("Withdrawn", "to", caller.String(), "amount", itoa(amount))664	return "withdrew " + itoa(amount) + Denom665}666667// WithdrawFees sends the accrued fee pot to the fee recipient. Only the fee668// recipient may call it.669func WithdrawFees(cur realm) string {670	rejectStraySend(cur)671	caller := cur.Previous().Address()672	if caller != feeRecipient {673		panic("only the fee recipient may withdraw fees")674	}675	if ledger.FeesAccrued() == 0 {676		panic("no fees accrued")677	}678	amount := ledger.WithdrawFees()679	coinio.Payout(0, cur, feeRecipient, Denom, amount)680681	chain.Emit("FeesWithdrawn", "to", feeRecipient.String(), "amount", itoa(amount))682	return "withdrew " + itoa(amount) + Denom + " in fees"683}684685// SweepDenom sends the surplus of a single denomination to the fee686// recipient. For ugnot only the excess over Liabilities() moves — escrowed687// tranches and claimable balances are untouchable by construction. Other688// denominations move wholly. Only the fee recipient may call it.689func SweepDenom(cur realm, denom string) string {690	rejectStraySend(cur)691	caller := cur.Previous().Address()692	if caller != feeRecipient {693		panic("only the fee recipient may sweep surplus")694	}695	reserve := int64(0)696	if denom == Denom {697		reserve = Liabilities()698	}699	swept := coinio.Sweep(0, cur, feeRecipient, denom, reserve)700701	chain.Emit("SurplusSwept", "to", feeRecipient.String(), "coins", itoa(swept)+denom)702	return "swept " + itoa(swept) + denom703}704705// --- administration ---706707// SetFeeBps sets the protocol fee snapshotted into FUTURE schedules at708// funding. Existing schedules keep the fee they were funded under, so an709// in-flight schedule's economics cannot be changed. Admin only.710func SetFeeBps(cur realm, bps int64) {711	rejectStraySend(cur)712	assertAdmin(cur.Previous().Address())713	if bps < 0 || bps > MaxFeeBps {714		panic("fee bps out of range [0, " + itoa(MaxFeeBps) + "]")715	}716	old := feeBps717	feeBps = bps718	chain.Emit("FeeBpsChanged", "old", itoa(old), "new", itoa(bps))719}720721// SetFeeRecipient re-points the fee/surplus role. The pot accrued under the722// OUTGOING recipient is settled to that recipient's own claimable balance723// first, so the successor inherits the role and not the earnings. Admin724// only; the zero address is rejected.725func SetFeeRecipient(cur realm, next address) {726	rejectStraySend(cur)727	assertAdmin(cur.Previous().Address())728	var zero address729	if next == zero {730		panic("empty fee recipient")731	}732	old := feeRecipient733734	// The fee pot is a single scalar — feeledger keeps no per-recipient735	// accounting — so a bare re-point would hand every basis point earned736	// under `old` to `next`, at the admin's sole discretion and with no737	// timelock. That is a privilege the rest of this realm does not grant:738	// the admin role itself is two-step, the fee is capped and snapshotted739	// per schedule, and the funder signs a maxFeeBps ceiling.740	//741	// Settling to `old`'s claimable balance moves F into U by the same742	// amount, so no coins move here and conservation is untouched; `old`743	// then withdraws on their own initiative like any other creditor.744	//745	// Refusing the call while fees are accrued would be the other available746	// fix and is worse: WithdrawFees is recipient-only, so a recipient who747	// simply declined to withdraw could hold the role hostage against the748	// admin indefinitely.749	if old != next && ledger.FeesAccrued() > 0 {750		pot := ledger.WithdrawFees()751		credited, fee, err := ledger.Deposit(old.String(), pot, 0)752		if err != nil {753			panic(err)754		}755		if credited != pot || fee != 0 {756			// Unreachable at 0 bps, where feeledger's credited+fee == amount757			// guarantee forces credited == amount. Asserted because this is758			// the one place the realm moves value between two of its own759			// liability terms, and a silent shortfall here would be a leak.760			panic("internal: fee settlement did not credit the full pot")761		}762		chain.Emit("FeesSettled", "to", old.String(), "amount", itoa(pot))763	}764765	feeRecipient = next766	chain.Emit("FeeRecipientChanged", "old", old.String(), "new", next.String())767}768769// ProposeAdmin nominates a successor admin. The handover takes effect only770// when that account calls AcceptAdmin, so a typo cannot strand the role.771// Admin only; the zero address clears a pending proposal.772func ProposeAdmin(cur realm, next address) {773	rejectStraySend(cur)774	assertAdmin(cur.Previous().Address())775	pendingAdmin = next776	chain.Emit("AdminProposed", "pending", next.String())777}778779// AcceptAdmin completes a handover. Only the proposed account may call it.780func AcceptAdmin(cur realm) {781	rejectStraySend(cur)782	caller := cur.Previous().Address()783	var zero address784	if pendingAdmin == zero || caller != pendingAdmin {785		panic("only the proposed admin may accept")786	}787	old := admin788	admin = caller789	pendingAdmin = zero790	chain.Emit("AdminTransferred", "old", old.String(), "new", admin.String())791}792793// --- read-only views ---794795// ScheduleInfo returns a schedule's fields by value.796func ScheduleInfo(id int64) (funder, beneficiary address, title string, total, feeBps int64,797	revocable bool, createdAt, numTranches, pending, releasedGross, revokedTotal int64) {798	s := mustSchedule(id)799	return s.funder, s.beneficiary, s.title, s.total, s.feeBps, s.revocable,800		s.createdAt, int64(len(s.trancheIDs)), s.pending, s.releasedGross, s.revokedTotal801}802803// Memo returns a schedule's raw memo text.804func Memo(id int64) string { return mustSchedule(id).memo }805806// TrancheInfo returns one tranche's fields by value. It answers for settled807// tranches too, after duebook has consumed the deferral.808func TrancheInfo(trancheID int64) (scheduleID, seq, amount, dueAt int64, status string, settledAt int64) {809	t := mustTranche(mustID(trancheID))810	return t.scheduleID, t.seq, t.amount, t.dueAt, t.status, t.settledAt811}812813// TranchesOf lists a schedule's tranches, newline-separated, one per line814// as "id seq amount dueAt status".815func TranchesOf(id int64) string {816	s := mustSchedule(id)817	out := ""818	for _, did := range s.trancheIDs {819		t := tranches.Get(padU(did)).(*tranche)820		out += utoa(did) + " " + itoa(t.seq) + " " + itoa(t.amount) + " " +821			itoa(t.dueAt) + " " + t.status + "\n"822	}823	return out824}825826// VestedOf returns, for a schedule, the amount whose due height has arrived827// and which is therefore no longer revocable — whether or not it has been828// released yet — alongside the amount still revocable by the funder.829func VestedOf(id int64) (vested, unvested int64) {830	s := mustSchedule(id)831	now := runtime.ChainHeight()832	for _, did := range s.trancheIDs {833		t := tranches.Get(padU(did)).(*tranche)834		if t.status == StatusRevoked {835			continue836		}837		if now >= t.dueAt {838			vested += t.amount839		} else {840			unvested += t.amount841		}842	}843	return vested, unvested844}845846// ReleasableNow lists tranche ids that are due and still pending, up to847// MaxListed, newline-separated. Anyone may Release each of them.848func ReleasableNow() string {849	now := runtime.ChainHeight()850	due := book.Due(now, MaxListed)851	out := ""852	for _, d := range due {853		out += utoa(d.ID) + "\n"854	}855	return out856}857858// Admin returns the current admin.859func Admin() address { return admin }860861// PendingAdmin returns the proposed successor admin, or the zero address.862func PendingAdmin() address { return pendingAdmin }863864// FeeRecipient returns who may withdraw fees and sweep surplus.865func FeeRecipient() address { return feeRecipient }866867// FeeBps returns the fee that will be snapshotted into newly funded868// schedules. Existing schedules keep their own snapshot.869func FeeBps() int64 { return feeBps }870871// FeeOn previews the fee and net credit for a tranche of amount at the872// CURRENT FeeBps.873func FeeOn(amount int64) (fee, credited int64) {874	f, err := feeledger.FeeFor(amount, feeBps)875	if err != nil {876		panic(err)877	}878	return f, amount - f879}880881// NumSchedules returns how many schedules have ever been funded.882func NumSchedules() int64 { return nextScheduleID }883884// OpenTrancheCount returns how many tranches are pending across the whole885// realm, against the MaxOpenTranches cap.886func OpenTrancheCount() int { return book.OpenCount() }887888// OpenTranchesOfFunder returns how many tranches funder currently holds889// pending, against the MaxOpenPerFunder cap. A funder can read this before890// funding to see the headroom they have left.891func OpenTranchesOfFunder(funder address) int64 { return openOf(funder) }892893// FundersWithOpenTranches returns how many distinct funders hold at least894// one pending tranche. It is the width of the per-funder cap: exhausting895// MaxOpenTranches requires this to reach ceil(MaxOpenTranches/MaxOpenPerFunder).896func FundersWithOpenTranches() int { return openByFunder.Size() }897898// EscrowTotal returns the escrow held by pending tranches (the E term).899func EscrowTotal() int64 { return escrowTotal }900901// BalanceOf returns addr's claimable balance (released tranches for a902// beneficiary, revocations for a funder).903func BalanceOf(addr address) int64 { return ledger.BalanceOf(addr.String()) }904905// UsersTotal returns the sum of all claimable balances (the U term).906func UsersTotal() int64 { return ledger.UsersTotal() }907908// FeesAccrued returns the fee pot (the F term).909func FeesAccrued() int64 { return ledger.FeesAccrued() }910911// Liabilities returns everything this realm owes:912// EscrowTotal + UsersTotal + FeesAccrued.913func Liabilities() int64 { return escrowTotal + ledger.Liabilities() }914915// Held returns the ugnot actually held at this realm's address (H).916func Held() int64 { return coinio.HeldAt(self, Denom) }917918// Surplus returns Held() - Liabilities() (the S term).919func Surplus() int64 { return Held() - Liabilities() }920921// Address returns this realm's address (the escrow target).922func Address() address { return self }923924// Height returns the current chain height. Delays in Fund's spec are925// relative to the height at which Fund executes, so this is an aid for926// composing a spec, not an input to one.927func Height() int64 { return runtime.ChainHeight() }928929// --- render ---930931// Render shows the realm at "" and a schedule detail at "<id>". Free text932// (titles are charset-restricted; memos are not) passes through the933// ecosystem sanitizer before it reaches markdown.934func Render(path string) string {935	if path != "" {936		return renderSchedule(path)937	}938	held := Held()939	liab := Liabilities()940	status := "OK"941	if held < liab {942		status = "VIOLATED"943	}944	now := runtime.ChainHeight()945946	out := "# Vesting — dated tranche disbursement\n\n"947	out += "Escrowed GNOT released in **discrete, individually dated tranches** — not by "948	out += "linear accrual. A tranche is worth nothing before its due height and its full "949	out += "amount from that height on.\n\n"950	out += "Schedule and exactly-once release via `duebook`; coin custody via `coinio`; "951	out += "accounting via `feeledger`.\n\n"952	out += "## Configuration\n\n"953	out += "- fee for new schedules: " + itoa(feeBps) + " bps (cap " + itoa(MaxFeeBps) +954		" bps; each schedule keeps the fee snapshotted at funding)\n"955	out += "- tranches per schedule: up to " + itoa(MaxTranches) + "\n"956	out += "- pending tranches realm-wide: " + itoa(int64(book.OpenCount())) + " of " +957		itoa(int64(MaxOpenTranches)) + "\n"958	out += "- pending tranches per funder: up to " + itoa(int64(MaxOpenPerFunder)) +959		" (" + itoa(int64(openByFunder.Size())) + " funder(s) hold at least one)\n"960	out += "- delay range: " + itoa(MinDelayBlocks) + " to " + itoa(MaxDelayBlocks) + " blocks\n"961	out += "- fee recipient: " + feeRecipient.String() + "\n"962	out += "- admin: " + admin.String() + "\n"963	out += "- current height: " + itoa(now) + "\n\n"964	out += "## Accounting (H == E + U + F + S)\n\n"965	out += "- escrowed in pending tranches (E): " + itoa(escrowTotal) + Denom + "\n"966	out += "- claimable (U): " + itoa(ledger.UsersTotal()) + Denom + "\n"967	out += "- fees accrued (F): " + itoa(ledger.FeesAccrued()) + Denom + "\n"968	out += "- held (H): " + itoa(held) + Denom + "\n"969	out += "- conservation: " + status + "\n\n"970	out += "## Latest schedules\n\n"971	if nextScheduleID == 0 {972		out += "No schedules yet.\n"973		return out974	}975	shown := 0976	schedules.ReverseIterate("", "", func(_ string, v any) bool {977		s := v.(*schedule)978		kind := "irrevocable"979		if s.revocable {980			kind = "revocable"981		}982		out += "- [#" + itoa(s.id) + "](/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting:" +983			itoa(s.id) + ") " + s.title + " — " + itoa(s.total) + Denom + " in " +984			itoa(int64(len(s.trancheIDs))) + " tranches (" + itoa(s.pending) + " pending, " +985			kind + ")\n"986		shown++987		return shown >= 20988	})989	return out990}991992func renderSchedule(path string) string {993	id, err := strconv.ParseInt(path, 10, 64)994	if err != nil {995		return "> [!WARNING]\n> invalid schedule id\n"996	}997	v := schedules.Get(padID(id))998	if v == nil {999		return "> [!WARNING]\n> unknown schedule id\n"1000	}1001	s := v.(*schedule)1002	now := runtime.ChainHeight()1003	vested, unvested := VestedOf(s.id)10041005	out := "# Schedule #" + itoa(s.id) + ": " + s.title + "\n\n"1006	out += "- funder: " + s.funder.String() + "\n"1007	out += "- beneficiary: " + s.beneficiary.String() + "\n"1008	out += "- total: " + itoa(s.total) + Denom + " in " + itoa(int64(len(s.trancheIDs))) + " tranches\n"1009	out += "- fee (snapshot): " + itoa(s.feeBps) + " bps\n"1010	if s.revocable {1011		out += "- revocable by the funder — but only for tranches that are NOT yet due\n"1012	} else {1013		out += "- **irrevocable**: the funder cannot reclaim any tranche\n"1014	}1015	out += "- funded at height: " + itoa(s.createdAt) + " (now " + itoa(now) + ")\n"1016	out += "- vested (no longer revocable): " + itoa(vested) + Denom + "\n"1017	out += "- unvested (still revocable if revocable): " + itoa(unvested) + Denom + "\n"1018	out += "- released so far: " + itoa(s.releasedGross) + Denom +1019		"; revoked: " + itoa(s.revokedTotal) + Denom + "\n\n"10201021	out += "## Tranches\n\n"1022	out += "| # | id | amount | due at | status |\n|---|---|---|---|---|\n"1023	shown := 01024	for _, did := range s.trancheIDs {1025		if shown >= MaxListed {1026			out += "| … | | | | (" + itoa(int64(len(s.trancheIDs))-int64(shown)) + " more) |\n"1027			break1028		}1029		t := tranches.Get(padU(did)).(*tranche)1030		note := t.status1031		if t.status == StatusPending && now >= t.dueAt {1032			note = "**releasable now**"1033		}1034		out += "| " + itoa(t.seq) + " | " + utoa(did) + " | " + itoa(t.amount) + Denom +1035			" | " + itoa(t.dueAt) + " | " + note + " |\n"1036		shown++1037	}1038	if s.memo != "" {1039		out += "\n## Memo\n\n" + sanitize.InlineText(s.memo) + "\n"1040	}1041	return out1042}10431044// --- internals ---10451046func assertAdmin(caller address) {1047	if caller != admin {1048		panic("admin only")1049	}1050}10511052// assertRevocableBy checks both halves of the revocation authority: the1053// schedule must have been funded revocable, and the caller must be its1054// funder.1055func assertRevocableBy(s *schedule, caller address) {1056	if !s.revocable {1057		panic("schedule " + itoa(s.id) + " is irrevocable")1058	}1059	if caller != s.funder {1060		panic("only the funder may revoke")1061	}1062}10631064func mustSchedule(id int64) *schedule {1065	v := schedules.Get(padID(id))1066	if v == nil {1067		panic("unknown schedule id")1068	}1069	return v.(*schedule)1070}10711072func mustTranche(did uint64) *tranche {1073	v := tranches.Get(padU(did))1074	if v == nil {1075		panic("unknown tranche id")1076	}1077	return v.(*tranche)1078}10791080// openOf reports how many tranches funder currently holds pending. A1081// missing key means zero, which is the same thing: addOpen removes the1082// entry when the count reaches zero.1083func openOf(funder address) int64 {1084	v := openByFunder.Get(funder.String())1085	if v == nil {1086		return 01087	}1088	return v.(int64)1089}10901091// addOpen adjusts funder's pending-tranche count by delta, which may be1092// negative. Every increment happens in Fund and every decrement is paired1093// with a pending -> terminal transition, so the count tracks exactly the1094// tranches openOf claims.1095func addOpen(funder address, delta int64) {1096	next := openOf(funder) + delta1097	if next < 0 {1098		// Unreachable: a tranche can only leave StatusPending once, and it1099		// was counted when it was created. Kept as a consistency assertion1100		// — if it fires, this counter has diverged from the tranche records1101		// and the transaction must not proceed.1102		panic("internal: funder open-tranche count went negative")1103	}1104	if next == 0 {1105		openByFunder.Remove(funder.String())1106		return1107	}1108	openByFunder.Set(funder.String(), next)1109}11101111// mustID converts an entrypoint's int64 tranche id to duebook's uint64,1112// rejecting the non-positive values duebook never issues.1113func mustID(id int64) uint64 {1114	if id <= 0 {1115		panic("tranche id must be positive")1116	}1117	return uint64(id)1118}11191120// parseSpec parses "delay:amount,delay:amount,…" into parallel slices,1121// enforcing every bound before any state is touched. Delays must be1122// strictly increasing; amounts must be positive. Nothing here consults or1123// mutates realm state, so a malformed spec is rejected with the escrow1124// still untouched (the whole transaction aborts regardless).1125func parseSpec(spec string) (delays, amounts []int64) {1126	if spec == "" {1127		panic("spec must list at least one tranche as \"delay:amount\"")1128	}1129	if len(spec) > MaxSpecLen {1130		panic("spec exceeds " + itoa(MaxSpecLen) + " bytes")1131	}1132	parts := strings.Split(spec, ",")1133	if len(parts) > MaxTranches {1134		panic("a schedule may have at most " + itoa(MaxTranches) + " tranches, got " +1135			itoa(int64(len(parts))))1136	}11371138	prev := int64(-1)1139	for i, part := range parts {1140		at := strings.Index(part, ":")1141		if at < 0 {1142			panic("tranche " + itoa(int64(i+1)) + ": expected \"delay:amount\", got \"" + part + "\"")1143		}1144		delay, err := strconv.ParseInt(part[:at], 10, 64)1145		if err != nil {1146			panic("tranche " + itoa(int64(i+1)) + ": delay is not an integer")1147		}1148		amount, err := strconv.ParseInt(part[at+1:], 10, 64)1149		if err != nil {1150			panic("tranche " + itoa(int64(i+1)) + ": amount is not an integer")1151		}1152		if delay < MinDelayBlocks || delay > MaxDelayBlocks {1153			panic("tranche " + itoa(int64(i+1)) + ": delay " + itoa(delay) +1154				" outside [" + itoa(MinDelayBlocks) + ", " + itoa(MaxDelayBlocks) + "]")1155		}1156		if delay <= prev {1157			panic("tranche " + itoa(int64(i+1)) + ": delays must be strictly increasing (" +1158				itoa(delay) + " follows " + itoa(prev) + "); combine same-height payments")1159		}1160		if amount <= 0 {1161			panic("tranche " + itoa(int64(i+1)) + ": amount must be positive")1162		}1163		prev = delay1164		delays = append(delays, delay)1165		amounts = append(amounts, amount)1166	}1167	return delays, amounts1168}11691170// assertValidTitle bounds length and restricts the charset so titles are1171// list-safe in Render without escaping.1172func assertValidTitle(title string) {1173	if len(title) == 0 || len(title) > MaxTitleLen {1174		panic("title must be 1-" + itoa(MaxTitleLen) + " characters")1175	}1176	for i := 0; i < len(title); i++ {1177		c := title[i]1178		switch {1179		case c >= 'a' && c <= 'z':1180		case c >= 'A' && c <= 'Z':1181		case c >= '0' && c <= '9':1182		case c == ' ' || c == '_' || c == '-':1183		default:1184			panic("title may only contain letters, digits, space, _ and -")1185		}1186	}1187}11881189// padID renders a schedule id as a fixed-width key so avl iteration order1190// is numeric order.1191func padID(id int64) string {1192	s := strconv.FormatInt(id, 10)1193	for len(s) < 12 {1194		s = "0" + s1195	}1196	return s1197}11981199// padU renders a deferral id as a fixed-width key. 20 digits covers the1200// whole uint64 range, so tranche iteration is numeric order too.1201func padU(id uint64) string {1202	s := strconv.FormatUint(id, 10)1203	for len(s) < 20 {1204		s = "0" + s1205	}1206	return s1207}12081209func itoa(n int64) string { return strconv.FormatInt(n, 10) }12101211func utoa(n uint64) string { return strconv.FormatUint(n, 10) }12121213func btoa(b bool) string {1214	if b {1215		return "true"1216	}1217	return "false"1218}12191220func checkedAdd(a, b int64) (int64, bool) {1221	sum := a + b1222	if (b > 0 && sum < a) || (b < 0 && sum > a) {1223		return 0, false1224	}1225	return sum, true1226}1227

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.