vesting_test.gno
7.46 Kb · 213 lines
1package vesting
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9// The real mainnet §132 term: 2026-09-12T15:00:00Z to 2028-09-12T15:00:00Z.
10const (
11 mainnetStart int64 = 1789225200
12 mainnetEnd int64 = 1852383600
13 mainnetTerm = mainnetEnd - mainnetStart // 63,158,400 seconds
14)
15
16func TestNewValidates(t *testing.T) {
17 for _, tc := range []struct {
18 name string
19 original, start, end int64
20 typ Type
21 wantErr error
22 }{
23 {"a real mainnet schedule", 106560000000, mainnetStart, mainnetEnd, Continuous, nil},
24 {"no schedule at all", 0, 0, 0, Continuous, nil},
25 {"zero original ignores the rest", 0, 99, 1, Continuous, nil},
26 {"a cliff needs no start", 100, 0, mainnetEnd, Delayed, nil},
27 {"negative original", -1, 0, 10, Continuous, ErrNegativeOriginal},
28 {"end not positive", 100, 0, 0, Continuous, ErrEndNotPositive},
29 {"negative start", 100, -1, 10, Continuous, ErrNegativeStart},
30 {"start equal to end", 100, 10, 10, Continuous, ErrStartAfterEnd},
31 {"start after end", 100, 11, 10, Continuous, ErrStartAfterEnd},
32 } {
33 _, err := New(tc.original, tc.start, tc.end, tc.typ)
34 if tc.wantErr == nil {
35 uassert.NoError(t, err, tc.name)
36 continue
37 }
38 uassert.ErrorIs(t, err, tc.wantErr, tc.name)
39 }
40}
41
42// TestVestedCurve pins the continuous curve at its boundaries and midpoint.
43func TestVestedCurve(t *testing.T) {
44 s, err := New(1000, 100, 200, Continuous)
45 uassert.NoError(t, err)
46
47 for _, tc := range []struct {
48 now, want int64
49 }{
50 {0, 0}, // long before
51 {100, 0}, // at the start
52 {125, 250}, // a quarter in
53 {150, 500}, // halfway
54 {199, 990}, // one tick short
55 {200, 1000}, // exactly at the end
56 {1 << 40, 1000},
57 } {
58 uassert.Equal(t, tc.want, s.Vested(tc.now))
59 uassert.Equal(t, 1000-tc.want, s.Locked(tc.now))
60 }
61}
62
63// TestDelayedIsACliff: Start is ignored and nothing vests until End.
64func TestDelayedIsACliff(t *testing.T) {
65 s, err := New(1000, 0, 200, Delayed)
66 uassert.NoError(t, err)
67
68 uassert.Equal(t, int64(0), s.Vested(0))
69 uassert.Equal(t, int64(0), s.Vested(199))
70 uassert.Equal(t, int64(1000), s.Vested(200))
71 uassert.Equal(t, int64(1000), s.Vested(1000))
72 uassert.Equal(t, int64(1000), s.Locked(199))
73 uassert.Equal(t, int64(0), s.Locked(200))
74}
75
76func TestZeroScheduleLocksNothing(t *testing.T) {
77 var s Schedule
78 uassert.True(t, s.IsZero())
79 uassert.Equal(t, int64(0), s.Vested(12345))
80 uassert.Equal(t, int64(0), s.Locked(12345))
81 uassert.Equal(t, int64(999), s.Spendable(999, 12345))
82 uassert.Equal(t, int64(1000), s.PermilleVested(12345))
83}
84
85// TestRoundsDown is the direction that matters: reporting one ugnot more than
86// the chain will move turns a page into a lie a user acts on.
87func TestRoundsDown(t *testing.T) {
88 s, err := New(10, 0, 3, Continuous) // 10/3 per second, never exact
89 uassert.NoError(t, err)
90
91 uassert.Equal(t, int64(3), s.Vested(1)) // 3.33 -> 3
92 uassert.Equal(t, int64(6), s.Vested(2)) // 6.66 -> 6
93 uassert.Equal(t, int64(10), s.Vested(3))
94}
95
96// TestNoOverflowAtChainScale is the whole reason this package exists rather
97// than reusing p/moul/x/daily/cliffvesting, which computes total*elapsed in
98// plain int64. Over the mainnet term that wraps above 146,036 GNOT: the
99// largest genesis grant is 318,720,000 GNOT, and the naive form returns a
100// NEGATIVE vested amount for it.
101func TestNoOverflowAtChainScale(t *testing.T) {
102 const largestGrant int64 = 318720000000000 // 318,720,000 GNOT in ugnot
103
104 s, err := New(largestGrant, mainnetStart, mainnetEnd, Continuous)
105 uassert.NoError(t, err)
106
107 // Exactly halfway through the term: the answer is half the grant, and the
108 // naive int64 product would have wrapped long before here.
109 half := s.Vested(mainnetStart + mainnetTerm/2)
110 uassert.Equal(t, largestGrant/2, half)
111 uassert.True(t, half > 0, "a wrapped product reports a negative amount")
112
113 // Every point on the curve stays inside [0, Original], monotonically.
114 prev := int64(0)
115 for i := int64(0); i <= 16; i++ {
116 got := s.Vested(mainnetStart + mainnetTerm*i/16)
117 uassert.True(t, got >= prev, "vested went backwards")
118 uassert.True(t, got >= 0 && got <= largestGrant, "vested left its bounds")
119 prev = got
120 }
121 uassert.Equal(t, largestGrant, prev)
122}
123
124// TestTotalSupplyGrantDoesNotOverflow takes the ceiling to the whole supply,
125// which is the largest schedule the chain could ever hold.
126func TestTotalSupplyGrantDoesNotOverflow(t *testing.T) {
127 const supply int64 = 1332999998328067 // total genesis allocation, ugnot
128
129 s, err := New(supply, mainnetStart, mainnetEnd, Continuous)
130 uassert.NoError(t, err)
131 uassert.Equal(t, supply/2, s.Vested(mainnetStart+mainnetTerm/2))
132 uassert.Equal(t, supply, s.Vested(mainnetEnd))
133}
134
135// TestSpendable is the number the page exists to show.
136func TestSpendable(t *testing.T) {
137 s, err := New(1000, 100, 200, Continuous)
138 uassert.NoError(t, err)
139
140 // Balance above the grant: the surplus is spendable from the start.
141 uassert.Equal(t, int64(200), s.Spendable(1200, 100)) // 1200 - 1000 locked
142 uassert.Equal(t, int64(700), s.Spendable(1200, 150)) // 1200 - 500 locked
143 uassert.Equal(t, int64(1200), s.Spendable(1200, 200))
144
145 // Spent down below what is still locked: nothing moves, and the answer is
146 // zero rather than a negative number.
147 uassert.Equal(t, int64(0), s.Spendable(400, 150))
148 uassert.Equal(t, int64(0), s.Spendable(500, 150)) // exactly locked
149 uassert.Equal(t, int64(1), s.Spendable(501, 150))
150
151 uassert.Equal(t, int64(0), s.Spendable(0, 150))
152 uassert.Equal(t, int64(0), s.Spendable(-5, 150))
153}
154
155// TestMoulsOwnAccount walks the real genesis row through the real schedule,
156// as a regression on the arithmetic the page will print.
157//
158// Genesis row, from the allocation sheet that built mainnet:
159//
160// g1manfred47kzduec920z88wfr64ylksmdcedlf5=111000000000ugnot;
161// vesting=106560000000ugnot,1789225200,1852383600
162func TestMoulsOwnAccount(t *testing.T) {
163 const (
164 genesisBalance int64 = 111000000000
165 grant int64 = 106560000000
166 )
167 s, err := New(grant, mainnetStart, mainnetEnd, Continuous)
168 uassert.NoError(t, err)
169
170 // At genesis: the 4% that never vested is immediately spendable.
171 uassert.Equal(t, int64(0), s.Vested(mainnetStart))
172 uassert.Equal(t, genesisBalance-grant, s.Spendable(genesisBalance, mainnetStart))
173 uassert.Equal(t, int64(4440000000), s.Spendable(genesisBalance, mainnetStart))
174
175 // Halfway: half the grant has vested.
176 uassert.Equal(t, grant/2, s.Vested(mainnetStart+mainnetTerm/2))
177
178 // At the end: everything moves, whatever the balance is by then.
179 uassert.Equal(t, int64(0), s.Locked(mainnetEnd))
180 uassert.Equal(t, genesisBalance, s.Spendable(genesisBalance, mainnetEnd))
181}
182
183func TestPermilleVested(t *testing.T) {
184 s, err := New(1000, 100, 200, Continuous)
185 uassert.NoError(t, err)
186
187 uassert.Equal(t, int64(0), s.PermilleVested(100))
188 uassert.Equal(t, int64(250), s.PermilleVested(125))
189 uassert.Equal(t, int64(1000), s.PermilleVested(200))
190
191 // Rounds down, like everything else here.
192 s2, err := New(3, 0, 300, Continuous)
193 uassert.NoError(t, err)
194 uassert.Equal(t, int64(333), s2.PermilleVested(100)) // 1/3 of 3 = 1 -> 333
195}
196
197func TestRemainingSeconds(t *testing.T) {
198 s, err := New(1000, 100, 200, Continuous)
199 uassert.NoError(t, err)
200
201 uassert.Equal(t, int64(100), s.RemainingSeconds(100))
202 uassert.Equal(t, int64(1), s.RemainingSeconds(199))
203 uassert.Equal(t, int64(0), s.RemainingSeconds(200))
204 uassert.Equal(t, int64(0), s.RemainingSeconds(10000))
205
206 var zero Schedule
207 uassert.Equal(t, int64(0), zero.RemainingSeconds(0))
208}
209
210func TestTypeString(t *testing.T) {
211 uassert.Equal(t, "continuous", Continuous.String())
212 uassert.Equal(t, "delayed", Delayed.String())
213}