Cosmic Bull

vesting

gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting on gno.land mainnet

realmfactory-applicationvalue-distributionlive on mainnet

Release escrowed funds to a beneficiary on a dated schedule.

Each tranche unlocks at its own date and can be claimed once, so a vesting promise does not rely on anyone remembering it.

In detail curated

Dated-tranche GNOT disbursement: escrow released in discrete, individually dated tranches rather than by linear accrual, so a tranche is worth nothing before its due height and its full amount from that height on. Revocation can never reach a vested tranche. Composes duebook (exactly-once dated settlement) + coinio + feeledger + avl/v0 + sanitize/v0. Application-factory benchmark #5 on this chain's numbering of groups; Group 4, the fifth authorized mainnet use. This mainnet artifact is NOT a pure re-namespacing of the pearl-1 twin: it carries an authorized, declared divergence (an IsCurrent() pin on rejectStraySend, a beneficiary != self guard in Fund, and one corrected comment) recorded in mainnet/DIVERGENCE.json and verified as FAITHFUL WITH DECLARED DIVERGENCE by tools/namespace_tree.py. It ships INERT -- zero escrowed, zero claimable, zero fees, zero schedules, 0 bps fee.

Use it

A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.

Read it — no key, no account, no GNOT

gnokey query vm/qeval \
  --data 'gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting.EscrowTotal()' \
  --remote https://rpc.gno.land:443

EscrowTotal is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.

Change its state

Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 12 crossing functions, with the argument names filled in.

The gno.land mainnet builder spends real GNOT. Start with the read above, and size -gas-fee from a simulation rather than from any example.

Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.

Import it from another realm

import "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting"

The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.

Deployments

gno.land mainnet gnoland-1

RenderedSourceCall builderState

chain-attested
Import pathgno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting
Namespaceg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Deployed at height387,646
Deploy transactionc1c91a3892fb22a27fa99d7daec9b404a09bca522722ac6a80ed6c72c268fc31 look it up on the RPC
Deployerg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Gas used119,843,868
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml vesting.gno vesting_test.gno
Deployed bytesvesting.gno — 45,717 bytes
sha25641b6e343d7e5dd6a0d663311aecd24e85009bf95e5cbdd5ba88372c3c53927da
Realm addressg1qzwg4z3k4ypxvzx67vyvk8f8ff3y80azv93w0h confirmed against the realm itself

Do not take the hash above on trust. $download returns the bytes gnoland-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/vesting$download&file=vesting.gno' | shasum -a 256

Expected: 41b6e343d7e5dd6a0d663311aecd24e85009bf95e5cbdd5ba88372c3c53927da — 45,717 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 32 exported functions.

Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:

AcceptAdminBalanceOfEscrowTotalFeeBpsFeeOnFeesAccruedFundFundersWithOpenTranchesHeightHeldLiabilitiesMemoNumSchedulesOpenTrancheCountOpenTranchesOfFunderProposeAdminReleasableNowReleaseRenderRevokeRevokeAllSetFeeBpsSetFeeRecipientSurplusSweepDenomTrancheInfoTranchesOfUsersTotalVestedOfWithdrawWithdrawAllWithdrawFees

Overview

Realm vesting escrows native GNOT for a named beneficiary and releases it in DISCRETE, INDIVIDUALLY DATED TRANCHES — each payable exactly once, at or after its own due height.

DISCRETE, NOT LINEAR — read this first

Everywhere else in this ecosystem "vesting" means CONTINUOUS LINEAR ACCRUAL: a formula of the form amount * elapsed / duration, evaluated whenever someone asks. This realm does no such arithmetic. A funder states an explicit list of (delay, amount) pairs at funding time, and nothing accrues between them: a tranche is worth zero until its due height and its full amount from that height on.

That is a deliberate choice, not a simplification:

There is no default curve to be surprised by: every tranche is stated explicitly at creation, or the schedule does not exist.

COMPOSITION

