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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}