Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

cliffvesting_test.gno

7.48 Kb · 216 lines
  1package cliffvesting
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/nt/uassert/v0"
  7)
  8
  9func mustNew(t *testing.T, total, start, cliff, end int64) Schedule {
 10	s, err := New(total, start, cliff, end)
 11	uassert.NoError(t, err)
 12	return s
 13}
 14
 15func TestNewValidation(t *testing.T) {
 16	_, err := New(0, 0, 0, 100)
 17	uassert.ErrorIs(t, err, ErrBadTotal)
 18	_, err = New(-5, 0, 0, 100)
 19	uassert.ErrorIs(t, err, ErrBadTotal)
 20
 21	_, err = New(100, 50, 50, 50)
 22	uassert.ErrorIs(t, err, ErrBadDuration, "end must be after start")
 23	_, err = New(100, 50, 50, 10)
 24	uassert.ErrorIs(t, err, ErrBadDuration)
 25
 26	_, err = New(100, 0, 200, 100)
 27	uassert.ErrorIs(t, err, ErrCliffAfter)
 28
 29	_, err = New(100, 50, 10, 100)
 30	uassert.ErrorIs(t, err, ErrCliffBefore)
 31
 32	_, err = New(100, 0, 100, 100)
 33	uassert.NoError(t, err, "a cliff exactly at the end is allowed")
 34}
 35
 36func TestNewLinearHasNoCliff(t *testing.T) {
 37	s, err := NewLinear(1000, 0, 100)
 38	uassert.NoError(t, err)
 39	uassert.False(t, s.HasCliff())
 40	uassert.Equal(t, int64(0), s.CliffAmount())
 41	uassert.Equal(t, int64(500), s.Vested(50), "linear from the start")
 42}
 43
 44// TestNothingBeforeCliff is the defining behaviour.
 45func TestNothingBeforeCliff(t *testing.T) {
 46	s := mustNew(t, 1200, 0, 300, 1200)
 47
 48	uassert.Equal(t, int64(0), s.Vested(0))
 49	uassert.Equal(t, int64(0), s.Vested(299), "one tick before the cliff, still nothing")
 50	uassert.Equal(t, int64(1200), s.Unvested(299))
 51}
 52
 53// TestCliffReleasesElapsedPortionAtOnce pins the step, not a ramp.
 54func TestCliffReleasesElapsedPortionAtOnce(t *testing.T) {
 55	s := mustNew(t, 1200, 0, 300, 1200)
 56
 57	uassert.Equal(t, int64(300), s.Vested(300), "25% of the term elapsed, released in one step")
 58	uassert.Equal(t, int64(300), s.CliffAmount())
 59	uassert.Equal(t, int64(25), s.PercentVested(300))
 60}
 61
 62func TestLinearAfterCliff(t *testing.T) {
 63	s := mustNew(t, 1200, 0, 300, 1200)
 64
 65	uassert.Equal(t, int64(600), s.Vested(600))
 66	uassert.Equal(t, int64(900), s.Vested(900))
 67	uassert.Equal(t, int64(1199), s.Vested(1199))
 68}
 69
 70// TestExactlyTotalAtEnd is what rounding down must never break.
 71func TestExactlyTotalAtEnd(t *testing.T) {
 72	s := mustNew(t, 1000, 0, 0, 3) // 1000/3 does not divide evenly
 73
 74	uassert.Equal(t, int64(333), s.Vested(1))
 75	uassert.Equal(t, int64(666), s.Vested(2))
 76	uassert.Equal(t, int64(1000), s.Vested(3), "the last instalment collects the remainder")
 77	uassert.Equal(t, int64(0), s.Unvested(3))
 78	uassert.Equal(t, int64(1000), s.Vested(99999), "still exactly total long after the end")
 79}
 80
 81// TestSmallTotalLongDuration is the rounding bug this package exists to avoid:
 82// with total < duration, dividing before multiplying truncates the rate to 0
 83// and nothing ever vests.
 84func TestSmallTotalLongDuration(t *testing.T) {
 85	s := mustNew(t, 7, 0, 0, 1000)
 86
 87	uassert.Equal(t, int64(0), s.Vested(100), "7*100/1000 = 0.7, floors to 0")
 88	uassert.Equal(t, int64(1), s.Vested(150), "7*150/1000 = 1.05, floors to 1")
 89	uassert.Equal(t, int64(3), s.Vested(500), "half the term, half of 7, floored")
 90	uassert.Equal(t, int64(7), s.Vested(1000), "and still exactly total at the end")
 91}
 92
 93func TestVestedIsMonotonic(t *testing.T) {
 94	s := mustNew(t, 997, 10, 40, 310)
 95
 96	prev := int64(-1)
 97	for tick := int64(0); tick <= 320; tick++ {
 98		v := s.Vested(tick)
 99		uassert.True(t, v >= prev, "vested must never decrease")
100		uassert.True(t, v <= s.Total, "vested must never exceed total")
101		prev = v
102	}
103}
104
105func TestUnvestedComplementsVested(t *testing.T) {
106	s := mustNew(t, 500, 0, 100, 400)
107	for _, tick := range []int64{0, 99, 100, 250, 399, 400, 500} {
108		uassert.Equal(t, s.Total, s.Vested(tick)+s.Unvested(tick),
109			"vested + unvested must always equal total")
110	}
111}
112
113func TestClaimable(t *testing.T) {
114	s := mustNew(t, 1000, 0, 0, 100)
115
116	uassert.Equal(t, int64(500), s.Claimable(50, 0))
117	uassert.Equal(t, int64(300), s.Claimable(50, 200), "already-claimed is deducted")
118	uassert.Equal(t, int64(0), s.Claimable(50, 500), "nothing left right now")
119	uassert.Equal(t, int64(500), s.Claimable(100, 500), "the rest at the end")
120}
121
122// TestClaimableNeverNegative guards the case where bookkeeping says more was
123// claimed than has vested — the caller gets 0, not a negative payout.
124func TestClaimableNeverNegative(t *testing.T) {
125	s := mustNew(t, 1000, 0, 0, 100)
126	uassert.Equal(t, int64(0), s.Claimable(10, 999))
127}
128
129func TestPercentVested(t *testing.T) {
130	s := mustNew(t, 1000, 0, 0, 100)
131	uassert.Equal(t, int64(0), s.PercentVested(0))
132	uassert.Equal(t, int64(50), s.PercentVested(50))
133	uassert.Equal(t, int64(99), s.PercentVested(99))
134	uassert.Equal(t, int64(100), s.PercentVested(100))
135	uassert.Equal(t, int64(100), s.PercentVested(1000))
136}
137
138// TestNonZeroStart checks the schedule is relative to Start, not to zero.
139func TestNonZeroStart(t *testing.T) {
140	s := mustNew(t, 400, 1000, 1100, 1400)
141
142	uassert.Equal(t, int64(0), s.Vested(999), "before the start")
143	uassert.Equal(t, int64(0), s.Vested(1099), "before the cliff")
144	uassert.Equal(t, int64(100), s.Vested(1100), "cliff: a quarter elapsed")
145	uassert.Equal(t, int64(200), s.Vested(1200))
146	uassert.Equal(t, int64(400), s.Vested(1400))
147}
148
149func TestDurationAndFlags(t *testing.T) {
150	s := mustNew(t, 100, 10, 20, 110)
151	uassert.Equal(t, int64(100), s.Duration())
152	uassert.True(t, s.HasCliff())
153	uassert.False(t, s.IsFullyVested(109))
154	uassert.True(t, s.IsFullyVested(110))
155	uassert.True(t, s.IsFullyVested(999))
156}
157
158// TestCliffAtEndIsAllOrNothing covers the degenerate schedule.
159func TestCliffAtEndIsAllOrNothing(t *testing.T) {
160	s := mustNew(t, 100, 0, 100, 100)
161	uassert.Equal(t, int64(0), s.Vested(99))
162	uassert.Equal(t, int64(100), s.Vested(100))
163	uassert.Equal(t, int64(100), s.CliffAmount())
164}
165
166// TestVestedDoesNotOverflow pins the defect that forced v1.
167//
168// v0 computed total*elapsed in plain int64. Over a term measured in seconds,
169// the product passes 2^63 for any grant above roughly 146,036 whole coins, and
170// the wrap is silent: the figures below came back NEGATIVE, which every caller
171// then treated as a real vested amount.
172func TestVestedDoesNotOverflow(t *testing.T) {
173	// The real gno.land mainnet vesting term, 2026-09-12 to 2028-09-12.
174	const (
175		start int64 = 1789225200
176		end   int64 = 1852383600
177		term        = end - start // 63,158,400 seconds
178	)
179
180	for _, tc := range []struct {
181		name  string
182		total int64
183	}{
184		{"just under the v0 ceiling", 146035000000},
185		{"just over the v0 ceiling", 146037000000},
186		{"the largest genesis grant", 318720000000000},
187		{"the whole genesis allocation", 1332999998328067},
188	} {
189		s, err := NewLinear(tc.total, start, end)
190		uassert.NoError(t, err, tc.name)
191
192		// Halfway through the term is exactly half the grant, and the naive
193		// int64 product has long since wrapped by this size.
194		uassert.Equal(t, tc.total/2, s.Vested(start+term/2), tc.name)
195
196		// The curve stays inside its bounds and never goes backwards.
197		prev := int64(0)
198		for i := int64(0); i <= 8; i++ {
199			got := s.Vested(start + term*i/8)
200			uassert.True(t, got >= prev, tc.name+": vested went backwards")
201			uassert.True(t, got >= 0, tc.name+": vested went negative")
202			uassert.True(t, got <= tc.total, tc.name+": vested exceeded the total")
203			prev = got
204		}
205		uassert.Equal(t, tc.total, prev, tc.name)
206
207		// PercentVested multiplies by 100 on top, so it has its own ceiling.
208		// It is 49 and not 50 at halfway for an ODD total: Vested floors to
209		// (total-1)/2 and the percentage floors again, so two roundings in the
210		// same direction land just under. That is the documented behaviour,
211		// not the overflow, so the assertion allows both.
212		half := s.PercentVested(start + term/2)
213		uassert.True(t, half == 49 || half == 50, tc.name+": halfway percent left 49..50")
214		uassert.Equal(t, int64(100), s.PercentVested(end), tc.name)
215	}
216}