This realm owns only the schedule state machine. Everything else is delegated to primitives already audited and deployed:

duebook — the entire schedule: due heights, and exactly-once release coinio — coin custody: escrow-in, payout, surplus sweep feeledger — claimable balances and the fee pot sanitize — funder-supplied free text, before it reaches markdown

It is duebook's FIRST PRODUCTIVE CONSUMER. duebook_demo is the falsification experiment and holds no coins by design; this realm is the first to put value behind the exactly-once claim.

LIFECYCLE

Fund (EOA + -send) : one transaction creates the schedule, escrows the exact total, and registers every tranche in duebook. There is no draft state and no half-funded schedule. Release (ANYONE) : once a tranche is due, anyone may release it. The value is credited to the RECORDED BENEFICIARY, never to the caller. Revoke (funder only) : returns a NOT-YET-DUE tranche to the funder, and only if the schedule was created revocable. A due tranche can never be revoked. Withdraw / WithdrawAll : pays out the caller's own claimable balance.

WHY NOTHING CAN BE TRAPPED, AND WHY THERE IS NO EXPIRY

Tranches are scheduled with duebook's ttl == 0, so they NEVER expire. Vested value cannot be lost by a beneficiary who is slow, offline, or gone. The sibling realms in this portfolio use a permissionless expiry valve to guarantee escrow is never trapped; here the valve is different and stronger: Release is PERMISSIONLESS, so every tranche becomes claimable-by-anyone the moment it is due, and the schedule drains itself without depending on any particular actor staying alive. Combined with "a due tranche can never be revoked", every ugnot in escrow has exactly one reachable destination and a height after which nobody can block it.

VESTED MEANS IRREVOCABLE, WITH NO RACE

Revoke refuses any tranche whose due height has arrived, even though duebook's Cancel would happily consume it. THAT CHECK IS THIS REALM'S, not duebook's. Without it a funder could watch for the due height and front-run the beneficiary's Release to claw back value already earned. Likewise, `revocable` is fixed at funding and has no setter: an irrevocable schedule cannot be made revocable later, which is the only thing that makes the promise worth anything.

AUTHORIZATION

Every identity is derived from the crossing entrypoint's cur.Previous().Address(). No function takes a caller identity as a parameter, so acting as another account is impossible by construction. Release is permissionless *because* it pays the recorded beneficiary: the caller's identity is not used to route value, so there is nothing for a stranger to gain.

