@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
package/README.md
CHANGED
|
@@ -74,6 +74,46 @@ npm run propose:add-member -- <multisig-address> <new-member-address>
|
|
|
74
74
|
|
|
75
75
|
The script uses the same `.env` configuration as deployment and prints the submitted operation id plus the final events emitted by the contract.
|
|
76
76
|
|
|
77
|
+
## Create a revoke-member proposal
|
|
78
|
+
|
|
79
|
+
The repository also includes a helper script to submit a multisig proposal that removes an existing owner.
|
|
80
|
+
|
|
81
|
+
```shell
|
|
82
|
+
npm run propose:revoke-member -- <multisig-address> <member-address>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The script uses the same `.env` configuration as deployment and prints the submitted operation id plus the final events emitted by the contract.
|
|
86
|
+
|
|
87
|
+
## Create a replace-member proposal
|
|
88
|
+
|
|
89
|
+
The repository also includes a helper script to submit a multisig proposal that replaces an existing owner with a new one.
|
|
90
|
+
|
|
91
|
+
```shell
|
|
92
|
+
npm run propose:replace-member -- <multisig-address> <current-member-address> <new-member-address>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The script uses the same `.env` configuration as deployment and prints the submitted operation id plus the final events emitted by the contract.
|
|
96
|
+
|
|
97
|
+
## Create a threshold proposal
|
|
98
|
+
|
|
99
|
+
The repository also includes a helper script to submit a multisig proposal that updates the required approval threshold.
|
|
100
|
+
|
|
101
|
+
```shell
|
|
102
|
+
npm run propose:threshold -- <multisig-address> <threshold>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The script uses the same `.env` configuration as deployment and prints the submitted operation id plus the final events emitted by the contract.
|
|
106
|
+
|
|
107
|
+
## Create an execution-delay proposal
|
|
108
|
+
|
|
109
|
+
The repository also includes a helper script to submit a multisig proposal that updates the execution delay.
|
|
110
|
+
|
|
111
|
+
```shell
|
|
112
|
+
npm run propose:execution-delay -- <multisig-address> <execution-delay>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The script uses the same `.env` configuration as deployment and prints the submitted operation id plus the final events emitted by the contract.
|
|
116
|
+
|
|
77
117
|
## Approve a proposal
|
|
78
118
|
|
|
79
119
|
The repository also includes a helper script to approve an existing multisig proposal by id.
|
|
@@ -3,8 +3,16 @@ import {
|
|
|
3
3
|
approve,
|
|
4
4
|
constructor,
|
|
5
5
|
getTransactions,
|
|
6
|
+
execute,
|
|
7
|
+
revoke,
|
|
6
8
|
} from '../contracts/Multisig';
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Storage,
|
|
11
|
+
mockAdminContext,
|
|
12
|
+
Address,
|
|
13
|
+
balanceOf,
|
|
14
|
+
} from '@massalabs/massa-as-sdk';
|
|
15
|
+
import { mockBalance } from '@massalabs/massa-as-sdk/assembly/vm-mock';
|
|
8
16
|
import {
|
|
9
17
|
Args,
|
|
10
18
|
u64ToBytes,
|
|
@@ -306,137 +314,88 @@ describe('Multisig contract tests', () => {
|
|
|
306
314
|
expect(transaction.data).toStrictEqual(new Args().add(42).serialize());
|
|
307
315
|
});
|
|
308
316
|
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// let destinationBalance = Coins.balanceOf(destination);
|
|
393
|
-
// let contractBalance = Coins.balanceOf(contractAddr);
|
|
394
|
-
// let initDestinationBalance = destinationBalance;
|
|
395
|
-
// let initContractBalance = contractBalance;
|
|
396
|
-
|
|
397
|
-
// switchUser(owners[1]);
|
|
398
|
-
// expect(() => {
|
|
399
|
-
// revoke(new Args().add(u64(3)).serialize());
|
|
400
|
-
// }).not.toThrow();
|
|
401
|
-
|
|
402
|
-
// switchUser(deployerAddress);
|
|
403
|
-
// generateEvent(
|
|
404
|
-
// createEvent('BALANCES BEFORE', [
|
|
405
|
-
// initDestinationBalance.toString(),
|
|
406
|
-
// initContractBalance.toString(),
|
|
407
|
-
// ]),
|
|
408
|
-
// );
|
|
409
|
-
|
|
410
|
-
// expect(() => {
|
|
411
|
-
// execute(new Args().add(u64(3)).serialize());
|
|
412
|
-
// }).toThrow();
|
|
413
|
-
|
|
414
|
-
// // the operation should not have been deleted
|
|
415
|
-
// let operationList = bytesToSerializableObjectArray<Transaction>(
|
|
416
|
-
// getTransactions([]),
|
|
417
|
-
// ).unwrap();
|
|
418
|
-
// expect(operationList.length).toBe(operationListLenght);
|
|
419
|
-
|
|
420
|
-
// // retrieve the operation in its current state in Storage
|
|
421
|
-
// let operation = retrieveOperation(i32(3));
|
|
422
|
-
|
|
423
|
-
// expect(operation.to).toBe(new Address(destination));
|
|
424
|
-
// expect(operation.value).toBe(u64(15000));
|
|
425
|
-
// expect(operation.executed).toBe(false);
|
|
426
|
-
|
|
427
|
-
// destinationBalance = Coins.balanceOf(destination);
|
|
428
|
-
// contractBalance = Coins.balanceOf(contractAddr);
|
|
429
|
-
// generateEvent(
|
|
430
|
-
// createEvent('BALANCES AFTER', [
|
|
431
|
-
// destinationBalance.toString(),
|
|
432
|
-
// contractBalance.toString(),
|
|
433
|
-
// ]),
|
|
434
|
-
// );
|
|
435
|
-
|
|
436
|
-
// // check that the transfer has not been done
|
|
437
|
-
// expect(destinationBalance).toBe(initDestinationBalance);
|
|
438
|
-
// expect(contractBalance).toBe(initContractBalance);
|
|
439
|
-
// });
|
|
317
|
+
// operation 2 is validated, let's execute it
|
|
318
|
+
test('execute transaction operation with success', () => {
|
|
319
|
+
// Fund the multisig so transferCoins in execute() has balance to move.
|
|
320
|
+
// The multisig has two validated 15000-coin transactions (op 2 and op 3)
|
|
321
|
+
// but only op 2 will actually be executed in this test, so 15000 is enough.
|
|
322
|
+
mockBalance(contractAddr, u64(15000));
|
|
323
|
+
|
|
324
|
+
const initDestinationBalance = balanceOf(destination);
|
|
325
|
+
const initContractBalance = balanceOf(contractAddr);
|
|
326
|
+
|
|
327
|
+
switchUser(owners[1]);
|
|
328
|
+
execute(new Args().add(u64(2)).serialize());
|
|
329
|
+
|
|
330
|
+
// op 2 is marked as executed
|
|
331
|
+
const transaction = retrieveOperation(i32(2));
|
|
332
|
+
expect(transaction.executed).toBe(true);
|
|
333
|
+
|
|
334
|
+
// the coins were actually transferred from the multisig to the destination
|
|
335
|
+
expect(balanceOf(destination)).toBe(initDestinationBalance + u64(15000));
|
|
336
|
+
expect(balanceOf(contractAddr) + u64(15000)).toBe(initContractBalance);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// operation 1 is not validated (only 1/2 approvals), let's try to execute it
|
|
340
|
+
test('execute transaction operation with failure', () => {
|
|
341
|
+
const initDestinationBalance = balanceOf(destination);
|
|
342
|
+
const initContractBalance = balanceOf(contractAddr);
|
|
343
|
+
|
|
344
|
+
switchUser(owners[1]);
|
|
345
|
+
|
|
346
|
+
// execute must revert because op 1 doesn't meet the required threshold
|
|
347
|
+
expect(() => {
|
|
348
|
+
execute(new Args().add(u64(1)).serialize());
|
|
349
|
+
}).toThrow();
|
|
350
|
+
|
|
351
|
+
// the operation is still in storage
|
|
352
|
+
expect(() => {
|
|
353
|
+
retrieveOperation(i32(1));
|
|
354
|
+
}).not.toThrow();
|
|
355
|
+
|
|
356
|
+
// no funds moved
|
|
357
|
+
expect(balanceOf(destination)).toBe(initDestinationBalance);
|
|
358
|
+
expect(balanceOf(contractAddr)).toBe(initContractBalance);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// operation 3 is validated by owners[1] & owners[2].
|
|
362
|
+
// now owners[1] will revoke it and we will try to execute it.
|
|
363
|
+
test('revoke operation', () => {
|
|
364
|
+
const initOperationListLength = bytesToSerializableObjectArray<Transaction>(
|
|
365
|
+
getTransactions([]),
|
|
366
|
+
).unwrap().length;
|
|
367
|
+
|
|
368
|
+
const initDestinationBalance = balanceOf(destination);
|
|
369
|
+
const initContractBalance = balanceOf(contractAddr);
|
|
370
|
+
|
|
371
|
+
// owners[1] revokes his approval on op 3. op 3 now has only 1 approval,
|
|
372
|
+
// which is below the required threshold.
|
|
373
|
+
switchUser(owners[1]);
|
|
374
|
+
revoke(new Args().add(u64(3)).serialize());
|
|
375
|
+
|
|
376
|
+
// the approval threshold is no longer met, so execute must revert.
|
|
377
|
+
expect(() => {
|
|
378
|
+
execute(new Args().add(u64(3)).serialize());
|
|
379
|
+
}).toThrow();
|
|
380
|
+
|
|
381
|
+
// the operation list is untouched (nothing gets deleted)
|
|
382
|
+
const operationList = bytesToSerializableObjectArray<Transaction>(
|
|
383
|
+
getTransactions([]),
|
|
384
|
+
).unwrap();
|
|
385
|
+
expect(operationList.length).toBe(initOperationListLength);
|
|
386
|
+
|
|
387
|
+
// op 3 is still present in storage, not executed, and the revocation
|
|
388
|
+
// reset its validation timestamp back to 0.
|
|
389
|
+
const operation = retrieveOperation(i32(3));
|
|
390
|
+
expect(operation.to).toBe(new Address(destination));
|
|
391
|
+
expect(operation.value).toBe(u64(15000));
|
|
392
|
+
expect(operation.executed).toBe(false);
|
|
393
|
+
expect(operation.timestamp).toBe(u64(0));
|
|
394
|
+
|
|
395
|
+
// no funds moved
|
|
396
|
+
expect(balanceOf(destination)).toBe(initDestinationBalance);
|
|
397
|
+
expect(balanceOf(contractAddr)).toBe(initContractBalance);
|
|
398
|
+
});
|
|
440
399
|
|
|
441
400
|
test('check operation list', () => {
|
|
442
401
|
let operationList = bytesToSerializableObjectArray<Transaction>(
|