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.

feeledger_test.gno

9.85 Kb · 330 lines
  1package feeledger
  2
  3import "testing"
  4
  5const (
  6	maxInt64 = int64(1<<63 - 1)
  7	minInt64 = int64(-1 << 63)
  8)
  9
 10// mustPanicWith asserts fn panics with an error whose message is want.
 11// Non-crossing panics in the same package are recoverable.
 12func mustPanicWith(t *testing.T, want string, fn func()) {
 13	t.Helper()
 14	defer func() {
 15		r := recover()
 16		if r == nil {
 17			t.Fatalf("expected panic %q, got none", want)
 18		}
 19		err, ok := r.(error)
 20		if !ok || err.Error() != want {
 21			t.Fatalf("panic = %v, want %q", r, want)
 22		}
 23	}()
 24	fn()
 25}
 26
 27func TestNewBounds(t *testing.T) {
 28	for _, bad := range []int64{-1, BpsDenominator + 1, minInt64, maxInt64} {
 29		if _, err := New(bad); err != ErrInvalidBps {
 30			t.Fatalf("New(%d) err = %v, want ErrInvalidBps", bad, err)
 31		}
 32	}
 33	for _, ok := range []int64{0, 1, BpsDenominator} {
 34		if _, err := New(ok); err != nil {
 35			t.Fatalf("New(%d) err = %v", ok, err)
 36		}
 37	}
 38	mustPanicWith(t, ErrInvalidBps.Error(), func() { MustNew(-1) })
 39}
 40
 41func TestFeeForRounding(t *testing.T) {
 42	// floor(max * 0.9999) computed the same overflow-free way feeFor does.
 43	wantMaxAt9999 := maxInt64/10000*9999 + maxInt64%10000*9999/10000
 44	cases := []struct {
 45		amount, bps, want int64
 46	}{
 47		{999, 100, 9},      // 9.99 floors to 9
 48		{10000, 1, 1},      // exact
 49		{9999, 1, 0},       // 0.9999 floors to 0 — no minimum fee
 50		{1, 9999, 0},       // 0.9999 floors to 0
 51		{10, 9999, 9},      // 9.999 floors to 9
 52		{100, 10000, 100},  // full-fee bps: fee == amount
 53		{0, 500, 0},        // zero amount previews zero fee
 54		{1000, 0, 0},       // zero-fee config
 55		{1000, 250, 25},    // exact 2.5%
 56		{999, 250, 24},     // 24.975 floors to 24
 57		{maxInt64, 10000, maxInt64}, // no intermediate overflow
 58		{maxInt64, 9999, wantMaxAt9999},
 59	}
 60	for _, c := range cases {
 61		got, err := FeeFor(c.amount, c.bps)
 62		if err != nil {
 63			t.Fatalf("FeeFor(%d,%d) err = %v", c.amount, c.bps, err)
 64		}
 65		if got != c.want {
 66			t.Fatalf("FeeFor(%d,%d) = %d, want %d", c.amount, c.bps, got, c.want)
 67		}
 68		if got < 0 || got > c.amount {
 69			t.Fatalf("FeeFor(%d,%d) = %d out of [0,amount]", c.amount, c.bps, got)
 70		}
 71	}
 72	if _, err := FeeFor(-1, 100); err != ErrInvalidAmount {
 73		t.Fatalf("FeeFor(-1,100) err = %v, want ErrInvalidAmount", err)
 74	}
 75	if _, err := FeeFor(100, -1); err != ErrInvalidBps {
 76		t.Fatalf("FeeFor(100,-1) err = %v, want ErrInvalidBps", err)
 77	}
 78	if _, err := FeeFor(100, BpsDenominator+1); err != ErrInvalidBps {
 79		t.Fatalf("FeeFor over-bps err = %v, want ErrInvalidBps", err)
 80	}
 81}
 82
 83func TestDepositValidation(t *testing.T) {
 84	l := MustNew(500)
 85	type bad struct {
 86		account     string
 87		amount, bps int64
 88		want        error
 89	}
 90	for _, c := range []bad{
 91		{"", 100, 0, ErrEmptyAccount},
 92		{"alice", 0, 0, ErrInvalidAmount},
 93		{"alice", -7, 0, ErrInvalidAmount},
 94		{"alice", minInt64, 0, ErrInvalidAmount},
 95		{"alice", 100, -1, ErrInvalidBps},
 96		{"alice", 100, 501, ErrInvalidBps}, // above the ledger cap
 97	} {
 98		if _, _, err := l.Deposit(c.account, c.amount, c.bps); err != c.want {
 99			t.Fatalf("Deposit(%q,%d,%d) err = %v, want %v", c.account, c.amount, c.bps, err, c.want)
100		}
101	}
102	if l.UsersTotal() != 0 || l.FeesAccrued() != 0 || l.Accounts() != 0 {
103		t.Fatal("failed deposits mutated state")
104	}
105}
106
107func TestDepositAndAccumulate(t *testing.T) {
108	l := MustNew(1000)
109	credited, fee, err := l.Deposit("alice", 1000, 250)
110	if err != nil || credited != 975 || fee != 25 {
111		t.Fatalf("Deposit = (%d,%d,%v), want (975,25,nil)", credited, fee, err)
112	}
113	credited, fee, err = l.Deposit("alice", 999, 250)
114	if err != nil || credited != 975 || fee != 24 { // fee floors, credit rounds up
115		t.Fatalf("Deposit#2 = (%d,%d,%v), want (975,24,nil)", credited, fee, err)
116	}
117	if got := l.BalanceOf("alice"); got != 1950 {
118		t.Fatalf("BalanceOf = %d, want 1950", got)
119	}
120	if l.UsersTotal() != 1950 || l.FeesAccrued() != 49 || l.Liabilities() != 1999 {
121		t.Fatalf("totals = %d/%d/%d", l.UsersTotal(), l.FeesAccrued(), l.Liabilities())
122	}
123	// credited + fee always reassembles both deposits exactly.
124	if l.Liabilities() != 1000+999 {
125		t.Fatal("conservation broken: credited+fee != deposited")
126	}
127}
128
129func TestFullFeeBpsCreditsZero(t *testing.T) {
130	l := MustNew(BpsDenominator)
131	credited, fee, err := l.Deposit("alice", 777, BpsDenominator)
132	if err != nil || credited != 0 || fee != 777 {
133		t.Fatalf("Deposit = (%d,%d,%v), want (0,777,nil)", credited, fee, err)
134	}
135	if l.Accounts() != 0 || l.BalanceOf("alice") != 0 {
136		t.Fatal("zero-credit deposit must not create an account entry")
137	}
138	if l.FeesAccrued() != 777 || l.UsersTotal() != 0 {
139		t.Fatal("fee pot mismatch on full-fee deposit")
140	}
141}
142
143func TestDepositOverflow(t *testing.T) {
144	l := MustNew(BpsDenominator)
145	if _, _, err := l.Deposit("alice", maxInt64, 0); err != nil {
146		t.Fatalf("seed deposit err = %v", err)
147	}
148	// Account balance would overflow.
149	if _, _, err := l.Deposit("alice", 1, 0); err != ErrOverflow {
150		t.Fatalf("balance-overflow err = %v, want ErrOverflow", err)
151	}
152	// Joint liabilities (users + fees) would overflow even though the
153	// fee pot alone would not.
154	if _, _, err := l.Deposit("bob", 100, BpsDenominator); err != ErrOverflow {
155		t.Fatalf("liabilities-overflow err = %v, want ErrOverflow", err)
156	}
157	if l.BalanceOf("bob") != 0 || l.FeesAccrued() != 0 || l.UsersTotal() != maxInt64 {
158		t.Fatal("failed overflow deposits mutated state")
159	}
160}
161
162func TestWithdraw(t *testing.T) {
163	l := MustNew(0)
164	l.MustDeposit("alice", 1000, 0)
165
166	if err := l.Withdraw("", 10); err != ErrEmptyAccount {
167		t.Fatalf("empty account err = %v", err)
168	}
169	if err := l.Withdraw("alice", 0); err != ErrInvalidAmount {
170		t.Fatalf("zero amount err = %v", err)
171	}
172	if err := l.Withdraw("alice", -3); err != ErrInvalidAmount {
173		t.Fatalf("negative amount err = %v", err)
174	}
175	if err := l.Withdraw("alice", 1001); err != ErrInsufficient {
176		t.Fatalf("over-balance err = %v", err)
177	}
178	if err := l.Withdraw("ghost", 1); err != ErrInsufficient {
179		t.Fatalf("absent account err = %v", err)
180	}
181	if l.BalanceOf("alice") != 1000 || l.UsersTotal() != 1000 {
182		t.Fatal("failed withdrawals mutated state")
183	}
184
185	if err := l.Withdraw("alice", 400); err != nil {
186		t.Fatalf("partial withdraw err = %v", err)
187	}
188	if l.BalanceOf("alice") != 600 || l.UsersTotal() != 600 || l.Accounts() != 1 {
189		t.Fatal("partial withdraw wrong state")
190	}
191	if err := l.Withdraw("alice", 600); err != nil {
192		t.Fatalf("exact withdraw err = %v", err)
193	}
194	if l.BalanceOf("alice") != 0 || l.UsersTotal() != 0 || l.Accounts() != 0 {
195		t.Fatal("exact withdraw must remove the account entry")
196	}
197	if err := l.Withdraw("alice", 1); err != ErrInsufficient {
198		t.Fatalf("repeat withdraw err = %v, want ErrInsufficient", err)
199	}
200	mustPanicWith(t, ErrInsufficient.Error(), func() { l.MustWithdraw("alice", 1) })
201}
202
203func TestWithdrawAll(t *testing.T) {
204	l := MustNew(0)
205	if got, err := l.WithdrawAll("nobody"); err != nil || got != 0 {
206		t.Fatalf("WithdrawAll(nobody) = (%d,%v), want (0,nil)", got, err)
207	}
208	if _, err := l.WithdrawAll(""); err != ErrEmptyAccount {
209		t.Fatalf("WithdrawAll(\"\") err = %v", err)
210	}
211	l.MustDeposit("alice", 555, 0)
212	if got, err := l.WithdrawAll("alice"); err != nil || got != 555 {
213		t.Fatalf("WithdrawAll = (%d,%v), want (555,nil)", got, err)
214	}
215	if got, err := l.WithdrawAll("alice"); err != nil || got != 0 {
216		t.Fatalf("repeat WithdrawAll = (%d,%v), want (0,nil)", got, err)
217	}
218	if l.UsersTotal() != 0 || l.Accounts() != 0 {
219		t.Fatal("WithdrawAll left state behind")
220	}
221}
222
223func TestWithdrawFees(t *testing.T) {
224	l := MustNew(1000)
225	l.MustDeposit("alice", 10000, 1000) // fee 1000
226	l.MustDeposit("bob", 10000, 1000)   // fee 1000
227	if got := l.WithdrawFees(); got != 2000 {
228		t.Fatalf("WithdrawFees = %d, want 2000", got)
229	}
230	if got := l.WithdrawFees(); got != 0 {
231		t.Fatalf("repeat WithdrawFees = %d, want 0", got)
232	}
233	if l.FeesAccrued() != 0 || l.UsersTotal() != 18000 {
234		t.Fatal("WithdrawFees corrupted totals")
235	}
236}
237
238func TestConservationAcrossSequence(t *testing.T) {
239	l := MustNew(1000)
240	deposited := int64(0)
241	withdrawn := int64(0)
242	feesOut := int64(0)
243
244	step := func() {
245		t.Helper()
246		// Internal consistency: usersTotal == sum of balances.
247		sum := int64(0)
248		l.Iterate(func(_ string, b int64) bool {
249			if b <= 0 {
250				t.Fatal("zero/negative balance stored")
251			}
252			sum += b
253			return false
254		})
255		if sum != l.UsersTotal() {
256			t.Fatalf("usersTotal %d != Σ balances %d", l.UsersTotal(), sum)
257		}
258		// Conservation: everything in == everything owed + everything out.
259		if deposited != l.Liabilities()+withdrawn+feesOut {
260			t.Fatalf("conservation: in=%d owed=%d out=%d+%d",
261				deposited, l.Liabilities(), withdrawn, feesOut)
262		}
263	}
264
265	deposit := func(who string, amt, bps int64) {
266		l.MustDeposit(who, amt, bps)
267		deposited += amt
268		step()
269	}
270	claim := func(who string, amt int64) {
271		l.MustWithdraw(who, amt)
272		withdrawn += amt
273		step()
274	}
275
276	deposit("alice", 1000, 0)
277	deposit("bob", 999, 250)
278	deposit("alice", 12345, 1000)
279	claim("alice", 1000)
280	deposit("carol", 1, 1000) // fee floors to 0
281	feesOut += l.WithdrawFees()
282	step()
283	claim("bob", 974)
284	all, _ := l.WithdrawAll("alice")
285	withdrawn += all
286	step()
287	deposit("dave", 7, 999)
288	feesOut += l.WithdrawFees()
289	step()
290}
291
292func TestCheckedAddBranches(t *testing.T) {
293	cases := []struct {
294		a, b, want int64
295		ok         bool
296	}{
297		{5, 3, 8, true},
298		{5, -3, 2, true},   // negative-b branch, no overflow
299		{-4, -6, -10, true},
300		{maxInt64, 1, 0, false},  // positive overflow
301		{minInt64, -1, 0, false}, // negative overflow
302		{maxInt64, -maxInt64, 0, true},
303	}
304	for _, c := range cases {
305		got, ok := checkedAdd(c.a, c.b)
306		if ok != c.ok || (ok && got != c.want) {
307			t.Fatalf("checkedAdd(%d,%d) = (%d,%v), want (%d,%v)", c.a, c.b, got, ok, c.want, c.ok)
308		}
309	}
310}
311
312func TestIterateSortedAndEarlyStop(t *testing.T) {
313	l := MustNew(0)
314	l.MustDeposit("carol", 3, 0)
315	l.MustDeposit("alice", 1, 0)
316	l.MustDeposit("bob", 2, 0)
317	got := ""
318	l.Iterate(func(a string, _ int64) bool {
319		got += a + ","
320		return false
321	})
322	if got != "alice,bob,carol," {
323		t.Fatalf("Iterate order = %q", got)
324	}
325	n := 0
326	l.Iterate(func(string, int64) bool { n++; return n == 2 })
327	if n != 2 {
328		t.Fatalf("early stop visited %d", n)
329	}
330}