HOW IT WIRES DUEBOOK CORRECTLY (duebook's five-point consumer contract)

  1. THE CLOCK IS runtime.ChainHeight(), never a call argument. A realm that lets a caller choose `now` has no delay at all.
  2. HEIGHTS ARE THE ONLY UNIT. No timestamp appears anywhere.
  3. THE *Book IS NEVER EXPORTED. It is an unexported package var and is never returned across a realm boundary; handing it out would hand out the right to schedule, cancel and claim.
  4. THE ACTOR IS AUTHORIZED from cur.Previous().Address(); the duebook owner recorded at Schedule is the funder, and duebook checks Cancel against that recorded value, never against an argument.
  5. THE EFFECT HAPPENS AFTER A SUCCESSFUL Claim, IN THE SAME TRANSACTION.

The deferral PAYLOAD is a human-readable label for events and rendering. THIS REALM NEVER PARSES IT. Every authoritative fact about a tranche — its amount, its schedule, its beneficiary — comes from this realm's own `tranches` tree, keyed by the deferral ID that duebook guarantees is never reused.

MONETARY INVARIANT (conservation)

Let H be ugnot held at this realm's address, E = escrowTotal (the sum of amounts over tranches still pending), U the ledger's claimable balances, F the fee pot, and S >= 0 out-of-band surplus:

H == E + U + F + S

Fund raises H and E together (coinio.Receive is the receipt-guaranteed shape). Release moves one tranche amount from E into U+F, with feeledger guaranteeing credited + fee == amount. Revoke moves a tranche amount from E into U, fee-free. Withdrawals debit the ledger BEFORE coinio.Payout moves the identical amount out (checks-effects-interactions). Any panic aborts the whole transaction. This realm never issues or removes coins. Surplus is recoverable only via SweepDenom, which reserves Liabilities().

ONLY GNOT

Fund rejects any envelope that is not exactly one positive ugnot coin. Foreign denominations sit in surplus and are sweepable.

Imports

Constants and variables

Tranche status values. A tranche is created pending and reaches exactly one terminal state; both terminals are frozen.

const (
	StatusPending  = "pending"
	StatusReleased = "released"
	StatusRevoked  = "revoked"
)

Bounds. All are compile-time constants: none is admin-mutable, so a schedule's shape cannot be changed under its funder or beneficiary.

const (
	// MinDelayBlocks is the floor on how far ahead the FIRST tranche may
	// fall. One block is enough to prove the delay is enforced while
	// keeping the realm exercisable on a live chain.
	MinDelayBlocks = int64(1)

	// MaxDelayBlocks caps the furthest tranche at roughly 10.8 years at
	// pearl-1's ~3.42s blocks. It also bounds now+delay far inside int64.
	MaxDelayBlocks = int64(100_000_000)

	// MaxTranches bounds one schedule. It bounds the Fund transaction's
	// work, the RevokeAll loop, and the schedule detail render.
	MaxTranches = 64

	// MaxOpenTranches caps pending tranches across EVERY schedule at once
	// — it is a whole-realm limit, not a per-schedule one, because it is
	// duebook's open-deferral cap. Fund fails when a new schedule would
	// cross it. Must be <= duebook.MaxOpenLimit.
	MaxOpenTranches = 5000

	// MaxOpenPerFunder bounds how many tranches ONE funder may hold
	// pending at once, so that MaxOpenTranches cannot be consumed by a
	// single account.
	//
	// Without this bound the whole-realm cap is a denial-of-service
	// surface that costs nothing to use: 79 Fund calls of 64 one-ugnot
	// tranches fill the realm, Fund then aborts for everybody, and the
	// attacker recovers 100% of the escrow fee-free with RevokeAll. Two
	// deliberate choices elsewhere remove every recovery path — ttl is
	// always 0, so duebook's permissionless Expire valve never opens, and
	// MaxOpenTranches is a compile-time constant with no admin setter. The
	// denial would therefore be total and permanent, which is out of all
	// proportion to its cost.
	//
	// 256 leaves one funder four full MaxTranches schedules open at once —
	// generous for the intended use — while exhausting the realm now takes
	// at least ceil(5000/256) = 20 separately funded accounts.
	//
	// That is a SYBIL BOUND and nothing more, so the residual is quantified
	// here rather than left to be inferred from the 20: the cap counts
	// TRANCHES, never value. parseSpec accepts amount == 1, so 256 pending
	// tranches cost 256ugnot and all 20 accounts together escrow about
	// 5000ugnot — every bit of it recoverable fee-free, since revokeOne
	// refunds at 0 bps. The real cost of the realm-wide denial is therefore
	// ~80 Fund transactions' gas and 20 funded addresses, NOT capital.
	// Anyone sizing the residual risk from the "20 accounts" figure alone
	// will size it wrong by orders of magnitude.
	//
	// Bounding the attack by VALUE instead would take a per-tranche minimum
	// amount. That is a change to what the application is — it would price
	// small legitimate tranches out — so it is a reviewer's decision, not a
	// routine fix, and it is deliberately not made here.
	MaxOpenPerFunder = 256

	// MaxTitleLen bounds a schedule title. Titles are charset-restricted
	// so they are list-safe in Render without escaping.
	MaxTitleLen = 80

	// MaxMemoLen bounds the free-text memo. It is a BYTE length, not a
	// rune count, because its job is to bound storage.
	MaxMemoLen = 1000

	// MaxSpecLen bounds the tranche specification string, so parsing cost
	// is bounded before any parsing begins.
	MaxSpecLen = 1400

	// MaxListed caps how many entries a single view or render returns.
	MaxListed = 50
)

Denom is the only asset this realm escrows.

const Denom = "ugnot"

MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%.

const MaxFeeBps = int64(1000)

Functions

AcceptAdmin

func AcceptAdmin(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as AcceptAdmin(cross(cur), ...).

AcceptAdmin completes a handover. Only the proposed account may call it.

BalanceOf

func BalanceOf(addr address) int64

BalanceOf returns addr's claimable balance (released tranches for a beneficiary, revocations for a funder).

EscrowTotal

func EscrowTotal() int64

EscrowTotal returns the escrow held by pending tranches (the E term).

FeeBps

func FeeBps() int64

FeeBps returns the fee that will be snapshotted into newly funded schedules. Existing schedules keep their own snapshot.

FeeOn

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

FeeOn previews the fee and net credit for a tranche of amount at the CURRENT FeeBps.

FeesAccrued

func FeesAccrued() int64

FeesAccrued returns the fee pot (the F term).

Fund

func Fund(cur realm, beneficiary address, title, memo, spec string, revocable bool, maxFeeBps int64) int64

Crossing function. Callable from a transaction via MsgCall, and from another realm as Fund(cross(cur), ...).

Fund creates a vesting schedule in a single transaction: it escrows the attached GNOT, registers every tranche with duebook, and returns the new schedule id. Only direct EOA calls with -send are accepted.

spec lists the tranches as "delay:amount" pairs separated by commas, for example "100:500,200:500,300:1000". Delays are in BLOCKS FROM NOW and must be STRICTLY INCREASING — two payments at the same height are one tranche with the amounts added. Every amount must be positive, and the amounts must sum to EXACTLY the attached send: this realm will not silently absorb a surplus or create an underfunded schedule.

revocable decides, permanently, whether the funder may reclaim not-yet-due tranches. There is no setter; an irrevocable schedule stays irrevocable.

maxFeeBps is the ceiling the caller signs for. Funding is rejected if the live fee exceeds it, which closes the race where an admin sequences a fee raise ahead of this transaction. Pass MaxFeeBps to accept any legal fee.

FundersWithOpenTranches

func FundersWithOpenTranches() int

FundersWithOpenTranches returns how many distinct funders hold at least one pending tranche. It is the width of the per-funder cap: exhausting MaxOpenTranches requires this to reach ceil(MaxOpenTranches/MaxOpenPerFunder).

Height

func Height() int64

Height returns the current chain height. Delays in Fund's spec are relative to the height at which Fund executes, so this is an aid for composing a spec, not an input to one.

Held

func Held() int64

Held returns the ugnot actually held at this realm's address (H).

Liabilities

func Liabilities() int64

Liabilities returns everything this realm owes: EscrowTotal + UsersTotal + FeesAccrued.

Memo

func Memo(id int64) string

Memo returns a schedule's raw memo text.

NumSchedules

func NumSchedules() int64

NumSchedules returns how many schedules have ever been funded.

OpenTrancheCount

func OpenTrancheCount() int

OpenTrancheCount returns how many tranches are pending across the whole realm, against the MaxOpenTranches cap.

OpenTranchesOfFunder

func OpenTranchesOfFunder(funder address) int64

OpenTranchesOfFunder returns how many tranches funder currently holds pending, against the MaxOpenPerFunder cap. A funder can read this before funding to see the headroom they have left.

ProposeAdmin

func ProposeAdmin(cur realm, next address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as ProposeAdmin(cross(cur), ...).

ProposeAdmin nominates a successor admin. The handover takes effect only when that account calls AcceptAdmin, so a typo cannot strand the role. Admin only; the zero address clears a pending proposal.

ReleasableNow

func ReleasableNow() string

ReleasableNow lists tranche ids that are due and still pending, up to MaxListed, newline-separated. Anyone may Release each of them.

Release

func Release(cur realm, trancheID int64) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as Release(cross(cur), ...).

Release pays out a due tranche. ANYONE may call it: the value is credited to the schedule's RECORDED BENEFICIARY, never to the caller, so a stranger calling this gains nothing and the beneficiary needs no liveness of their own. The protocol fee snapshotted at funding is charged here.

Aborts if the tranche does not exist, is not due yet, or has already been released or revoked — the last of those is duebook's guarantee, not a check written here.

Render

func Render(path string) string

Render shows the realm at "" and a schedule detail at "<id>". Free text (titles are charset-restricted; memos are not) passes through the ecosystem sanitizer before it reaches markdown.

Revoke

func Revoke(cur realm, trancheID int64) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as Revoke(cross(cur), ...).

Revoke returns a NOT-YET-DUE tranche to the funder's claimable balance, fee-free. Only the funder may call it, and only on a schedule created revocable.

A tranche whose due height has arrived can never be revoked, even though duebook's Cancel would consume it: that refusal is this realm's, and it is what stops a funder front-running the beneficiary's Release to claw back value already vested.

RevokeAll

func RevokeAll(cur realm, scheduleID int64) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as RevokeAll(cross(cur), ...).

RevokeAll returns every not-yet-due tranche of a schedule to the funder's claimable balance in one transaction, and returns how many were revoked. Already-due and already-settled tranches are skipped, not an error. Funder only, revocable schedules only. Aborts if nothing was revocable, so a no-op cannot be mistaken for success.

SetFeeBps

func SetFeeBps(cur realm, bps int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeBps(cross(cur), ...).

SetFeeBps sets the protocol fee snapshotted into FUTURE schedules at funding. Existing schedules keep the fee they were funded under, so an in-flight schedule's economics cannot be changed. Admin only.

SetFeeRecipient

func SetFeeRecipient(cur realm, next address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeRecipient(cross(cur), ...).

SetFeeRecipient re-points the fee/surplus role. The pot accrued under the OUTGOING recipient is settled to that recipient's own claimable balance first, so the successor inherits the role and not the earnings. Admin only; the zero address is rejected.

Surplus

func Surplus() int64

Surplus returns Held() - Liabilities() (the S term).

SweepDenom

func SweepDenom(cur realm, denom string) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as SweepDenom(cross(cur), ...).

SweepDenom sends the surplus of a single denomination to the fee recipient. For ugnot only the excess over Liabilities() moves — escrowed tranches and claimable balances are untouchable by construction. Other denominations move wholly. Only the fee recipient may call it.

TrancheInfo

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

TrancheInfo returns one tranche's fields by value. It answers for settled tranches too, after duebook has consumed the deferral.

TranchesOf

func TranchesOf(id int64) string

TranchesOf lists a schedule's tranches, newline-separated, one per line as "id seq amount dueAt status".

UsersTotal

func UsersTotal() int64

UsersTotal returns the sum of all claimable balances (the U term).

VestedOf

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

VestedOf returns, for a schedule, the amount whose due height has arrived and which is therefore no longer revocable — whether or not it has been released yet — alongside the amount still revocable by the funder.

Withdraw

func Withdraw(cur realm, amount int64) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as Withdraw(cross(cur), ...).

Withdraw sends amount ugnot of the caller's claimable balance (released tranches for a beneficiary, revocations for a funder) to the caller.

WithdrawAll

func WithdrawAll(cur realm) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as WithdrawAll(cross(cur), ...).

WithdrawAll sends the caller's entire claimable balance to the caller. Aborts if there is nothing to withdraw.

WithdrawFees

func WithdrawFees(cur realm) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as WithdrawFees(cross(cur), ...).

WithdrawFees sends the accrued fee pot to the fee recipient. Only the fee recipient may call it.


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importschain, chain/runtime, chain/runtime/unsafe, gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio, gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/duebook, gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv, strings
First-party dependenciesgno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio, gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/duebook, gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source

Source filemainnet/r/vesting/vesting.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical