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}1227AcceptAdmin(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.
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.