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}