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

merkledrop_test.gno

6.77 Kb · 225 lines
  1package merkledrop
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime"
  7	"testing"
  8
  9	"gno.land/p/moul/x/merkle/v0"
 10	"gno.land/p/nt/urequire/v0"
 11)
 12
 13const (
 14	alice   = address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // demo index 0, 100
 15	bob     = address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // demo index 1, 250
 16	mallory = address("g1sqmuwtsrgd6t8y2n8f6z5xkltmpmcd64sx3vgg")
 17)
 18
 19func fund(n int64) {
 20	testing.IssueCoins(Address(), chain.NewCoins(chain.NewCoin(Denom, n)))
 21}
 22
 23func balanceOf(a address) int64 {
 24	return banker.NewReadonlyBanker().GetCoins(a).AmountOf(Denom)
 25}
 26
 27// Every seeded allocation must verify against the seeded root, and no other
 28// address or amount may.
 29func TestDemoDropVerifies(t *testing.T) {
 30	seed()
 31	if Total() != 4 || Root() == "" {
 32		t.Fatalf("seed left Total=%d Root=%q", Total(), Root())
 33	}
 34	for i, a := range demoAllocations {
 35		proof := DemoProof(i)
 36		if !Verify(i, a.Address, a.Amount, proof) {
 37			t.Errorf("allocation %d does not verify", i)
 38		}
 39		if Verify(i, mallory, a.Amount, proof) {
 40			t.Errorf("allocation %d verified for the wrong address", i)
 41		}
 42		if Verify(i, a.Address, a.Amount+1, proof) {
 43			t.Errorf("allocation %d verified for the wrong amount", i)
 44		}
 45		if i > 0 && Verify(i-1, a.Address, a.Amount, proof) {
 46			t.Errorf("allocation %d verified at the wrong index", i)
 47		}
 48	}
 49}
 50
 51// The leaf is the whole interface to an off-chain generator, so pin it.
 52func TestLeafEncoding(t *testing.T) {
 53	got := Leaf(3, alice, 1234)
 54	want := "gno.land/r/moul/x/daily/merkledrop/v1|3|g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5|1234"
 55	if got != want {
 56		t.Errorf("Leaf = %q, want %q", got, want)
 57	}
 58}
 59
 60func TestClaimPays(cur realm, t *testing.T) {
 61	seed()
 62	fund(1000)
 63	before := balanceOf(alice)
 64
 65	testing.SetRealm(testing.NewUserRealm(alice))
 66	Claim(cross(cur), 0, 100, DemoProof(0))
 67
 68	if got := balanceOf(alice) - before; got != 100 {
 69		t.Errorf("alice received %d ugnot, want 100", got)
 70	}
 71	if !HasClaimed(0) {
 72		t.Error("index 0 not marked claimed")
 73	}
 74	if ClaimedBy(0) != alice {
 75		t.Errorf("ClaimedBy(0) = %s, want alice", ClaimedBy(0))
 76	}
 77	if Paid() != 100 || Claims() != 1 {
 78		t.Errorf("Paid=%d Claims=%d, want 100 and 1", Paid(), Claims())
 79	}
 80	if Balance() != 900 {
 81		t.Errorf("drop balance %d, want 900", Balance())
 82	}
 83}
 84
 85func TestClaimRejects(cur realm, t *testing.T) {
 86	seed()
 87	fund(1000)
 88
 89	testing.SetRealm(testing.NewUserRealm(alice))
 90	urequire.AbortsContains(t, cur, "invalid proof", func() {
 91		Claim(cross(cur), 0, 999, DemoProof(0)) // wrong amount
 92	})
 93	urequire.AbortsContains(t, cur, "invalid proof", func() {
 94		Claim(cross(cur), 1, 250, DemoProof(1)) // bob's allocation, alice calling
 95	})
 96	urequire.AbortsContains(t, cur, "invalid proof", func() {
 97		Claim(cross(cur), 0, 100, "deadbeef") // malformed proof
 98	})
 99	urequire.AbortsContains(t, cur, "amount must be positive", func() {
100		Claim(cross(cur), 0, 0, DemoProof(0))
101	})
102
103	testing.SetRealm(testing.NewUserRealm(mallory))
104	urequire.AbortsContains(t, cur, "invalid proof", func() {
105		Claim(cross(cur), 0, 100, DemoProof(0)) // not in the tree at all
106	})
107
108	// Nothing above may have consumed an allocation.
109	if Claims() != 0 || Paid() != 0 {
110		t.Errorf("a rejected claim left state behind: Claims=%d Paid=%d", Claims(), Paid())
111	}
112}
113
114func TestDoubleClaimRejected(cur realm, t *testing.T) {
115	seed()
116	fund(1000)
117	testing.SetRealm(testing.NewUserRealm(bob))
118	Claim(cross(cur), 1, 250, DemoProof(1))
119	urequire.AbortsContains(t, cur, "already claimed", func() {
120		Claim(cross(cur), 1, 250, DemoProof(1))
121	})
122	if Claims() != 1 {
123		t.Errorf("Claims = %d, want 1", Claims())
124	}
125}
126
127// An underfunded drop must refuse rather than mark the allocation claimed and
128// leave the recipient with nothing to re-claim.
129func TestUnderfundedDropDoesNotBurnAnAllocation(cur realm, t *testing.T) {
130	seed()
131	fund(10) // less than alice's 100
132	testing.SetRealm(testing.NewUserRealm(alice))
133	urequire.AbortsContains(t, cur, "cannot cover", func() {
134		Claim(cross(cur), 0, 100, DemoProof(0))
135	})
136	if HasClaimed(0) {
137		t.Error("a refused claim still consumed the allocation")
138	}
139	fund(1000)
140	Claim(cross(cur), 0, 100, DemoProof(0))
141	if !HasClaimed(0) {
142		t.Error("the claim did not go through once funded")
143	}
144}
145
146func TestSetDropOwnerOnly(cur realm, t *testing.T) {
147	seed()
148	newRoot := merkle.New([][]byte{[]byte("only-leaf")}).RootHex()
149
150	testing.SetRealm(testing.NewUserRealm(mallory))
151	urequire.AbortsContains(t, cur, "not owner", func() {
152		SetDrop(cross(cur), newRoot, 1, 0)
153	})
154
155	testing.SetRealm(testing.NewUserRealm(Ownable.Owner()))
156	SetDrop(cross(cur), newRoot, 1, 0)
157	if Root() != newRoot || Total() != 1 {
158		t.Errorf("SetDrop left Root=%s Total=%d", Root(), Total())
159	}
160	if len(demo) != 0 {
161		t.Error("a real drop must not keep the seeded allocation list")
162	}
163	if Claims() != 0 || Paid() != 0 {
164		t.Error("SetDrop did not reset the claim ledger")
165	}
166}
167
168func TestSetDropRejectsBadInput(cur realm, t *testing.T) {
169	seed()
170	testing.SetRealm(testing.NewUserRealm(Ownable.Owner()))
171	good := merkle.New([][]byte{[]byte("x")}).RootHex()
172	urequire.AbortsContains(t, cur, "32 bytes of hex", func() { SetDrop(cross(cur), "zz", 1, 0) })
173	urequire.AbortsContains(t, cur, "32 bytes of hex", func() { SetDrop(cross(cur), "abcd", 1, 0) })
174	urequire.AbortsContains(t, cur, "total must be positive", func() { SetDrop(cross(cur), good, 0, 0) })
175}
176
177func TestClosingAndSweep(cur realm, t *testing.T) {
178	seed()
179	fund(1000)
180	owner := Ownable.Owner()
181	testing.SetRealm(testing.NewUserRealm(owner))
182
183	// A drop that never closes can never be swept: that is what setting a
184	// closing height buys.
185	urequire.AbortsContains(t, cur, "not closed yet", func() { Sweep(cross(cur)) })
186
187	closeAt := runtime.ChainHeight() + 10
188	root := merkle.New([][]byte{[]byte(Leaf(0, alice, 100))}).RootHex()
189	SetDrop(cross(cur), root, 1, closeAt)
190	if IsClosed() {
191		t.Fatal("drop closed before its height")
192	}
193	urequire.AbortsContains(t, cur, "not closed yet", func() { Sweep(cross(cur)) })
194
195	testing.SkipHeights(11)
196	if !IsClosed() {
197		t.Fatal("drop still open past its closing height")
198	}
199	testing.SetRealm(testing.NewUserRealm(alice))
200	urequire.AbortsContains(t, cur, "has closed", func() {
201		Claim(cross(cur), 0, 100, "")
202	})
203
204	ownerBefore := balanceOf(owner)
205	testing.SetRealm(testing.NewUserRealm(owner))
206	Sweep(cross(cur))
207	if Balance() != 0 {
208		t.Errorf("drop balance after sweep = %d, want 0", Balance())
209	}
210	if balanceOf(owner)-ownerBefore != 1000 {
211		t.Errorf("owner received %d, want 1000", balanceOf(owner)-ownerBefore)
212	}
213	seed()
214}
215
216func TestIndexKeyPadsForOrdering(t *testing.T) {
217	// ufmt has no width flags, so the padding is by hand. Unpadded keys sort
218	// "0","1","10","11","2" and Render loses the order past nine entries.
219	if indexKey(7) != "000007" || indexKey(123456) != "123456" {
220		t.Errorf("indexKey(7)=%q indexKey(123456)=%q", indexKey(7), indexKey(123456))
221	}
222	if !(indexKey(2) < indexKey(10)) {
223		t.Error("padded keys do not sort numerically")
224	}
225}