@massalabs/multisig-contract 0.0.2-dev.20260414091108

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.
@@ -0,0 +1,373 @@
1
+ import {
2
+ Args,
3
+ u64ToBytes,
4
+ serializableObjectsArrayToBytes,
5
+ i32ToBytes,
6
+ nativeTypeArrayToBytes,
7
+ bytesToU64,
8
+ } from '@massalabs/as-types';
9
+ import {
10
+ Address,
11
+ Context,
12
+ Storage,
13
+ call,
14
+ createEvent,
15
+ generateEvent,
16
+ isAddressEoa,
17
+ transferCoins,
18
+ } from '@massalabs/massa-as-sdk';
19
+ import {
20
+ _notApproved,
21
+ _notExecuted,
22
+ _onlyOwner,
23
+ _txExists,
24
+ addOwner as _addOwner,
25
+ addTransaction,
26
+ getApprovalCount,
27
+ hasApproved,
28
+ required,
29
+ removeOwner as _removeOwner,
30
+ setApproval,
31
+ owners,
32
+ _isMultisig,
33
+ } from './multisig-internals';
34
+ import { DELAY, REQUIRED, TRANSACTIONS } from '../storage/Multisig';
35
+ import { Transaction } from '../structs/Transaction';
36
+ import { Upgradeable } from '../libraries/Upgradeable';
37
+ import { SafeMath } from '../libraries/SafeMath';
38
+
39
+ /**
40
+ * @dev Contract constructor sets initial owners and required number of confirmations.
41
+ * @param {StaticArray<u8>} bs - Byte string containing
42
+ * - the list of initial owners
43
+ * - required number of approvals
44
+ * - the upgrade delay
45
+ * - the execution delay
46
+ */
47
+ export function constructor(bs: StaticArray<u8>): void {
48
+ assert(Context.isDeployingContract(), 'already deployed');
49
+
50
+ const args = new Args(bs);
51
+ const owners: string[] = args.nextStringArray().expect('owners not found');
52
+ const required = args.nextI32().expect('required not found');
53
+ const upgradeDelay = args.nextU64().expect('upgradeDelay not found');
54
+ const executionDelay = args.nextU64().expect('executionDelay not found');
55
+
56
+ Upgradeable.__Upgradeable_init(upgradeDelay);
57
+ assert(owners.length > 1, 'At least 2 owners required');
58
+ assert(required > 0 && required <= owners.length, 'invalid required');
59
+
60
+ for (let i = 0; i < owners.length; i++) {
61
+ _addOwner(owners[i]);
62
+ }
63
+ Storage.set(REQUIRED, i32ToBytes(required));
64
+ Storage.set(DELAY, u64ToBytes(executionDelay));
65
+ }
66
+
67
+ /**
68
+ * @notice Function to receive coins
69
+ * @param _ unused
70
+ */
71
+ export function receiveCoins(_: StaticArray<u8>): void {
72
+ const event = createEvent('Deposit', [
73
+ Context.caller().toString(),
74
+ Context.transferredCoins().toString(),
75
+ ]);
76
+ generateEvent(event);
77
+ }
78
+
79
+ /**
80
+ * @dev Allows an owner to submit and confirm a transaction.
81
+ * @param {StaticArray<u8>} bs - Byte string containing the transaction to submit
82
+ * @returns Returns transaction ID.
83
+ */
84
+ export function submit(bs: StaticArray<u8>): StaticArray<u8> {
85
+ const args = new Args(bs);
86
+ const tx = args.nextSerializable<Transaction>().unwrap();
87
+
88
+ _onlyOwner();
89
+
90
+ const id = addTransaction(tx);
91
+
92
+ const event = createEvent('Submit', [id.toString()]);
93
+ generateEvent(event);
94
+
95
+ return u64ToBytes(id);
96
+ }
97
+
98
+ /**
99
+ * @dev Allows an owner to confirm a transaction.
100
+ * @param {StaticArray<u8>} bs - Byte string containing the transaction ID to approve
101
+ */
102
+ export function approve(bs: StaticArray<u8>): void {
103
+ const args = new Args(bs);
104
+ const txId = args.nextU64().unwrap();
105
+
106
+ _onlyOwner();
107
+ _txExists(txId);
108
+ _notApproved(txId);
109
+ _notExecuted(txId);
110
+
111
+ setApproval(txId, true);
112
+
113
+ if (getApprovalCount(txId) == required()) {
114
+ const tx = TRANSACTIONS.getSome(txId);
115
+ tx.timestamp = Context.timestamp();
116
+ TRANSACTIONS.set(txId, tx);
117
+ }
118
+
119
+ const event = createEvent('Approve', [
120
+ txId.toString(),
121
+ Context.caller().toString(),
122
+ ]);
123
+ generateEvent(event);
124
+ }
125
+
126
+ /**
127
+ * @dev Allows an owner to execute a confirmed transaction.
128
+ * @param {StaticArray<u8>} bs - Byte string containing the transaction ID to execute
129
+ */
130
+ export function execute(bs: StaticArray<u8>): void {
131
+ const args = new Args(bs);
132
+ const txId = args.nextU64().unwrap();
133
+
134
+ _onlyOwner();
135
+ _txExists(txId);
136
+ _notExecuted(txId);
137
+
138
+ assert(getApprovalCount(txId) >= required(), 'not enough approvals');
139
+
140
+ const tx = TRANSACTIONS.getSome(txId);
141
+
142
+ assert(
143
+ SafeMath.add(tx.timestamp, bytesToU64(Storage.get(DELAY))) <=
144
+ Context.timestamp(),
145
+ 'delay not passed',
146
+ );
147
+
148
+ tx.executed = true;
149
+ TRANSACTIONS.set(txId, tx);
150
+
151
+ if (!isAddressEoa(tx.to.toString())) {
152
+ call(tx.to, tx.method, new Args(tx.data), tx.value);
153
+ } else {
154
+ transferCoins(tx.to, tx.value);
155
+ }
156
+
157
+ const event = createEvent('Execute', [txId.toString()]);
158
+ generateEvent(event);
159
+ }
160
+
161
+ /**
162
+ * @dev Allows an owner to revoke a transaction.
163
+ * @param {StaticArray<u8>} bs - Byte string containing the transaction ID to revoke
164
+ */
165
+ export function revoke(bs: StaticArray<u8>): void {
166
+ const args = new Args(bs);
167
+ const txId = args.nextU64().unwrap();
168
+
169
+ _onlyOwner();
170
+ _txExists(txId);
171
+ _notExecuted(txId);
172
+
173
+ assert(hasApproved(txId, Context.caller()), 'tx not approved');
174
+
175
+ if (getApprovalCount(txId) == required()) {
176
+ const tx = TRANSACTIONS.getSome(txId);
177
+ tx.timestamp = u64(0);
178
+ TRANSACTIONS.set(txId, tx);
179
+ }
180
+
181
+ setApproval(txId, false);
182
+
183
+ const event = createEvent('Revoke', [
184
+ txId.toString(),
185
+ Context.caller().toString(),
186
+ ]);
187
+ generateEvent(event);
188
+ }
189
+
190
+ export function setTimestamp(bs: StaticArray<u8>): void {
191
+ const args = new Args(bs);
192
+ const txId = args.nextU64().unwrap();
193
+
194
+ _onlyOwner();
195
+ _txExists(txId);
196
+ _notExecuted(txId);
197
+
198
+ if (getApprovalCount(txId) >= required()) {
199
+ const tx = TRANSACTIONS.getSome(txId);
200
+ assert(tx.timestamp == u64(0), 'timestamp already set');
201
+ tx.timestamp = Context.timestamp();
202
+ TRANSACTIONS.set(txId, tx);
203
+
204
+ const event = createEvent('SetTimestamp', [txId.toString()]);
205
+ generateEvent(event);
206
+ }
207
+ }
208
+
209
+ // ======================================
210
+ // =========== MULTISIG ===============
211
+ // ======================================
212
+
213
+ /**
214
+ * @dev Allows to add a new owner. Transaction has to be sent by the multisig contract itself.
215
+ * @param bs byte string containing the owner Address of new owner.
216
+ */
217
+ export function addOwner(bs: StaticArray<u8>): void {
218
+ const args = new Args(bs);
219
+ const owner = args.nextString().unwrap();
220
+
221
+ _isMultisig();
222
+
223
+ _addOwner(owner);
224
+
225
+ const event = createEvent('AddOwner', [owner]);
226
+ generateEvent(event);
227
+ }
228
+
229
+ /**
230
+ * @dev Allows to remove an owner. Transaction has to be sent by the multisig contract itself.
231
+ * @param bs byte string containing the owner Address of the owner to remove.
232
+ */
233
+ export function removeOwner(bs: StaticArray<u8>): void {
234
+ const args = new Args(bs);
235
+ const owner = args.nextString().unwrap();
236
+
237
+ _isMultisig();
238
+ assert(
239
+ owners().length - 1 >= required() && owners().length > 2,
240
+ 'cannot remove owner',
241
+ );
242
+
243
+ _removeOwner(owner);
244
+
245
+ const event = createEvent('RemoveOwner', [owner]);
246
+ generateEvent(event);
247
+ }
248
+
249
+ /**
250
+ * @dev Allows to replace an owner. Transaction has to be sent by the multisig contract itself.
251
+ * @param bs byte string containing the owner Address of the owner to replace and the Address of the new owner.
252
+ */
253
+ export function replaceOwner(bs: StaticArray<u8>): void {
254
+ const args = new Args(bs);
255
+ const oldOwner = args.nextString().unwrap();
256
+ const newOwner = args.nextString().unwrap();
257
+
258
+ _isMultisig();
259
+
260
+ _removeOwner(oldOwner);
261
+ _addOwner(newOwner);
262
+
263
+ const event = createEvent('ReplaceOwner', [oldOwner, newOwner]);
264
+ generateEvent(event);
265
+ }
266
+
267
+ /**
268
+ * @dev Allows to change the number of required confirmations. Transaction has to be sent by wallet.
269
+ * @param _required Number of required confirmations.
270
+ */
271
+ export function changeRequirement(bs: StaticArray<u8>): void {
272
+ const args = new Args(bs);
273
+ const required = args.nextI32().unwrap();
274
+
275
+ _isMultisig();
276
+ assert(required > 0 && required <= owners().length, 'invalid required');
277
+
278
+ Storage.set(REQUIRED, i32ToBytes(required));
279
+
280
+ const event = createEvent('ChangeRequirement', [required.toString()]);
281
+ generateEvent(event);
282
+ }
283
+
284
+ /**
285
+ * @dev Allows to change the delay necessary before the execution of a tx. Transaction has to be sent by wallet.
286
+ * @param executionDelay time between the validation & the execution of a tx in ms.
287
+ */
288
+ export function changeExecutionDelay(bs: StaticArray<u8>): void {
289
+ const args = new Args(bs);
290
+ const executionDelay = args.nextU64().unwrap();
291
+
292
+ _isMultisig();
293
+
294
+ Storage.set(DELAY, u64ToBytes(executionDelay));
295
+
296
+ const event = createEvent('changeExecutionDelay', [
297
+ executionDelay.toString(),
298
+ ]);
299
+ generateEvent(event);
300
+ }
301
+
302
+ /**
303
+ * @dev Allows to change the delay necessary before a SC upgrade. Transaction has to be sent by wallet.
304
+ * @param upgradeDelay time between the proposition & validation of an upgrade in ms.
305
+ */
306
+ export function changeUpgradeDelay(bs: StaticArray<u8>): void {
307
+ const args = new Args(bs);
308
+ const upgradeDelay = args.nextU64().unwrap();
309
+
310
+ _isMultisig();
311
+
312
+ Upgradeable.__Upgradeable_setUpgradeDelay(upgradeDelay);
313
+
314
+ const event = createEvent('changeUpgradeDelay', [upgradeDelay.toString()]);
315
+ generateEvent(event);
316
+ }
317
+
318
+ export function proposeUpgrade(newImplementation: StaticArray<u8>): void {
319
+ _isMultisig();
320
+ Upgradeable.proposeUpgrade(newImplementation);
321
+ }
322
+
323
+ export function upgrade(_: StaticArray<u8>): void {
324
+ _isMultisig();
325
+ Upgradeable.upgrade();
326
+ }
327
+
328
+ // ======================================
329
+ // ============ VIEW ==================
330
+ // ======================================
331
+
332
+ /**
333
+ * @param bs byte string containing the id of the transaction to start from and to end to.
334
+ * @dev The arguments are optional. If not provided, the function will return all the transactions.
335
+ * To prevent using up all the gas. If To is provided, From must be provided as well.
336
+ * @returns the list of txs.
337
+ */
338
+ export function getTransactions(bs: StaticArray<u8>): StaticArray<u8> {
339
+ const args = new Args(bs);
340
+ const from = args.nextU64().isOk() ? args.nextU64().unwrap() : u64(0);
341
+ const to = args.nextU64().isOk()
342
+ ? args.nextU64().unwrap()
343
+ : TRANSACTIONS.size();
344
+ const txs: Transaction[] = [];
345
+
346
+ for (let i = from; i < to; i++) {
347
+ const tx = TRANSACTIONS.getSome(i);
348
+ txs.push(tx);
349
+ }
350
+
351
+ return serializableObjectsArrayToBytes(txs);
352
+ }
353
+
354
+ /**
355
+ * @param bs byte string containing the transaction ID.
356
+ * @returns the addresses that approved the transaction.
357
+ */
358
+ export function getApprovals(bs: StaticArray<u8>): StaticArray<u8> {
359
+ const args = new Args(bs);
360
+ const txId = args.nextU64().unwrap();
361
+
362
+ const approvals: string[] = [];
363
+ const _owners = owners();
364
+
365
+ for (let i = 0; i < owners.length; i++) {
366
+ const owner = _owners[i];
367
+ if (hasApproved(txId, new Address(owner))) {
368
+ approvals.push(owner);
369
+ }
370
+ }
371
+
372
+ return nativeTypeArrayToBytes(approvals);
373
+ }
@@ -0,0 +1,106 @@
1
+ import { Args, bytesToI32 } from '@massalabs/as-types';
2
+ import { Address, Context, Storage } from '@massalabs/massa-as-sdk';
3
+ import { Transaction } from '../structs/Transaction';
4
+ import {
5
+ APPROVED,
6
+ OWNERS,
7
+ REQUIRED,
8
+ IS_OWNER,
9
+ TRANSACTIONS,
10
+ } from '../storage/Multisig';
11
+
12
+ // GETTERS
13
+
14
+ export function getApprovalCount(txId: u64): i32 {
15
+ const _owners = owners();
16
+ let count = 0;
17
+ for (let i = 0; i < _owners.length; i++) {
18
+ if (hasApproved(txId, new Address(_owners[i]))) {
19
+ count++;
20
+ }
21
+ }
22
+ return count;
23
+ }
24
+
25
+ export function owners(): string[] {
26
+ return Storage.has(OWNERS)
27
+ ? new Args(Storage.get(OWNERS)).nextStringArray().unwrap()
28
+ : [];
29
+ }
30
+
31
+ export function required(): i32 {
32
+ return bytesToI32(Storage.get(REQUIRED));
33
+ }
34
+
35
+ export function hasApproved(txId: u64, owner: Address): bool {
36
+ return (
37
+ APPROVED.contains(buildApprovalKey(txId, owner)) &&
38
+ APPROVED.getSome(buildApprovalKey(txId, owner))
39
+ );
40
+ }
41
+
42
+ export function setApproval(txId: u64, approved: bool): void {
43
+ APPROVED.set(buildApprovalKey(txId, Context.caller()), approved);
44
+ }
45
+
46
+ // MODIFIERS
47
+
48
+ export function _onlyOwner(): void {
49
+ assert(IS_OWNER.contains(Context.caller().toString()), 'not owner');
50
+ }
51
+
52
+ export function _txExists(txId: u64): void {
53
+ assert(TRANSACTIONS.contains(txId), 'tx does not exist');
54
+ }
55
+
56
+ export function _notApproved(txId: u64): void {
57
+ assert(!hasApproved(txId, Context.caller()), 'tx already approved');
58
+ }
59
+
60
+ export function _notExecuted(txId: u64): void {
61
+ assert(!TRANSACTIONS.getSome(txId).executed, 'tx already executed');
62
+ }
63
+
64
+ export function _isMultisig(): void {
65
+ assert(Context.callee() == Context.caller(), 'not multisig');
66
+ }
67
+
68
+ // HELPERS
69
+
70
+ export function buildApprovalKey(txId: u64, owner: Address): string {
71
+ return txId.toString() + owner.toString();
72
+ }
73
+
74
+ export function addTransaction(transaction: Transaction): u64 {
75
+ const id = TRANSACTIONS.size();
76
+ // ensure transaction was not wrongly set
77
+ transaction.timestamp = 0;
78
+ transaction.executed = false;
79
+ TRANSACTIONS.set(id, transaction);
80
+ return id;
81
+ }
82
+
83
+ export function addOwner(owner: string): void {
84
+ assert(!IS_OWNER.contains(owner), 'already owner');
85
+
86
+ IS_OWNER.set(owner, true);
87
+
88
+ const _owners = owners();
89
+ _owners.push(owner);
90
+ setOwners(_owners);
91
+ }
92
+
93
+ export function removeOwner(owner: string): void {
94
+ assert(IS_OWNER.contains(owner), 'not owner');
95
+
96
+ IS_OWNER.delete(owner);
97
+
98
+ const _owners = owners();
99
+ const index = _owners.indexOf(owner);
100
+ _owners.splice(index, 1);
101
+ setOwners(_owners);
102
+ }
103
+
104
+ export function setOwners(owners: string[]): void {
105
+ Storage.set(OWNERS, new Args().add(owners).serialize());
106
+ }
@@ -0,0 +1,100 @@
1
+ import {
2
+ Args,
3
+ NoArg,
4
+ byteToBool,
5
+ bytesToI32,
6
+ bytesToU64,
7
+ stringToBytes,
8
+ } from '@massalabs/as-types';
9
+ import { Address, call, Storage } from '@massalabs/massa-as-sdk';
10
+ import { DELAY, OWNERS, REQUIRED } from '../storage/Multisig';
11
+ import { buildApprovalKey } from '../contracts/multisig-internals';
12
+
13
+ const APPROVED = 'approved';
14
+ export class IMultisig {
15
+ constructor(public _origin: Address) {}
16
+
17
+ init(
18
+ owners: string[],
19
+ required: i32,
20
+ upgradeDelay: u64,
21
+ validationDelay: u64,
22
+ ): void {
23
+ call(
24
+ this._origin,
25
+ 'constructor',
26
+ new Args()
27
+ .add(owners)
28
+ .add(required)
29
+ .add(upgradeDelay)
30
+ .add(validationDelay),
31
+ 0,
32
+ );
33
+ }
34
+
35
+ submit(to: string, method: string, value: u64, data: StaticArray<u8>): u64 {
36
+ const res = call(
37
+ this._origin,
38
+ 'submit',
39
+ new Args().add(to).add(method).add(value).add(data),
40
+ 0,
41
+ );
42
+ return bytesToU64(res);
43
+ }
44
+
45
+ receiveCoins(value: u64): void {
46
+ call(this._origin, 'receiveCoins', NoArg, value);
47
+ }
48
+
49
+ approve(txId: u64): void {
50
+ call(this._origin, 'approve', new Args().add(txId), 0);
51
+ }
52
+
53
+ execute(txId: u64): void {
54
+ call(this._origin, 'execute', new Args().add(txId), 0);
55
+ }
56
+
57
+ revoke(txId: u64): void {
58
+ call(this._origin, 'revoke', new Args().add(txId), 0);
59
+ }
60
+
61
+ getApprovalCount(txId: u64): i32 {
62
+ const _owners = this.owners();
63
+ let count = 0;
64
+ for (let i = 0; i < _owners.length; i++) {
65
+ if (this.hasApproved(txId, new Address(_owners[i]))) {
66
+ count++;
67
+ }
68
+ }
69
+ return count;
70
+ }
71
+
72
+ owners(): string[] {
73
+ return Storage.hasOf(this._origin, OWNERS)
74
+ ? new Args(Storage.getOf(this._origin, OWNERS)).nextStringArray().unwrap()
75
+ : [];
76
+ }
77
+
78
+ required(): i32 {
79
+ return bytesToI32(Storage.getOf(this._origin, REQUIRED));
80
+ }
81
+
82
+ delay(): i32 {
83
+ return bytesToU64(Storage.getOf(this._origin, DELAY));
84
+ }
85
+
86
+ hasApproved(txId: u64, owner: Address): bool {
87
+ return (
88
+ Storage.hasOf(
89
+ this._origin,
90
+ stringToBytes(APPROVED + '::' + buildApprovalKey(txId, owner)),
91
+ ) &&
92
+ byteToBool(
93
+ Storage.getOf(
94
+ this._origin,
95
+ stringToBytes(APPROVED + '::' + buildApprovalKey(txId, owner)),
96
+ ),
97
+ )
98
+ );
99
+ }
100
+ }