Transaction

FB719F05492A05…FBA075C63539

Block 25,928 · index 0 · indexed

Summary

Hash
FB719F05492A051CBAF46154F0C8F4FA3554F75CA1B7F4E60386FBA075C63539
Block
25,928
Size
7051 bytes
Gas used
11,082,295 / 13,298,814
Fee
1000000ugnot
Memo
gnopublish
Status
success

Messages

Arguments · 9

  1. #1cliffvesting
  2. #2README.md
  3. #3# `gno.land/p/moul/x/daily/cliffvesting/v0` **Cliff-then-linear vesting calculator** — `New`, `NewLinear`, `Vested`, `Unvested`, `Claimable`, `PercentVested`, `CliffAmount`, `IsFullyVested`, `Duration`, `HasCliff`. ```go import "gno.land/p/moul/x/daily/cliffvesting/v0" s, _ := cliffvesting.New(1200, 0, 3, 12) // total, start, cliff, end s.Vested(2) // 0 — before the cliff s.Vested(3) // 300 — the cliff releases the elapsed portion at once s.Vested(12) // 1200 s.Claimable(6, 300) // 300 — vested minus already claimed ``` **Pure.** No state, no balances, no transfers. The arithmetic is the part that is easy to get subtly wrong and easy to test; custody belongs to the realm holding the coins. The cliff is a **step, not a ramp**: nothing vests before it, then the whole elapsed portion unlocks at once, and the rest accrues linearly. **All integer arithmetic** — no float ever reaches consensus state: - `total*elapsed/duration`, multiplying **first**. The reverse computes a per-tick rate that truncates to zero whenever `total < duration`, so nothing would ever vest. That is the classic vesting bug; it has its own test (7 tokens over 1000 ticks). - Rounding is **down**, so a beneficiary is never paid more than they earned, and the **final instalment collects the remainder**: 1000 over 3 periods is `333 + 333 + 334`, and `Vested(end)` is exactly `total`, never `total-1`. - `Claimable` never returns negative, even if the caller's bookkeeping says more was claimed than has vested. Times are `int64`, so the caller may use block heights or unix seconds — the unit only has to be consistent. **Live demo:** [`r/moul/x/daily/cliffvestingdemo`](https://github.com/moul/gno-contracts/tree/main/r/moul/x/daily/cliffvestingdemo/v0) · render it at [`/r/moul/x/daily/cliffvestingdemo/v0`](https://gno.land/r/moul/x/daily/cliffvestingdemo/v0). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4cliffvesting.gno
  5. #5// Package cliffvesting computes cliff-then-linear vesting schedules as a PURE // calculator — no state, no balances, no transfers. // // The shape is the standard employee/token grant: nothing vests until the // cliff, the cliff releases the whole elapsed portion at once, and the rest // accrues linearly until the end of the term. Keeping it pure is deliberate: // the arithmetic is the part that is easy to get subtly wrong and easy to test, // while custody belongs to the realm holding the coins. // // Everything is integer arithmetic. Vested is computed as // total*elapsed/duration with the multiplication FIRST, so the usual rounding // bug — dividing before multiplying and truncating the rate to zero — cannot // happen. Rounding is always DOWN, which means the beneficiary never receives // more than they have earned and the final instalment collects the remainder; // at t >= end the result is exactly total, never total-1. // // Times are int64 so the caller can use block heights or unix seconds. The unit // only has to be consistent. // // A live demo of this package is at // [r/moul/x/daily/cliffvestingdemo](/r/moul/x/daily/cliffvestingdemo/v0). package cliffvesting import "errors" var ( ErrBadTotal = errors.New("cliffvesting: total must be positive") ErrBadDuration = errors.New("cliffvesting: duration must be positive") ErrCliffAfter = errors.New("cliffvesting: cliff must not fall after the end") ErrCliffBefore = errors.New("cliffvesting: cliff must not fall before the start") ) // Schedule is a cliff-then-linear vesting plan. Construct with New so the // invariants are checked once. type Schedule struct { Total int64 // total amount to vest Start int64 // vesting begins Cliff int64 // nothing is claimable before this End int64 // fully vested at or after this } // New validates and returns a Schedule. cliff must lie within [start, end]. // Passing cliff == start means "no cliff". func New(total, start, cliff, end int64) (Schedule, error) { s := Schedule{Total: total, Start: start, Cliff: cliff, End: end} if total <= 0 { return Schedule{}, ErrBadTotal } if end <= start { return Schedule{}, ErrBadDuration } if cliff > end { return Schedule{}, ErrCliffAfter } if cliff < start { return Schedule{}, ErrCliffBefore } return s, nil } // NewLinear is New with no cliff. func NewLinear(total, start, end int64) (Schedule, error) { return New(total, start, start, end) } // Duration returns the length of the vesting term. func (s Schedule) Duration() int64 { return s.End - s.Start } // HasCliff reports whether the schedule has a non-trivial cliff. func (s Schedule) HasCliff() bool { return s.Cliff > s.Start } // Vested returns how much has vested at time t. Zero before the cliff, exactly // Total at or after End, and floor(total*elapsed/duration) in between. func (s Schedule) Vested(t int64) int64 { if t < s.Cliff || t < s.Start { return 0 } if t >= s.End { return s.Total } elapsed := t - s.Start // Multiply BEFORE dividing: the reverse truncates the per-tick rate to // zero whenever total < duration, which is the classic vesting bug. return s.Total * elapsed / s.Duration() } // Unvested returns the remainder still locked at time t. func (s Schedule) Unvested(t int64) int64 { return s.Total - s.Vested(t) } // Claimable returns what can be withdrawn at time t given how much has already // been claimed. Never negative, even if claimed somehow exceeds vested. func (s Schedule) Claimable(t, claimed int64) int64 { v := s.Vested(t) - claimed if v < 0 { return 0 } return v } // IsFullyVested reports whether the term has completed at time t. func (s Schedule) IsFullyVested(t int64) bool { return t >= s.End } // PercentVested returns the vested share at t as an integer percentage, // rounded down. Integer-only: no floats reach consensus state. func (s Schedule) PercentVested(t int64) int64 { return s.Vested(t) * 100 / s.Total } // CliffAmount returns the lump sum released the instant the cliff is reached. func (s Schedule) CliffAmount() int64 { return s.Vested(s.Cliff) }
  6. #6gnomod.toml
  7. #7module = "gno.land/p/moul/x/daily/cliffvesting/v0" gno = "0.9"
  8. #8/gno.MemPackageType
  9. #9 MPUserProd

Result log

msg:0,success:true,log:,events:[]

← Back to block 25,928