@massalabs/multisig-contract 0.0.2-dev.20260414091108 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/assembly/__tests__/basic-tests.spec.ts +91 -132
- package/assembly/__tests__/coverage-tests.spec.ts +457 -0
- package/assembly/__tests__/execution-delay-tests.spec.ts +177 -0
- package/assembly/__tests__/member-management-tests.spec.ts +210 -0
- package/assembly/__tests__/single-owner-tests.spec.ts +155 -0
- package/assembly/__tests__/upgradeable-tests.spec.ts +124 -0
- package/assembly/contracts/Multisig.ts +3 -3
- package/assembly/libraries/Upgradeable.ts +6 -2
- package/package.json +6 -2
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constructor,
|
|
3
|
+
submit,
|
|
4
|
+
approve,
|
|
5
|
+
execute,
|
|
6
|
+
revoke,
|
|
7
|
+
setTimestamp,
|
|
8
|
+
receiveCoins,
|
|
9
|
+
changeRequirement,
|
|
10
|
+
changeExecutionDelay,
|
|
11
|
+
changeUpgradeDelay,
|
|
12
|
+
getApprovals,
|
|
13
|
+
} from '../contracts/Multisig';
|
|
14
|
+
import {
|
|
15
|
+
Storage,
|
|
16
|
+
mockAdminContext,
|
|
17
|
+
Address,
|
|
18
|
+
generateEvent,
|
|
19
|
+
} from '@massalabs/massa-as-sdk';
|
|
20
|
+
import {
|
|
21
|
+
mockBalance,
|
|
22
|
+
mockScCall,
|
|
23
|
+
} from '@massalabs/massa-as-sdk/assembly/vm-mock';
|
|
24
|
+
import {
|
|
25
|
+
Args,
|
|
26
|
+
bytesToU64,
|
|
27
|
+
bytesToI32,
|
|
28
|
+
bytesToNativeTypeArray,
|
|
29
|
+
bytesToSerializableObjectArray,
|
|
30
|
+
} from '@massalabs/as-types';
|
|
31
|
+
import {
|
|
32
|
+
changeCallStack,
|
|
33
|
+
resetStorage,
|
|
34
|
+
} from '@massalabs/massa-as-sdk/assembly/vm-mock/storage';
|
|
35
|
+
import { Transaction } from '../structs/Transaction';
|
|
36
|
+
import { getApprovalCount, hasApproved } from '../contracts/multisig-internals';
|
|
37
|
+
import { getTransactions } from '../contracts/Multisig';
|
|
38
|
+
import { DELAY, REQUIRED } from '../storage/Multisig';
|
|
39
|
+
|
|
40
|
+
// address of the contract set in vm-mock. must match contractAddr of vm.js
|
|
41
|
+
const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
|
|
42
|
+
|
|
43
|
+
// default deployer used by the vm-mock (caller != callee so isDeployingContract is true)
|
|
44
|
+
const deployerAddress = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
|
|
45
|
+
|
|
46
|
+
// multisig owners
|
|
47
|
+
const ownerA = 'AU1qDAxGJ387ETi9JRQzZWSPKYq4YPXrFvdiE4VoXUaiAt38JFEC';
|
|
48
|
+
const ownerB = 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb';
|
|
49
|
+
const ownerC = 'AU12LmTm4zRYkUQZusw7eevvV5ySzSwndJpENQ7EZHcmDbWafx96T';
|
|
50
|
+
|
|
51
|
+
// a well-formed user address that is not an owner
|
|
52
|
+
const nonOwner = 'AU1aMywGBgBywiL6WcbKR4ugxoBtdP9P3waBVi5e713uvj7F1DJL';
|
|
53
|
+
|
|
54
|
+
// EOA destination
|
|
55
|
+
const destination = 'AU155TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb';
|
|
56
|
+
|
|
57
|
+
// smart-contract destination used to exercise execute()'s `call` branch
|
|
58
|
+
const scDestination = 'AS1uku77MYEHy3i12WeERtD2JQzeKePz5zmJjWCq6A8wWyZakccQ';
|
|
59
|
+
|
|
60
|
+
function switchUser(user: string): void {
|
|
61
|
+
changeCallStack(user + ' , ' + contractAddr);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function switchToMultisig(): void {
|
|
65
|
+
changeCallStack(contractAddr + ' , ' + contractAddr);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Bootstraps a fresh 2-of-3 multisig with executionDelay = 0 and upgradeDelay = 0.
|
|
69
|
+
function setupMultisig(
|
|
70
|
+
execDelay: u64 = u64(0),
|
|
71
|
+
upgDelay: u64 = u64(0),
|
|
72
|
+
reqd: i32 = i32(2),
|
|
73
|
+
): void {
|
|
74
|
+
resetStorage();
|
|
75
|
+
changeCallStack(deployerAddress + ' , ' + contractAddr);
|
|
76
|
+
constructor(
|
|
77
|
+
new Args()
|
|
78
|
+
.add<Array<string>>([ownerA, ownerB, ownerC])
|
|
79
|
+
.add(reqd)
|
|
80
|
+
.add(upgDelay)
|
|
81
|
+
.add(execDelay)
|
|
82
|
+
.serialize(),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Submits + approves a transaction by `execDelay` owners so the validation
|
|
87
|
+
// threshold is reached. Returns the txId.
|
|
88
|
+
function submitAndValidate(tx: Transaction): u64 {
|
|
89
|
+
switchUser(ownerA);
|
|
90
|
+
const idBytes = submit(new Args().add(tx).serialize());
|
|
91
|
+
const id = bytesToU64(idBytes);
|
|
92
|
+
approve(new Args().add(id).serialize());
|
|
93
|
+
switchUser(ownerB);
|
|
94
|
+
approve(new Args().add(id).serialize());
|
|
95
|
+
return id;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function newTransfer(to: string, value: u64): Transaction {
|
|
99
|
+
return new Transaction(new Address(to), '', value, [], 0, false);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
beforeAll(() => {
|
|
103
|
+
resetStorage();
|
|
104
|
+
mockAdminContext(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ==========================================================================
|
|
108
|
+
// approve() — error paths
|
|
109
|
+
// ==========================================================================
|
|
110
|
+
|
|
111
|
+
describe('approve: error paths', () => {
|
|
112
|
+
test('reverts on a non-existent transaction id', () => {
|
|
113
|
+
setupMultisig();
|
|
114
|
+
switchUser(ownerA);
|
|
115
|
+
expect(() => {
|
|
116
|
+
approve(new Args().add(u64(42)).serialize());
|
|
117
|
+
}).toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('reverts when called by a non-owner', () => {
|
|
121
|
+
setupMultisig();
|
|
122
|
+
switchUser(ownerA);
|
|
123
|
+
submit(new Args().add(newTransfer(destination, u64(1000))).serialize());
|
|
124
|
+
|
|
125
|
+
switchUser(nonOwner);
|
|
126
|
+
expect(() => {
|
|
127
|
+
approve(new Args().add(u64(0)).serialize());
|
|
128
|
+
}).toThrow();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('reverts when the transaction has already been executed', () => {
|
|
132
|
+
setupMultisig();
|
|
133
|
+
mockBalance(contractAddr, u64(1000));
|
|
134
|
+
const id = submitAndValidate(newTransfer(destination, u64(1000)));
|
|
135
|
+
|
|
136
|
+
// execute so the tx becomes `executed = true`
|
|
137
|
+
switchUser(ownerA);
|
|
138
|
+
execute(new Args().add(id).serialize());
|
|
139
|
+
|
|
140
|
+
// ownerC now tries to approve - should revert on `_notExecuted`
|
|
141
|
+
switchUser(ownerC);
|
|
142
|
+
expect(() => {
|
|
143
|
+
approve(new Args().add(u64(0)).serialize());
|
|
144
|
+
}).toThrow();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ==========================================================================
|
|
149
|
+
// execute() — error paths and branches
|
|
150
|
+
// ==========================================================================
|
|
151
|
+
|
|
152
|
+
describe('execute: error paths and branches', () => {
|
|
153
|
+
test('reverts on a non-existent transaction id', () => {
|
|
154
|
+
setupMultisig();
|
|
155
|
+
switchUser(ownerA);
|
|
156
|
+
expect(() => {
|
|
157
|
+
execute(new Args().add(u64(99)).serialize());
|
|
158
|
+
}).toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('reverts when the transaction has already been executed', () => {
|
|
162
|
+
setupMultisig();
|
|
163
|
+
mockBalance(contractAddr, u64(1000));
|
|
164
|
+
const id = submitAndValidate(newTransfer(destination, u64(1000)));
|
|
165
|
+
|
|
166
|
+
switchUser(ownerA);
|
|
167
|
+
execute(new Args().add(id).serialize());
|
|
168
|
+
|
|
169
|
+
// second execute call must fail (already executed)
|
|
170
|
+
expect(() => {
|
|
171
|
+
execute(new Args().add(u64(0)).serialize());
|
|
172
|
+
}).toThrow();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("reverts with 'delay not passed' when the execution delay hasn't elapsed", () => {
|
|
176
|
+
// use a huge execution delay so now() + huge delay > Context.timestamp()
|
|
177
|
+
setupMultisig(u64(1_000_000_000_000));
|
|
178
|
+
mockBalance(contractAddr, u64(1000));
|
|
179
|
+
submitAndValidate(newTransfer(destination, u64(1000)));
|
|
180
|
+
|
|
181
|
+
switchUser(ownerA);
|
|
182
|
+
expect(() => {
|
|
183
|
+
execute(new Args().add(u64(0)).serialize());
|
|
184
|
+
}).toThrow();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('executes the call-branch when the destination is a smart contract', () => {
|
|
188
|
+
setupMultisig();
|
|
189
|
+
mockBalance(contractAddr, u64(500));
|
|
190
|
+
|
|
191
|
+
// Call a method on a smart-contract destination with some payload
|
|
192
|
+
const tx = new Transaction(
|
|
193
|
+
new Address(scDestination),
|
|
194
|
+
'doSomething',
|
|
195
|
+
u64(500),
|
|
196
|
+
new Args().add(u64(123)).serialize(),
|
|
197
|
+
0,
|
|
198
|
+
false,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// execute() reaches the `!isAddressEoa(...) -> call(...)` branch for SC
|
|
202
|
+
// destinations; the vm-mock needs a mocked return value for that call.
|
|
203
|
+
// If the transferCoins branch were hit by mistake, vm-mock would throw
|
|
204
|
+
// because `scDestination` has no bytecode registered.
|
|
205
|
+
mockScCall([]);
|
|
206
|
+
const id = submitAndValidate(tx);
|
|
207
|
+
|
|
208
|
+
switchUser(ownerA);
|
|
209
|
+
execute(new Args().add(id).serialize());
|
|
210
|
+
|
|
211
|
+
// the transaction is marked executed — the call branch didn't revert
|
|
212
|
+
const operationList = bytesToSerializableObjectArray<Transaction>(
|
|
213
|
+
getTransactions([]),
|
|
214
|
+
).unwrap();
|
|
215
|
+
expect(operationList[i32(id)].executed).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ==========================================================================
|
|
220
|
+
// revoke() — error paths and behavior
|
|
221
|
+
// ==========================================================================
|
|
222
|
+
|
|
223
|
+
describe('revoke: error paths and behavior', () => {
|
|
224
|
+
test('reverts on a non-existent transaction id', () => {
|
|
225
|
+
setupMultisig();
|
|
226
|
+
switchUser(ownerA);
|
|
227
|
+
expect(() => {
|
|
228
|
+
revoke(new Args().add(u64(99)).serialize());
|
|
229
|
+
}).toThrow();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('reverts when the transaction has already been executed', () => {
|
|
233
|
+
setupMultisig();
|
|
234
|
+
mockBalance(contractAddr, u64(1000));
|
|
235
|
+
const id = submitAndValidate(newTransfer(destination, u64(1000)));
|
|
236
|
+
|
|
237
|
+
switchUser(ownerA);
|
|
238
|
+
execute(new Args().add(id).serialize());
|
|
239
|
+
|
|
240
|
+
expect(() => {
|
|
241
|
+
revoke(new Args().add(u64(0)).serialize());
|
|
242
|
+
}).toThrow();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('reverts when the caller has not approved the tx', () => {
|
|
246
|
+
setupMultisig();
|
|
247
|
+
switchUser(ownerA);
|
|
248
|
+
submit(new Args().add(newTransfer(destination, u64(1000))).serialize());
|
|
249
|
+
// ownerA never approved — revoking must fail
|
|
250
|
+
expect(() => {
|
|
251
|
+
revoke(new Args().add(u64(0)).serialize());
|
|
252
|
+
}).toThrow();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('resets the approval count after a revoke below the threshold', () => {
|
|
256
|
+
setupMultisig();
|
|
257
|
+
switchUser(ownerA);
|
|
258
|
+
submit(new Args().add(newTransfer(destination, u64(1000))).serialize());
|
|
259
|
+
approve(new Args().add(u64(0)).serialize());
|
|
260
|
+
|
|
261
|
+
expect(getApprovalCount(u64(0))).toBe(1);
|
|
262
|
+
expect(hasApproved(u64(0), new Address(ownerA))).toBe(true);
|
|
263
|
+
|
|
264
|
+
revoke(new Args().add(u64(0)).serialize());
|
|
265
|
+
|
|
266
|
+
expect(getApprovalCount(u64(0))).toBe(0);
|
|
267
|
+
expect(hasApproved(u64(0), new Address(ownerA))).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ==========================================================================
|
|
272
|
+
// changeRequirement()
|
|
273
|
+
// ==========================================================================
|
|
274
|
+
|
|
275
|
+
describe('changeRequirement', () => {
|
|
276
|
+
test('reverts when called by a non-multisig caller', () => {
|
|
277
|
+
setupMultisig();
|
|
278
|
+
switchUser(ownerA);
|
|
279
|
+
expect(() => {
|
|
280
|
+
changeRequirement(new Args().add(i32(1)).serialize());
|
|
281
|
+
}).toThrow();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('reverts when the new value is 0', () => {
|
|
285
|
+
setupMultisig();
|
|
286
|
+
switchToMultisig();
|
|
287
|
+
expect(() => {
|
|
288
|
+
changeRequirement(new Args().add(i32(0)).serialize());
|
|
289
|
+
}).toThrow();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('reverts when the new value exceeds the owner count', () => {
|
|
293
|
+
setupMultisig();
|
|
294
|
+
switchToMultisig();
|
|
295
|
+
expect(() => {
|
|
296
|
+
// 3 owners → required = 4 is invalid
|
|
297
|
+
changeRequirement(new Args().add(i32(4)).serialize());
|
|
298
|
+
}).toThrow();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('updates REQUIRED storage on a valid call', () => {
|
|
302
|
+
setupMultisig();
|
|
303
|
+
switchToMultisig();
|
|
304
|
+
changeRequirement(new Args().add(i32(3)).serialize());
|
|
305
|
+
expect(bytesToI32(Storage.get(REQUIRED))).toBe(3);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ==========================================================================
|
|
310
|
+
// changeExecutionDelay()
|
|
311
|
+
// ==========================================================================
|
|
312
|
+
|
|
313
|
+
describe('changeExecutionDelay', () => {
|
|
314
|
+
test('reverts when called by a non-multisig caller', () => {
|
|
315
|
+
setupMultisig();
|
|
316
|
+
switchUser(ownerA);
|
|
317
|
+
expect(() => {
|
|
318
|
+
changeExecutionDelay(new Args().add(u64(5000)).serialize());
|
|
319
|
+
}).toThrow();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('updates DELAY storage on a valid call', () => {
|
|
323
|
+
setupMultisig();
|
|
324
|
+
switchToMultisig();
|
|
325
|
+
changeExecutionDelay(new Args().add(u64(123456)).serialize());
|
|
326
|
+
expect(bytesToU64(Storage.get(DELAY))).toBe(u64(123456));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('after raising the delay, execute() becomes subject to the delay gate', () => {
|
|
330
|
+
setupMultisig();
|
|
331
|
+
mockBalance(contractAddr, u64(1000));
|
|
332
|
+
const id = submitAndValidate(newTransfer(destination, u64(1000)));
|
|
333
|
+
|
|
334
|
+
// raise the execution delay after the tx has already been validated
|
|
335
|
+
switchToMultisig();
|
|
336
|
+
changeExecutionDelay(new Args().add(u64(1_000_000_000_000)).serialize());
|
|
337
|
+
|
|
338
|
+
// now the delay gate should trip
|
|
339
|
+
switchUser(ownerA);
|
|
340
|
+
expect(() => {
|
|
341
|
+
execute(new Args().add(u64(0)).serialize());
|
|
342
|
+
}).toThrow();
|
|
343
|
+
// silence "unused id" warning
|
|
344
|
+
generateEvent(id.toString());
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ==========================================================================
|
|
349
|
+
// changeUpgradeDelay()
|
|
350
|
+
// ==========================================================================
|
|
351
|
+
|
|
352
|
+
describe('changeUpgradeDelay', () => {
|
|
353
|
+
test('reverts when called by a non-multisig caller', () => {
|
|
354
|
+
setupMultisig();
|
|
355
|
+
switchUser(ownerA);
|
|
356
|
+
expect(() => {
|
|
357
|
+
changeUpgradeDelay(new Args().add(u64(10000)).serialize());
|
|
358
|
+
}).toThrow();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('does not throw when called by the multisig itself', () => {
|
|
362
|
+
setupMultisig();
|
|
363
|
+
switchToMultisig();
|
|
364
|
+
changeUpgradeDelay(new Args().add(u64(10000)).serialize());
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ==========================================================================
|
|
369
|
+
// setTimestamp()
|
|
370
|
+
// ==========================================================================
|
|
371
|
+
|
|
372
|
+
describe('setTimestamp', () => {
|
|
373
|
+
test('reverts when called by a non-owner', () => {
|
|
374
|
+
setupMultisig();
|
|
375
|
+
switchUser(ownerA);
|
|
376
|
+
submit(new Args().add(newTransfer(destination, u64(1000))).serialize());
|
|
377
|
+
|
|
378
|
+
switchUser(nonOwner);
|
|
379
|
+
expect(() => {
|
|
380
|
+
setTimestamp(new Args().add(u64(0)).serialize());
|
|
381
|
+
}).toThrow();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('reverts on a non-existent transaction id', () => {
|
|
385
|
+
setupMultisig();
|
|
386
|
+
switchUser(ownerA);
|
|
387
|
+
expect(() => {
|
|
388
|
+
setTimestamp(new Args().add(u64(99)).serialize());
|
|
389
|
+
}).toThrow();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('is a no-op when the approval threshold has not been reached', () => {
|
|
393
|
+
setupMultisig();
|
|
394
|
+
switchUser(ownerA);
|
|
395
|
+
submit(new Args().add(newTransfer(destination, u64(1000))).serialize());
|
|
396
|
+
approve(new Args().add(u64(0)).serialize());
|
|
397
|
+
// only 1/2 approvals — setTimestamp must leave the timestamp untouched
|
|
398
|
+
setTimestamp(new Args().add(u64(0)).serialize());
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('reverts when the timestamp is already set (threshold reached via approve)', () => {
|
|
402
|
+
setupMultisig();
|
|
403
|
+
submitAndValidate(newTransfer(destination, u64(1000)));
|
|
404
|
+
// approve() already set tx.timestamp, so setTimestamp must revert
|
|
405
|
+
switchUser(ownerA);
|
|
406
|
+
expect(() => {
|
|
407
|
+
setTimestamp(new Args().add(u64(0)).serialize());
|
|
408
|
+
}).toThrow();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ==========================================================================
|
|
413
|
+
// receiveCoins()
|
|
414
|
+
// ==========================================================================
|
|
415
|
+
|
|
416
|
+
describe('receiveCoins', () => {
|
|
417
|
+
test('does not throw', () => {
|
|
418
|
+
setupMultisig();
|
|
419
|
+
switchUser(ownerA);
|
|
420
|
+
receiveCoins([]);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ==========================================================================
|
|
425
|
+
// getApprovals()
|
|
426
|
+
// ==========================================================================
|
|
427
|
+
|
|
428
|
+
describe('getApprovals', () => {
|
|
429
|
+
test('returns the list of owners that approved a transaction', () => {
|
|
430
|
+
setupMultisig();
|
|
431
|
+
|
|
432
|
+
// submit, then have ownerA and ownerC approve (skipping ownerB)
|
|
433
|
+
switchUser(ownerA);
|
|
434
|
+
submit(new Args().add(newTransfer(destination, u64(1000))).serialize());
|
|
435
|
+
approve(new Args().add(u64(0)).serialize());
|
|
436
|
+
switchUser(ownerC);
|
|
437
|
+
approve(new Args().add(u64(0)).serialize());
|
|
438
|
+
|
|
439
|
+
const raw = getApprovals(new Args().add(u64(0)).serialize());
|
|
440
|
+
const approvers = bytesToNativeTypeArray<string>(raw);
|
|
441
|
+
|
|
442
|
+
expect(approvers.length).toBe(2);
|
|
443
|
+
expect(approvers.includes(ownerA)).toBe(true);
|
|
444
|
+
expect(approvers.includes(ownerC)).toBe(true);
|
|
445
|
+
expect(approvers.includes(ownerB)).toBe(false);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('returns an empty list for a transaction with no approvals', () => {
|
|
449
|
+
setupMultisig();
|
|
450
|
+
switchUser(ownerA);
|
|
451
|
+
submit(new Args().add(newTransfer(destination, u64(1000))).serialize());
|
|
452
|
+
|
|
453
|
+
const raw = getApprovals(new Args().add(u64(0)).serialize());
|
|
454
|
+
const approvers = bytesToNativeTypeArray<string>(raw);
|
|
455
|
+
expect(approvers.length).toBe(0);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constructor,
|
|
3
|
+
submit,
|
|
4
|
+
approve,
|
|
5
|
+
execute,
|
|
6
|
+
getTransactions,
|
|
7
|
+
} from '../contracts/Multisig';
|
|
8
|
+
import {
|
|
9
|
+
Storage,
|
|
10
|
+
mockAdminContext,
|
|
11
|
+
Address,
|
|
12
|
+
Context,
|
|
13
|
+
balanceOf,
|
|
14
|
+
} from '@massalabs/massa-as-sdk';
|
|
15
|
+
import { mockBalance } from '@massalabs/massa-as-sdk/assembly/vm-mock';
|
|
16
|
+
import {
|
|
17
|
+
Args,
|
|
18
|
+
bytesToU64,
|
|
19
|
+
bytesToSerializableObjectArray,
|
|
20
|
+
} from '@massalabs/as-types';
|
|
21
|
+
import {
|
|
22
|
+
changeCallStack,
|
|
23
|
+
resetStorage,
|
|
24
|
+
} from '@massalabs/massa-as-sdk/assembly/vm-mock/storage';
|
|
25
|
+
import { Transaction } from '../structs/Transaction';
|
|
26
|
+
import { SafeMath } from '../libraries/SafeMath';
|
|
27
|
+
import { DELAY } from '../storage/Multisig';
|
|
28
|
+
|
|
29
|
+
// address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
|
|
30
|
+
const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
|
|
31
|
+
|
|
32
|
+
// default deployer (caller != callee so Context.isDeployingContract() is true)
|
|
33
|
+
const deployerAddress = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
|
|
34
|
+
|
|
35
|
+
// multisig owners
|
|
36
|
+
const ownerA = 'AU1qDAxGJ387ETi9JRQzZWSPKYq4YPXrFvdiE4VoXUaiAt38JFEC';
|
|
37
|
+
const ownerB = 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb';
|
|
38
|
+
|
|
39
|
+
// destination for the dummy transaction
|
|
40
|
+
const destination = 'AU155TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb';
|
|
41
|
+
|
|
42
|
+
function switchUser(user: string): void {
|
|
43
|
+
changeCallStack(user + ' , ' + contractAddr);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function retrieveOperation(opIndex: i32): Transaction {
|
|
47
|
+
const operationList = bytesToSerializableObjectArray<Transaction>(
|
|
48
|
+
getTransactions([]),
|
|
49
|
+
).unwrap();
|
|
50
|
+
return operationList[opIndex];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeAll(() => {
|
|
54
|
+
resetStorage();
|
|
55
|
+
mockAdminContext(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Zero-ms execution delay', () => {
|
|
59
|
+
test('constructor accepts executionDelay = 0 and stores it as zero', () => {
|
|
60
|
+
resetStorage();
|
|
61
|
+
changeCallStack(deployerAddress + ' , ' + contractAddr);
|
|
62
|
+
|
|
63
|
+
expect(() => {
|
|
64
|
+
constructor(
|
|
65
|
+
new Args()
|
|
66
|
+
.add<Array<string>>([ownerA, ownerB])
|
|
67
|
+
.add(i32(2))
|
|
68
|
+
// upgradeDelay
|
|
69
|
+
.add(u64(0))
|
|
70
|
+
// executionDelay = 0 ms (no wait between approval and execution)
|
|
71
|
+
.add(u64(0))
|
|
72
|
+
.serialize(),
|
|
73
|
+
);
|
|
74
|
+
}).not.toThrow();
|
|
75
|
+
|
|
76
|
+
// DELAY must be stored as u64(0)
|
|
77
|
+
expect(bytesToU64(Storage.get(DELAY))).toBe(u64(0));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('once approved, a transaction is immediately eligible for execution (delay check passes)', () => {
|
|
81
|
+
resetStorage();
|
|
82
|
+
changeCallStack(deployerAddress + ' , ' + contractAddr);
|
|
83
|
+
constructor(
|
|
84
|
+
new Args()
|
|
85
|
+
.add<Array<string>>([ownerA, ownerB])
|
|
86
|
+
.add(i32(2))
|
|
87
|
+
.add(u64(0))
|
|
88
|
+
.add(u64(0))
|
|
89
|
+
.serialize(),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const tx = new Transaction(
|
|
93
|
+
new Address(destination),
|
|
94
|
+
'',
|
|
95
|
+
u64(0),
|
|
96
|
+
[],
|
|
97
|
+
0,
|
|
98
|
+
false,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// submit + reach the required threshold (2 approvals)
|
|
102
|
+
switchUser(ownerA);
|
|
103
|
+
const opIdBytes = submit(new Args().add(tx).serialize());
|
|
104
|
+
const opId = bytesToU64(opIdBytes);
|
|
105
|
+
approve(new Args().add(opId).serialize());
|
|
106
|
+
|
|
107
|
+
switchUser(ownerB);
|
|
108
|
+
approve(new Args().add(opId).serialize());
|
|
109
|
+
|
|
110
|
+
// With a 0 ms execution delay, the gate enforced by execute()
|
|
111
|
+
// SafeMath.add(tx.timestamp, DELAY) <= Context.timestamp()
|
|
112
|
+
// must already be satisfied as soon as the threshold is reached.
|
|
113
|
+
const storedTx = retrieveOperation(i32(opId));
|
|
114
|
+
const delay = bytesToU64(Storage.get(DELAY));
|
|
115
|
+
expect(delay).toBe(u64(0));
|
|
116
|
+
expect(storedTx.timestamp).toBeGreaterThan(u64(0));
|
|
117
|
+
expect(SafeMath.add(storedTx.timestamp, delay)).toBeLessThanOrEqual(
|
|
118
|
+
Context.timestamp(),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('execute() succeeds immediately after approval with executionDelay = 0', () => {
|
|
123
|
+
resetStorage();
|
|
124
|
+
changeCallStack(deployerAddress + ' , ' + contractAddr);
|
|
125
|
+
constructor(
|
|
126
|
+
new Args()
|
|
127
|
+
.add<Array<string>>([ownerA, ownerB])
|
|
128
|
+
.add(i32(2))
|
|
129
|
+
.add(u64(0))
|
|
130
|
+
.add(u64(0))
|
|
131
|
+
.serialize(),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Fund the multisig so transferCoins in execute() has balance to move.
|
|
135
|
+
const transferAmount: u64 = 15000;
|
|
136
|
+
mockBalance(contractAddr, transferAmount);
|
|
137
|
+
|
|
138
|
+
const tx = new Transaction(
|
|
139
|
+
new Address(destination),
|
|
140
|
+
'',
|
|
141
|
+
transferAmount,
|
|
142
|
+
[],
|
|
143
|
+
0,
|
|
144
|
+
false,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
switchUser(ownerA);
|
|
148
|
+
const opIdBytes = submit(new Args().add(tx).serialize());
|
|
149
|
+
const opId = bytesToU64(opIdBytes);
|
|
150
|
+
approve(new Args().add(opId).serialize());
|
|
151
|
+
|
|
152
|
+
switchUser(ownerB);
|
|
153
|
+
approve(new Args().add(opId).serialize());
|
|
154
|
+
|
|
155
|
+
const destinationBalanceBefore = balanceOf(destination);
|
|
156
|
+
const contractBalanceBefore = balanceOf(contractAddr);
|
|
157
|
+
expect(contractBalanceBefore).toBe(transferAmount);
|
|
158
|
+
|
|
159
|
+
// Immediately execute - with executionDelay = 0 no waiting is required.
|
|
160
|
+
// Calling execute() directly (not via a closure) because AssemblyScript
|
|
161
|
+
// closures cannot capture local variables like opId.
|
|
162
|
+
switchUser(ownerA);
|
|
163
|
+
execute(new Args().add(opId).serialize());
|
|
164
|
+
|
|
165
|
+
// tx is flagged as executed
|
|
166
|
+
const storedTx = retrieveOperation(i32(opId));
|
|
167
|
+
expect(storedTx.executed).toBe(true);
|
|
168
|
+
|
|
169
|
+
// coins actually moved from the multisig to the destination
|
|
170
|
+
expect(balanceOf(destination)).toBe(
|
|
171
|
+
destinationBalanceBefore + transferAmount,
|
|
172
|
+
);
|
|
173
|
+
expect(balanceOf(contractAddr)).toBe(
|
|
174
|
+
contractBalanceBefore - transferAmount,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|