-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathsplitter.js
274 lines (204 loc) · 9.24 KB
/
splitter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/* global describe:false */
const {
getBalance, gasCostFor, loopSerial, lastOf, assertPromiseThrows, toWei,
calculateContractAddress, getTransactionCount, sendTransaction,
} = require('./utils');
const Splitter = artifacts.require("./Splitter.sol");
contract('Splitter', (accounts) => {
const between = [...accounts.slice(0, 2)];
var instance;
beforeEach(async () => {
// reset state between runs
instance = await Splitter.new(between);
});
it('starts in the expected state', async () => {
const initialTotalInput = await instance.totalInput();
assert(initialTotalInput.eq(0));
// check initial withdrawls
await Promise.all(between.map(async acc => {
const withdrew = await instance.amountsWithdrew(acc);
assert(withdrew.eq(0));
}));
await Promise.all(between.map(async acc => {
const balance = await instance.balance({ from: acc });
assert(balance.eq(0));
}));
});
it('should accept funds', async () => {
const depositing = toWei(1, 'ether');
const contractBalanceBefore = await getBalance(instance.address);
await instance.sendTransaction({ from: lastOf(accounts), value: depositing });
const contractBalanceAfter = await getBalance(instance.address);
assert(contractBalanceAfter.sub(contractBalanceBefore).eq(depositing));
});
it('updates internal state when adding funds', async () => {
const funder = lastOf(accounts);
const funding = toWei(1, 'ether');
// add funds
await instance.sendTransaction({ from: funder, value: funding });
// check final withdrawls
await Promise.all(between.map(async acc => {
const withdrew = await instance.amountsWithdrew(acc);
assert(withdrew.eq(0));
}));
// check totalInput
const finalTotalInput = await instance.totalInput();
assert(finalTotalInput.eq(funding));
// check balances
await Promise.all(between.map(async acc => {
const balance = await instance.balance({ from: acc });
assert(balance.eq(funding.div(2)));
}));
});
it('should split a single funding evenly between multiple parties', async () => {
const funder = lastOf(accounts);
const funding = toWei(1, 'ether');
// add funds
await instance.sendTransaction({ from: funder, value: funding });
// try withdrawing each share
// we need to run serially otherwise we are looking at the wrong global
// state
await loopSerial(between, async (acc) => {
// collect initial state
const initialContractBalance = await getBalance(instance.address);
let initialAccountBalance = await getBalance(acc);
const contractAccountBalance = await instance.balance({ from: acc });
// sweep contract
const tx = await instance.withdrawAll({ from: acc });
// undo gas cost
const cost = await gasCostFor(tx);
initialAccountBalance = initialAccountBalance.sub(cost);
// collect final state
const finalContractBalance = await getBalance(instance.address);
const finalAccountBalance = await getBalance(acc);
const share = funding.div(between.length);
const contractBalanceChange = finalContractBalance.sub(initialContractBalance);
assert(contractBalanceChange.eq(share.mul(-1)));
const accountBalanceChange = finalAccountBalance.sub(initialAccountBalance);
assert(contractAccountBalance.eq(accountBalanceChange));
const finalWithdrawlAmount = await instance.amountsWithdrew(acc);
assert(accountBalanceChange.eq(finalWithdrawlAmount));
// ensure the right amount was moved
assert(accountBalanceChange.eq(contractBalanceChange.mul(-1)));
});
const finalBalance = await getBalance(instance.address);
assert(finalBalance.eq(0));
});
it("should always say non-participating parties have a balnce of 0 wei", async () => {
const acc = lastOf(accounts);
const amount = toWei(1, 'ether');
const initialBalance = await instance.balance({ from: acc });
assert(initialBalance.eq(0));
await instance.sendTransaction({ from: acc, value: amount });
const finalBalance = await instance.balance({ from: acc });
assert(finalBalance.eq(0));
});
it('should fail to overdraw funds by an non-participating party', async () => {
const acc = lastOf(accounts);
const amount = toWei(1, 'ether');
await instance.sendTransaction({ from: acc, value: amount });
const initialContractBalance = await getBalance(instance.address);
await assertPromiseThrows(() => instance.withdraw(100, { from: acc }));
const finalContractBalance = await getBalance(instance.address);
// ensure state wasn't mutated
const amountWithdrew = await instance.amountsWithdrew(acc);
assert(amountWithdrew.eq(0));
assert(initialContractBalance.sub(finalContractBalance).eq(0));
assert(finalContractBalance.eq(amount));
});
it("should throw when non-participants withdraw 0 wei", async () => {
const acc = lastOf(accounts);
// make sure nothing happens (throw or state change)
const initialContractBalance = await getBalance(instance.address);
const initialAmountWithdrawn = await instance.amountsWithdrew(acc);
await assertPromiseThrows(() => instance.withdrawAll({ from: acc }));
const finalContractBalance = await getBalance(instance.address);
const finalAmountWithdrawn = await instance.amountsWithdrew(acc);
assert(finalContractBalance.eq(initialContractBalance));
assert(initialAmountWithdrawn.eq(0));
assert(finalAmountWithdrawn.sub(initialAmountWithdrawn).eq(0));
});
it('should withdraw some but not all wei from an account', async () => {
const funder = lastOf(accounts);
const fundAmount = 100;
await instance.sendTransaction({ from: funder, value: fundAmount });
const withdrawAmount = 10;
const initialContractBalance = await getBalance(instance.address);
await instance.withdraw(withdrawAmount, { from: accounts[0] });
const finalContractBalance = await getBalance(instance.address);
assert(initialContractBalance.sub(finalContractBalance).eq(withdrawAmount));
});
it('should evenly split multiple fundings with withdrawls in between', async () => {
const funder = lastOf(accounts);
const initialBalances = await Promise.all(
between.map(acc => getBalance(acc)));
const eachFunding = toWei(1, 'ether');
// fund once
await instance.sendTransaction({ from: funder, value: eachFunding });
// withdraw some stuff
const tx = await instance.withdrawAll({ from: accounts[0] });
// we are adding the gas cost back to ignore it in the balance change
const cost = await gasCostFor(tx);
initialBalances[0] = initialBalances[0].sub(cost);
// fund again
await instance.sendTransaction({ from: funder, value: eachFunding });
// sweep all accounts and check balances
await loopSerial(between, async (acc, i) => {
// sweep account
const tx = await instance.withdrawAll({ from: acc });
const cost = await gasCostFor(tx);
const initialBalance = initialBalances[i].sub(cost);
// compare balances
const finalBalance = await getBalance(acc);
const balanceChange = finalBalance.sub(initialBalance);
// 1 ether is half of the amount funded into the account
assert(balanceChange.eq(eachFunding));
});
});
it('should leave the remainder of un-evenly split inputs in the contract for future withdrawls', async () => {
const funder = lastOf(accounts);
const funding = toWei(1, 'ether').add(1);
const initialBalances = await Promise.all(
between.map(acc => getBalance(acc)));
await instance.sendTransaction({ from: funder, value: funding });
// sweep accounts
await loopSerial(between, async (acc, i) => {
// sweep
const tx = await instance.withdrawAll({ from: acc });
const cost = await gasCostFor(tx);
const initialBalance = initialBalances[i].sub(cost);
const finalBalance = await getBalance(acc);
const balanceChange = finalBalance.sub(initialBalance);
assert(balanceChange.eq(funding.sub(1).div(2)));
});
// the contract balance should hold the remainder
const contractBalance = await getBalance(instance.address);
assert(contractBalance.eq(1));
});
it('should not expose withdrawInternal', async () => {
// A similar bug brought down the parity multi-sig wallet.
assert(instance.withdrawInternal === undefined);
});
it('should handle a non-zero starting balance', async () => {
const deployer = accounts[0];
const funder = accounts[3];
const deployerNonce = await getTransactionCount(deployer);
const contractAddress = calculateContractAddress(deployer, deployerNonce);
await sendTransaction({
from: funder,
to: `0x${contractAddress}`,
value: web3.toWei(1, 'ether'),
});
const instance = await Splitter.new({ from: deployer });
const totalInput = await instance.totalInput();
assert(totalInput.eq(web3.toWei(1, 'ether')));
});
});
describe('calculateContractAddress', () => {
it('should calculate the correct address', () => {
const sender = '0x42da8a05cb7ed9a43572b5ba1b8f82a0a6e263dc';
const nonce = 158088;
const contractAddress = calculateContractAddress(sender, nonce);
assert.strictEqual(contractAddress, '2b6c0e2198942e0cedbdfc5b9f63f81ac6eeb5e7');
});
});