const StatusPending, StatusReleased, StatusRevoked
Tranche status values. A tranche is created pending and reaches exactly one terminal state; both terminals are frozen.
Realm vesting escrows native GNOT for a named beneficiary and releases it in DISCRETE, INDIVIDUALLY DATED TRANCHES — ...
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.
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.
This realm owns only the schedule state machine. Everything else is delegated to primitives already audited and deployed:
1duebook — the entire schedule: due heights, and exactly-once release
2coinio — coin custody: escrow-in, payout, surplus sweep
3feeledger — claimable balances and the fee pot
4sanitize — 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.
1Fund (EOA + -send) : one transaction creates the schedule, escrows
2 the exact total, and registers every tranche
3 in duebook. There is no draft state and no
4 half-funded schedule.
5Release (ANYONE) : once a tranche is due, anyone may release it.
6 The value is credited to the RECORDED
7 BENEFICIARY, never to the caller.
8Revoke (funder only) : returns a NOT-YET-DUE tranche to the funder,
9 and only if the schedule was created
10 revocable. A due tranche can never be revoked.
11Withdraw / WithdrawAll : pays out the caller's own claimable balance.
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.
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.
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.
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.
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:
1H == 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().
Fund rejects any envelope that is not exactly one positive ugnot coin. Foreign denominations sit in surplus and are sweepable.
Tranche status values. A tranche is created pending and reaches exactly one terminal state; both terminals are frozen.
1const (
2 // MinDelayBlocks is the floor on how far ahead the FIRST tranche may
3 // fall. One block is enough to prove the delay is enforced while
4 // keeping the realm exercisable on a live chain.
5 MinDelayBlocks = int64(1)
6
7 // MaxDelayBlocks caps the furthest tranche at roughly 10.8 years at
8 // pearl-1's ~3.42s blocks. It also bounds now+delay far inside int64.
9 MaxDelayBlocks = int64(100_000_000)
10
11 // MaxTranches bounds one schedule. It bounds the Fund transaction's
12 // work, the RevokeAll loop, and the schedule detail render.
13 MaxTranches = 64
14
15 // MaxOpenTranches caps pending tranches across EVERY schedule at once
16 // — it is a whole-realm limit, not a per-schedule one, because it is
17 // duebook's open-deferral cap. Fund fails when a new schedule would
18 // cross it. Must be <= duebook.MaxOpenLimit.
19 MaxOpenTranches = 5000
20
21 // MaxOpenPerFunder bounds how many tranches ONE funder may hold
22 // pending at once, so that MaxOpenTranches cannot be consumed by a
23 // single account.
24 //
25 // Without this bound the whole-realm cap is a denial-of-service
26 // surface that costs nothing to use: 79 Fund calls of 64 one-ugnot
27 // tranches fill the realm, Fund then aborts for everybody, and the
28 // attacker recovers 100% of the escrow fee-free with RevokeAll. Two
29 // deliberate choices elsewhere remove every recovery path — ttl is
30 // always 0, so duebook's permissionless Expire valve never opens, and
31 // MaxOpenTranches is a compile-time constant with no admin setter. The
32 // denial would therefore be total and permanent, which is out of all
33 // proportion to its cost.
34 //
35 // 256 leaves one funder four full MaxTranches schedules open at once —
36 // generous for the intended use — while exhausting the realm now takes
37 // at least ceil(5000/256) = 20 separately funded accounts.
38 //
39 // That is a SYBIL BOUND and nothing more, so the residual is quantified
40 // here rather than left to be inferred from the 20: the cap counts
41 // TRANCHES, never value. parseSpec accepts amount == 1, so 256 pending
42 // tranches cost 256ugnot and all 20 accounts together escrow about
43 // 5000ugnot — every bit of it recoverable fee-free, since revokeOne
44 // refunds at 0 bps. The real cost of the realm-wide denial is therefore
45 // ~80 Fund transactions' gas and 20 funded addresses, NOT capital.
46 // Anyone sizing the residual risk from the "20 accounts" figure alone
47 // will size it wrong by orders of magnitude.
48 //
49 // Bounding the attack by VALUE instead would take a per-tranche minimum
50 // amount. That is a change to what the application is — it would price
51 // small legitimate tranches out — so it is a reviewer's decision, not a
52 // routine fix, and it is deliberately not made here.
53 MaxOpenPerFunder = 256
54
55 // MaxTitleLen bounds a schedule title. Titles are charset-restricted
56 // so they are list-safe in Render without escaping.
57 MaxTitleLen = 80
58
59 // MaxMemoLen bounds the free-text memo. It is a BYTE length, not a
60 // rune count, because its job is to bound storage.
61 MaxMemoLen = 1000
62
63 // MaxSpecLen bounds the tranche specification string, so parsing cost
64 // is bounded before any parsing begins.
65 MaxSpecLen = 1400
66
67 // MaxListed caps how many entries a single view or render returns.
68 MaxListed = 50
69)Bounds. All are compile-time constants: none is admin-mutable, so a schedule's shape cannot be changed under its funder or beneficiary.
Denom is the only asset this realm escrows.
MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%.
AcceptAdmin completes a handover. Only the proposed account may call it.
BalanceOf returns addr's claimable balance (released tranches for a beneficiary, revocations for a funder).
EscrowTotal returns the escrow held by pending tranches (the E term).
FeeBps returns the fee that will be snapshotted into newly funded schedules. Existing schedules keep their own snapshot.
FeeOn previews the fee and net credit for a tranche of amount at the CURRENT FeeBps.
FeesAccrued returns the fee pot (the F term).
1func Fund(cur realm, beneficiary address, title, memo, spec string, revocable bool, maxFeeBps int64) int64Fund 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 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 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 returns the ugnot actually held at this realm's address (H).
Liabilities returns everything this realm owes: EscrowTotal + UsersTotal + FeesAccrued.
Memo returns a schedule's raw memo text.
NumSchedules returns how many schedules have ever been funded.
OpenTrancheCount returns how many tranches are pending across the whole realm, against the MaxOpenTranches cap.
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 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 lists tranche ids that are due and still pending, up to MaxListed, newline-separated. Anyone may Release each of them.
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 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 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 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 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 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 returns Held() - Liabilities() (the S term).
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.
1func 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 lists a schedule's tranches, newline-separated, one per line as "id seq amount dueAt status".
UsersTotal returns the sum of all claimable balances (the U term).
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 sends amount ugnot of the caller's claimable balance (released tranches for a beneficiary, revocations for a funder) to the caller.
WithdrawAll sends the caller's entire claimable balance to the caller. Aborts if there is nothing to withdraw.
WithdrawFees sends the accrued fee pot to the fee recipient. Only the fee recipient may call it.