cliffvesting_test.gno
5.46 Kb · 164 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}