From 3b8cf92630cd65797c487306ea97c6be42e0883b Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Sat, 12 Aug 2023 14:41:15 +0400 Subject: [PATCH 01/25] first draft of a multisig smart contract --- smart-contracts/assembly/contracts/index.ts | 2 + .../assembly/contracts/multisig/index.ts | 1 + .../assembly/contracts/multisig/multisig.ts | 519 ++++++++++++++++++ 3 files changed, 522 insertions(+) create mode 100644 smart-contracts/assembly/contracts/multisig/index.ts create mode 100644 smart-contracts/assembly/contracts/multisig/multisig.ts diff --git a/smart-contracts/assembly/contracts/index.ts b/smart-contracts/assembly/contracts/index.ts index b41f39d..5e56918 100644 --- a/smart-contracts/assembly/contracts/index.ts +++ b/smart-contracts/assembly/contracts/index.ts @@ -1,7 +1,9 @@ import * as FT from './FT'; import * as NFT from './NFT'; +import * as multisig from './multisig'; import * as ownership from './utils/ownership'; export { FT }; export { NFT }; +export { multisig }; export { ownership }; diff --git a/smart-contracts/assembly/contracts/multisig/index.ts b/smart-contracts/assembly/contracts/multisig/index.ts new file mode 100644 index 0000000..8c0ee08 --- /dev/null +++ b/smart-contracts/assembly/contracts/multisig/index.ts @@ -0,0 +1 @@ +export * from './multisig'; diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts new file mode 100644 index 0000000..3ce939c --- /dev/null +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -0,0 +1,519 @@ +// ======================================================== // +// MULTISIG CONTRACT // +// // +// Accepts funds in a smart contract locked by a multisig // +// mechanism. Owners can be registered several times, // +// implementing a simple weighted signature scheme. // +// // +// Inspired by the solidity multisig contract here: // +// https://solidity-by-example.org/app/multi-sig-wallet // +// // +// ======================================================== // + +import { + Address, + Context, + Coins, + generateEvent, + Storage, + createEvent, + callerHasWriteAccess, +} from '@massalabs/massa-as-sdk'; + +import { Args, + Result, + u8toByte, + u64ToBytes, + stringToBytes, + bytesToU64, + byteToU8, + bytesToSerializableObjectArray, + serializableObjectsArrayToBytes } from '@massalabs/as-types'; + +const DEPOSIT_EVENT_NAME = 'DEPOSIT'; +const SUBMIT_TRANSACTION_EVENT_NAME = 'SUBMIT_TRANSACTION'; +const CONFIRM_TRANSACTION_EVENT_NAME = 'CONFIRM_TRANSACTION'; +const EXECUTE_TRANSACTION_EVENT_NAME = 'EXECUTE_TRANSACTION'; +const REVOKE_TRANSACTION_EVENT_NAME = 'REVOKE_TRANSACTION'; +const RETRIEVE_TRANSACTION_EVENT_NAME = 'RETRIEVE_TRANSACTION'; +const GET_OWNERS_EVENT_NAME = 'GET_OWNERS'; + +export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes('NB CONFIRMATIONS REQUIRED'); +export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); +export const TRANSACTION_INDEX_KEY = stringToBytes('TRANSACTION INDEX'); + + +// ======================================================== // +// ==== HELPER FUNCTIONS & TYPES ==== // +// ======================================================== // + +class Transaction { + toAddress: Address; // the destination address + amount: u64; // the amount + confirmedOwnerList: Array
; // the Array listing the owners who have already signed + confirmationWeightedSum: u8; // the confirmation total weight sum, for easy check + + constructor(toAddress: Address = new Address(), amount: u64 = 0) { + this.toAddress = toAddress; + this.amount = amount; + this.confirmedOwnerList = new Array
(); + this.confirmationWeightedSum = 0; + } + + serialize() : StaticArray { + + // create a serializable transaction record to store in Storage + // Here we store some redundant information, like the confirmation + // total weight sum, trading some Storage space for Compute space, + // knowing that the transaction will be erased from Storage once executed + const argTransaction = new Args() + .add
(this.toAddress) + .add(this.amount) + .addSerializableObjectArray>(this.confirmedOwnerList) + .add(this.confirmationWeightedSum); + return argTransaction.serialize(); + } + + deserialize(data: StaticArray) : void { + + const args = new Args(data); + + this.toAddress = args + .nextSerializable
() + .expect('Error while deserializing transaction toAddress'); + this.amount = args + .nextU64() + .expect('Error while deserializing transaction amount'); + this.confirmedOwnerList = args + .nextSerializableObjectArray
() + .expect('Error while deserializing transaction confirmedOwnerList'); + this.confirmationWeightedSum = args + .nextU8() + .expect('Error while deserializing transaction confirmationWeightedSum'); + } + + isAlreadyConfirmed(owner: Address) : bool { + return this.confirmedOwnerList.includes(owner); + } + + confirm(owner: Address, weight: u8) : void { + this.confirmedOwnerList.push(owner); + this.confirmationWeightedSum += weight; + } + + revoke(owner: Address, weight: u8) : void { + let newConfirmedOwnerList = new Array
(); + for (let i = 0; i < this.confirmedOwnerList.length; i++) { + let address = this.confirmedOwnerList[i]; + if (address != owner) + newConfirmedOwnerList.push(address); + } + this.confirmedOwnerList = newConfirmedOwnerList; + assert(this.confirmationWeightedSum >= weight, + "fatal error: confirmationWeightedSum is less than revoked weight!"); + this.confirmationWeightedSum -= weight; + } + + isValidated() : bool { + let nbConfirmationsRequired = byteToU8(Storage.get(NB_CONFIRMATIONS_REQUIRED_KEY)); + return this.confirmationWeightedSum >= nbConfirmationsRequired; + } +} + +function storeTransaction(txIndex: u64, + transaction: Transaction): void { + + // we simply use the transaction index as a key to store it + Storage.set(u64ToBytes(txIndex), transaction.serialize()); +} + +function retrieveTransaction(txIndex: u64): Result { + + const transactionKey = u64ToBytes(txIndex); + + if (Storage.has(transactionKey)) { + let transaction = new Transaction(); + transaction.deserialize(Storage.get(transactionKey)); + return new Result(transaction); + } + + return new Result(new Transaction(), "unknown or already executed transaction index") +} + +function deleteTransaction(txIndex: u64): void { + + const transactionKey = u64ToBytes(txIndex); + Storage.del(transactionKey); +} + +// ======================================================== // +// ==== CONSTRUCTOR ==== // +// ======================================================== // + + +/** + * Initialize the multisig wallet + * Can be called only once + * + * @example + * ```typescript + * constructor( + * new Args() + * .add(3) // nb of confirmations required + * .addSerializableObjectArray([Owner1Address, Owner2Address, ..., OwnerNAddress]) + * .serialize(), + * ); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the number of confirmations required (u8) + * - the Array of addresses for each owner of the multisig (Array
). + */ +export function constructor(stringifyArgs: StaticArray): void { + // This line is important. It ensures that this function can't be called in the future. + // If you remove this check, someone could call your constructor function and reset your smart contract. + assert(callerHasWriteAccess()); + + const args = new Args(stringifyArgs); + const MAX_OWNERS : i32 = 255; + + // initialize nb of confirmations required + const nbConfirmationsRequired = args + .nextU8() + .expect('Error while initializing nbConfirmationsRequired'); + + assert(nbConfirmationsRequired > 0 , + 'The number of confirmations required must be at least 1'); + + Storage.set(NB_CONFIRMATIONS_REQUIRED_KEY, u8toByte(nbConfirmationsRequired)); + + // initialize array of owners addresses + const ownerAddresses : Array
= args + .nextSerializableObjectArray
() + .expect('Error while initializing owners addresses array'); + + assert(ownerAddresses.length > 0 && ownerAddresses.length <= MAX_OWNERS, + 'The multisig must have between one and 255 owners'); + + assert(nbConfirmationsRequired as i32 <= ownerAddresses.length, + 'The number of confirmations cannot exceed the number of owners of the multisig'); + + let ownerWeight = new Map(); + for (let i = 0; i < ownerAddresses.length; i++) { + let address = ownerAddresses[i]; + assert(address.toString(), "null address is not a valid owner"); + const currentWeight = ownerWeight.get(address) || 0; // returns 0 if the key is not present + ownerWeight.set(address, currentWeight + 1); + } + + for (let i = 0; i < ownerAddresses.length; i++) { + let address = ownerAddresses[i]; + // we store directly each address weight in the Storage + Storage.set(address.serialize(), u8toByte(ownerWeight.get(address))); + } + + // We store the list of owners to be queries later if needed + Storage.set(OWNERS_ADDRESSES_KEY, serializableObjectsArrayToBytes(ownerAddresses)); + + // initialize transaction index + Storage.set(TRANSACTION_INDEX_KEY, u64ToBytes(0)); +} + +/** + * Returns the version of this smart contract. + * This versioning is following the best practices defined in https://semver.org/. + * + * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18 + * @returns contract version + */ +export function version(_: StaticArray): StaticArray { + return stringToBytes('0.0.0'); +} + +// ======================================================== // +// ==== COIN DEPOSIT ==== // +// ======================================================== // + +/** + * Accepts funds to credit the multisig wallet + * + * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18 + * @returns token name. + */ +export function deposit(_: StaticArray): void { + + generateEvent( + createEvent(DEPOSIT_EVENT_NAME, [ + Context.caller().toString(), + Context.transferredCoins().toString(), + Coins.balance().toString(), + ]), + ); +} + +// ======================================================== // +// ==== TRANSACTIONS ==== // +// ======================================================== // + +/** + * Submit a transaction and generate an event with its index number + * + * @example + * ```typescript + * submitTransaction( + * new Args() + * .add
(Address("...")) // destination address + * .add(150000) // amount + * .serialize(), + * ); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the destination address for the transfert (Address) + * - the amount of the transaction (u64). + * @returns transaction index. + */ +export function submitTransaction(stringifyArgs: StaticArray): u64 { + + const args = new Args(stringifyArgs); + + // initialize address + const toAddress = args + .nextSerializable
() + .expect('Error while initializing transaction address'); + + // initialize amount + const amount = args + .nextU64() + .expect('Error while initializing transaction amount'); + + let txIndex = bytesToU64(Storage.get(TRANSACTION_INDEX_KEY)); + txIndex++; + + storeTransaction(txIndex, new Transaction(toAddress, amount)); + + // update the new txIndex value for the next transaction + Storage.set(TRANSACTION_INDEX_KEY, u64ToBytes(txIndex)); + + generateEvent( + createEvent(SUBMIT_TRANSACTION_EVENT_NAME, [ + Context.caller().toString(), + txIndex.toString(), + toAddress.toString(), + amount.toString(), + ]), + ); + + return txIndex; +} + +/** + * Confirms a transaction by an owner, and generate an event + * + * @example + * ```typescript + * confirmTransaction( + * new Args() + * .add(index) // the transaction index + * .serialize(), + * ); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the transaction index (u64) + */ +export function confirmTransaction(stringifyArgs: StaticArray): void { + + const args = new Args(stringifyArgs); + + // initialize transaction index + const txIndex = args + .nextU64() + .expect('Error while initializing transaction index'); + + let owner = Context.caller(); + + // check owner is legit and retrieve the weight + let ownerKey = owner.serialize(); + assert(Storage.has(ownerKey), "Caller address is not an owner"); + let weight = byteToU8(Storage.get(ownerKey)); + + // check the transaction exists and retrieve it from Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + + // did we already confirm it? + assert(!transaction.isAlreadyConfirmed(owner), + "The caller address has already confirmed this transaction"); + + // confirm it and update the Storage + transaction.confirm(owner, weight); + storeTransaction(txIndex, transaction); + + generateEvent( + createEvent(CONFIRM_TRANSACTION_EVENT_NAME, [ + owner.toString(), + txIndex.toString() + ]), + ); +} + +/** + * Execute a transaction and generate an event in case of success + * + * @example + * ```typescript + * executeTransaction( + * new Args() + * .add(index) // the transaction index + * .serialize(), + * ); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the transaction index (u64) + */ +export function executeTransaction(stringifyArgs: StaticArray): void { + + const args = new Args(stringifyArgs); + + // initialize transaction index + const txIndex = args + .nextU64() + .expect('Error while initializing transaction index'); + + // check the transaction exists and retrieve it from Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + + // if the transaction is sufficiently confirmed, execute it + assert(transaction.isValidated(), + "The transaction is unsufficiently confirmed, cannot execute"); + Coins.transferCoins(transaction.toAddress, transaction.amount); + + // clean up Storage and remove executed transaction + // NB: we could decide to keep it for archive purposes but then the + // Storage cost would increase forever. + deleteTransaction(txIndex); + + generateEvent( + createEvent(EXECUTE_TRANSACTION_EVENT_NAME, [ + Context.caller().toString(), + txIndex.toString(), + transaction.toAddress.toString(), + transaction.amount.toString(), + ]), + ); +} + +/** + * Revoke a transaction confirmation by an owner, and generate an event + * + * @example + * ```typescript + * revokeConfirmation( + * new Args() + * .add(index) // the transaction index + * .serialize(), + * ); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the transaction index (u64) + */ +export function revokeConfirmation(stringifyArgs: StaticArray): void { + + const args = new Args(stringifyArgs); + + // initialize transaction index + const txIndex = args + .nextU64() + .expect('Error while initializing transaction index'); + + let owner = Context.caller(); + + // check owner is legit and retrieve the weight + let ownerKey = owner.serialize(); + assert(Storage.has(ownerKey), "Caller address is not an owner"); + let weight = byteToU8(Storage.get(ownerKey)); + + // check the transaction exists and retrieve it from Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + + // did we actually already confirmed it? + assert(transaction.isAlreadyConfirmed(owner), + "The caller address has not yet confirmed this transaction"); + + // revoke it and update the Storage + transaction.revoke(owner, weight); + storeTransaction(txIndex, transaction); + + generateEvent( + createEvent(REVOKE_TRANSACTION_EVENT_NAME, [ + owner.toString(), + txIndex.toString() + ]), + ); +} + +/** + * Retrieve the list of the multisig owners Addresses and emit an event + * + * @example + * ```typescript + * let owners = bytesToSerializableObjectArray
(getOwners()).unwrap(); + * ``` + * + */ +export function getOwners(_ : StaticArray) : StaticArray { + + let serializedOwnerAddresses = Storage.get(OWNERS_ADDRESSES_KEY) + let owners = bytesToSerializableObjectArray
(serializedOwnerAddresses).unwrap(); + + // generate the event with the list of owners + let eventPayLoad : Array = []; + for (let i = 0; i < owners.length; i++) + eventPayLoad.push(owners[i].toString()); + generateEvent(createEvent(GET_OWNERS_EVENT_NAME, eventPayLoad)); + + return serializedOwnerAddresses; +} + +/** + * Retrieve a currently stored transaction and generate an event + * + * @example + * ```typescript + * let transaction = new Transaction(); + * transaction.deserialize(getTransaction( + * new Args() + * .add(index) // the transaction index + * .serialize() + * )); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the transaction index (u64) + */ +export function getTransaction(stringifyArgs : StaticArray) : StaticArray { + + const args = new Args(stringifyArgs); + + // initialize transaction index + const txIndex = args + .nextU64() + .expect('Error while initializing transaction index'); + + // check the transaction exists and retrieve it from Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + + // generate the event with the list of confirmed owners + let eventPayLoad : Array = [ + txIndex.toString(), + transaction.toAddress.toString(), + transaction.amount.toString(), + transaction.confirmationWeightedSum.toString()]; + for (let i = 0; i < transaction.confirmedOwnerList.length; i++) + eventPayLoad.push(transaction.confirmedOwnerList[i].toString()); + generateEvent(createEvent(RETRIEVE_TRANSACTION_EVENT_NAME, eventPayLoad)); + + return transaction.serialize(); +} From e6535512530109fc3e307d1598a3948beb513937 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Tue, 15 Aug 2023 14:30:52 +0400 Subject: [PATCH 02/25] use string for owner addresses to make it easier on the typescript side. --- .../assembly/contracts/multisig/multisig.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 3ce939c..1d96a97 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -160,14 +160,14 @@ function deleteTransaction(txIndex: u64): void { * constructor( * new Args() * .add(3) // nb of confirmations required - * .addSerializableObjectArray([Owner1Address, Owner2Address, ..., OwnerNAddress]) + * .add>("Owner1Address", "Owner2Address", ..., "OwnerNAddress"]) * .serialize(), * ); * ``` * * @param stringifyArgs - Args object serialized as a string containing: * - the number of confirmations required (u8) - * - the Array of addresses for each owner of the multisig (Array
). + * - the Array of addresses for each owner of the multisig (Array). */ export function constructor(stringifyArgs: StaticArray): void { // This line is important. It ensures that this function can't be called in the future. @@ -188,10 +188,15 @@ export function constructor(stringifyArgs: StaticArray): void { Storage.set(NB_CONFIRMATIONS_REQUIRED_KEY, u8toByte(nbConfirmationsRequired)); // initialize array of owners addresses - const ownerAddresses : Array
= args - .nextSerializableObjectArray
() + const ownerStringAddresses : Array = args + .nextStringArray() .expect('Error while initializing owners addresses array'); + // convert to actual Addresses + const ownerAddresses : Array
= []; + for (let i = 0; i < ownerStringAddresses.length; i++) + ownerAddresses.push(new Address(ownerStringAddresses[i])); + assert(ownerAddresses.length > 0 && ownerAddresses.length <= MAX_OWNERS, 'The multisig must have between one and 255 owners'); From d1a9aaf42ac1d0838b949d57e3961b650ec9f9d2 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Tue, 15 Aug 2023 14:31:13 +0400 Subject: [PATCH 03/25] use prefixes before owner and txIndex keys to avoid collisions --- .../assembly/contracts/multisig/multisig.ts | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 1d96a97..a47cbf7 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -25,6 +25,7 @@ import { Args, u8toByte, u64ToBytes, stringToBytes, + bytesToString, bytesToU64, byteToU8, bytesToSerializableObjectArray, @@ -38,6 +39,9 @@ const REVOKE_TRANSACTION_EVENT_NAME = 'REVOKE_TRANSACTION'; const RETRIEVE_TRANSACTION_EVENT_NAME = 'RETRIEVE_TRANSACTION'; const GET_OWNERS_EVENT_NAME = 'GET_OWNERS'; +export const TRANSACTION_INDEX_PREFIX_KEY = '00'; +export const OWNER_PREFIX_KEY = '01'; + export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes('NB CONFIRMATIONS REQUIRED'); export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); export const TRANSACTION_INDEX_KEY = stringToBytes('TRANSACTION INDEX'); @@ -47,6 +51,18 @@ export const TRANSACTION_INDEX_KEY = stringToBytes('TRANSACTION INDEX'); // ==== HELPER FUNCTIONS & TYPES ==== // // ======================================================== // +function makeTransactionKey(txIndex: u64) : StaticArray { + + return stringToBytes(TRANSACTION_INDEX_PREFIX_KEY + + bytesToString(u64ToBytes(txIndex))); +} + +function makeOwnerKey(address: Address) : StaticArray { + + return stringToBytes(OWNER_PREFIX_KEY + + bytesToString(address.serialize())); +} + class Transaction { toAddress: Address; // the destination address amount: u64; // the amount @@ -124,12 +140,12 @@ function storeTransaction(txIndex: u64, transaction: Transaction): void { // we simply use the transaction index as a key to store it - Storage.set(u64ToBytes(txIndex), transaction.serialize()); + Storage.set(makeTransactionKey(txIndex), transaction.serialize()); } function retrieveTransaction(txIndex: u64): Result { - const transactionKey = u64ToBytes(txIndex); + const transactionKey = makeTransactionKey(txIndex); if (Storage.has(transactionKey)) { let transaction = new Transaction(); @@ -142,7 +158,7 @@ function retrieveTransaction(txIndex: u64): Result { function deleteTransaction(txIndex: u64): void { - const transactionKey = u64ToBytes(txIndex); + const transactionKey = makeTransactionKey(txIndex); Storage.del(transactionKey); } @@ -207,14 +223,16 @@ export function constructor(stringifyArgs: StaticArray): void { for (let i = 0; i < ownerAddresses.length; i++) { let address = ownerAddresses[i]; assert(address.toString(), "null address is not a valid owner"); - const currentWeight = ownerWeight.get(address) || 0; // returns 0 if the key is not present + let currentWeight : u8 = 0; + if (ownerWeight.has(address)) + currentWeight = ownerWeight.get(address); ownerWeight.set(address, currentWeight + 1); } for (let i = 0; i < ownerAddresses.length; i++) { let address = ownerAddresses[i]; // we store directly each address weight in the Storage - Storage.set(address.serialize(), u8toByte(ownerWeight.get(address))); + Storage.set(makeOwnerKey(address), u8toByte(ownerWeight.get(address))); } // We store the list of owners to be queries later if needed @@ -339,7 +357,7 @@ export function confirmTransaction(stringifyArgs: StaticArray): void { let owner = Context.caller(); // check owner is legit and retrieve the weight - let ownerKey = owner.serialize(); + let ownerKey = makeOwnerKey(owner); assert(Storage.has(ownerKey), "Caller address is not an owner"); let weight = byteToU8(Storage.get(ownerKey)); @@ -436,7 +454,7 @@ export function revokeConfirmation(stringifyArgs: StaticArray): void { let owner = Context.caller(); // check owner is legit and retrieve the weight - let ownerKey = owner.serialize(); + let ownerKey = makeOwnerKey(owner); assert(Storage.has(ownerKey), "Caller address is not an owner"); let weight = byteToU8(Storage.get(ownerKey)); From 23bf14fd6beddbf1d7bdfe3f26c450963039709b Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Thu, 17 Aug 2023 16:43:07 +0400 Subject: [PATCH 04/25] improve documentation, add a ms1_ prefix to multisig functions --- README.md | 4 +++ smart-contracts/README.md | 1 + .../assembly/contracts/multisig/multisig.ts | 30 +++++++++---------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7ca86a6..9ce2dc1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ The [Massa Domain Name Service standard](smart-contracts/assembly/contracts/dns/ This is MassaLabs implementation of [the ENS](https://docs.ens.domains/). +## Multisig + +The [Multisig standard implementation](smart-contracts/assembly/contracts/multisig) defines a simple multisig contract able to store funds and release them in a weighted multisig schema (multiple declaration of an owner will increase its weight). + ## Massa Units The [Massa Units standard](units.md) defines a set of common units of measurement for use on the Massa blockchain. diff --git a/smart-contracts/README.md b/smart-contracts/README.md index 865a89c..68492ce 100644 --- a/smart-contracts/README.md +++ b/smart-contracts/README.md @@ -2,6 +2,7 @@ - [fungible token](assembly/contracts/FT): implementation of the ERC20 token. - [non-fungible token](assembly/contracts/NFT) +- [multisig](assembly/contract/multisig) ## Documentation diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index a47cbf7..1a29ffd 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -249,7 +249,7 @@ export function constructor(stringifyArgs: StaticArray): void { * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18 * @returns contract version */ -export function version(_: StaticArray): StaticArray { +export function ms1_version(_: StaticArray): StaticArray { return stringToBytes('0.0.0'); } @@ -263,7 +263,7 @@ export function version(_: StaticArray): StaticArray { * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18 * @returns token name. */ -export function deposit(_: StaticArray): void { +export function ms1_deposit(_: StaticArray): void { generateEvent( createEvent(DEPOSIT_EVENT_NAME, [ @@ -283,7 +283,7 @@ export function deposit(_: StaticArray): void { * * @example * ```typescript - * submitTransaction( + * ms1_submitTransaction( * new Args() * .add
(Address("...")) // destination address * .add(150000) // amount @@ -296,7 +296,7 @@ export function deposit(_: StaticArray): void { * - the amount of the transaction (u64). * @returns transaction index. */ -export function submitTransaction(stringifyArgs: StaticArray): u64 { +export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { const args = new Args(stringifyArgs); @@ -335,7 +335,7 @@ export function submitTransaction(stringifyArgs: StaticArray): u64 { * * @example * ```typescript - * confirmTransaction( + * ms1_confirmTransaction( * new Args() * .add(index) // the transaction index * .serialize(), @@ -345,7 +345,7 @@ export function submitTransaction(stringifyArgs: StaticArray): u64 { * @param stringifyArgs - Args object serialized as a string containing: * - the transaction index (u64) */ -export function confirmTransaction(stringifyArgs: StaticArray): void { +export function ms1_confirmTransaction(stringifyArgs: StaticArray): void { const args = new Args(stringifyArgs); @@ -385,7 +385,7 @@ export function confirmTransaction(stringifyArgs: StaticArray): void { * * @example * ```typescript - * executeTransaction( + * ms1_executeTransaction( * new Args() * .add(index) // the transaction index * .serialize(), @@ -395,7 +395,7 @@ export function confirmTransaction(stringifyArgs: StaticArray): void { * @param stringifyArgs - Args object serialized as a string containing: * - the transaction index (u64) */ -export function executeTransaction(stringifyArgs: StaticArray): void { +export function ms1_executeTransaction(stringifyArgs: StaticArray): void { const args = new Args(stringifyArgs); @@ -432,7 +432,7 @@ export function executeTransaction(stringifyArgs: StaticArray): void { * * @example * ```typescript - * revokeConfirmation( + * ms1_revokeConfirmation( * new Args() * .add(index) // the transaction index * .serialize(), @@ -442,7 +442,7 @@ export function executeTransaction(stringifyArgs: StaticArray): void { * @param stringifyArgs - Args object serialized as a string containing: * - the transaction index (u64) */ -export function revokeConfirmation(stringifyArgs: StaticArray): void { +export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { const args = new Args(stringifyArgs); @@ -478,15 +478,15 @@ export function revokeConfirmation(stringifyArgs: StaticArray): void { } /** - * Retrieve the list of the multisig owners Addresses and emit an event + * Retrieve the list of the multisig owners addresses as strings and emit an event * * @example * ```typescript - * let owners = bytesToSerializableObjectArray
(getOwners()).unwrap(); + * let owners = bytesToSerializableObjectArray
(ms1_getOwners()).unwrap(); * ``` * */ -export function getOwners(_ : StaticArray) : StaticArray { +export function ms1_getOwners(_ : StaticArray) : StaticArray { let serializedOwnerAddresses = Storage.get(OWNERS_ADDRESSES_KEY) let owners = bytesToSerializableObjectArray
(serializedOwnerAddresses).unwrap(); @@ -506,7 +506,7 @@ export function getOwners(_ : StaticArray) : StaticArray { * @example * ```typescript * let transaction = new Transaction(); - * transaction.deserialize(getTransaction( + * transaction.deserialize(ms1_getTransaction( * new Args() * .add(index) // the transaction index * .serialize() @@ -516,7 +516,7 @@ export function getOwners(_ : StaticArray) : StaticArray { * @param stringifyArgs - Args object serialized as a string containing: * - the transaction index (u64) */ -export function getTransaction(stringifyArgs : StaticArray) : StaticArray { +export function ms1_getTransaction(stringifyArgs : StaticArray) : StaticArray { const args = new Args(stringifyArgs); From 53aac6b3bf736e61a499e15272460b8b5be7feae Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Fri, 18 Aug 2023 17:05:17 +0400 Subject: [PATCH 05/25] start to add some tests to multisig --- .../contracts/multisig/__tests__/as-pect.d.ts | 1 + .../multisig/__tests__/multisig.spec.ts | 348 ++++++++++++++++++ .../assembly/contracts/multisig/multisig.ts | 20 +- 3 files changed, 359 insertions(+), 10 deletions(-) create mode 100644 smart-contracts/assembly/contracts/multisig/__tests__/as-pect.d.ts create mode 100644 smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/as-pect.d.ts b/smart-contracts/assembly/contracts/multisig/__tests__/as-pect.d.ts new file mode 100644 index 0000000..6101ea2 --- /dev/null +++ b/smart-contracts/assembly/contracts/multisig/__tests__/as-pect.d.ts @@ -0,0 +1 @@ +/// diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts new file mode 100644 index 0000000..1e7ebb9 --- /dev/null +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -0,0 +1,348 @@ +import { + ms1_deposit, + ms1_submitTransaction, + ms1_confirmTransaction, + ms1_executeTransaction, + ms1_revokeConfirmation, + ms1_getOwners, + ms1_getTransaction, + constructor, + Transaction, + retrieveTransaction +} from '../multisig'; + +import { Storage, + mockAdminContext, + Address, + createEvent, + generateEvent } from '@massalabs/massa-as-sdk'; + +import { + Args, + byteToBool, + byteToU8, + bytesToU64, + stringToBytes, + bytesToString, + serializableObjectsArrayToBytes, + bytesToSerializableObjectArray, + Serializable +} from '@massalabs/as-types'; + +import { + changeCallStack, + resetStorage, +} from '@massalabs/massa-as-sdk/assembly/vm-mock/storage'; + +// address of admin caller set in vm-mock. must match with adminAddress of @massalabs/massa-as-sdk/vm-mock/vm.js +const deployerAddress = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'; + +// address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js +const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT'; + +// nb of confirmations required +const nbConfirmations : u8 = 2; + +// the multisig owners +const owners : Array = [ + 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + 'AU1qDAxGJ387ETi9JRQzZWSPKYq4YPXrFvdiE4VoXUaiAt38JFEC', + 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb' +]; + +// where transaction funds are sent +const destination = 'AU155TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb'; + +const ownerList = [owners[0], owners[0], owners[1], owners[2]]; +const ownerWeight : Array = [2, 1, 1]; + +export const TRANSACTION_INDEX_PREFIX_KEY = '00'; +export const OWNER_PREFIX_KEY = '01'; + +export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes('NB CONFIRMATIONS REQUIRED'); +export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); +export const TRANSACTION_INDEX_KEY = stringToBytes('TRANSACTION INDEX'); + +function makeTransactionKey(txIndex: u64) : StaticArray { + return stringToBytes(TRANSACTION_INDEX_PREFIX_KEY + + bytesToString(u64ToBytes(txIndex))); +} + +function makeOwnerKey(owner: string) : StaticArray { + return stringToBytes(OWNER_PREFIX_KEY + + bytesToString(new Args().add(owner).serialize())); +} + +class SerializableString implements Serializable { + s: string; + + constructor(s: string = '') { + this.s = s; + } + + public serialize(): StaticArray { + + return stringToBytes(this.s); + } + + public deserialize( + data: StaticArray, + offset: i32, + ): Result { + + this.s = bytesToString(data); + return Result(0); + } +} + +function switchUser(user: string): void { + changeCallStack(user + ' , ' + contractAddr); +} + +beforeAll(() => { + resetStorage(); + mockAdminContext(true); +}); + +describe('Multisig contract tests', () => { + test('constructor', () => { + + //--------------------------- + // check invalid constructors + + // 0 confirmations + expect(() => { + const serializedArgs = new Args() + .add(u8(0)) + .add>(ownerList) + .serialize(); + constructor(serializedArgs); + }).toThrow(); + + // no owners + expect(() => { + const serializedArgs = new Args() + .add(u8(1)) + .add>([]) + .serialize(); + constructor(serializedArgs); + }).toThrow(); + + // invalid args + expect(() => { + constructor([]); + }).toThrow(); + + resetStorage(); + + //--------------------------- + // define a valid constructor + const serializedArgs = new Args() + .add(nbConfirmations) + .add>(ownerList) + .serialize(); + constructor(serializedArgs); + + // check the nb of confirmation required is properly stored + expect(byteToU8(Storage.get(NB_CONFIRMATIONS_REQUIRED_KEY))).toBe(nbConfirmations); + + // compare the array of addresses as string to the array of Address in storage + let serializableStringList : Array = []; + for (let i = 0; i < ownerList.length; ++i) + serializableStringList.push(new SerializableString(ownerList[i])); + let ownersFromStorage = bytesToSerializableObjectArray
( + Storage.get(OWNERS_ADDRESSES_KEY)).unwrap(); + let serializableOwnerStringList : Array = []; + for (let i = 0; i < ownersFromStorage.length; ++i) + serializableOwnerStringList.push(new SerializableString(ownersFromStorage[i].toString())); + expect(serializableObjectsArrayToBytes(serializableOwnerStringList)) + .toStrictEqual(serializableObjectsArrayToBytes(serializableStringList)); + + // check the weight of each owner + expect(byteToU8(Storage.get(makeOwnerKey(owners[0])))).toBe(2); + expect(byteToU8(Storage.get(makeOwnerKey(owners[1])))).toBe(1); + expect(byteToU8(Storage.get(makeOwnerKey(owners[2])))).toBe(1); + + // check that the index transaction is set to 0 + expect(bytesToU64(Storage.get(TRANSACTION_INDEX_KEY))).toBe(0); + }); + + test('submit transaction', () => { + + // expect the transaction index to be 1 + expect(ms1_submitTransaction(new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize() + )).toBe(1); + + // check that the transaction is correctly stored + let transactionResult = retrieveTransaction(1); + expect(transactionResult.isOk()).toBe(true); + + // check the transaction content + let transaction = transactionResult.unwrap(); + expect(transaction.toAddress).toBe(new Address(destination)); + expect(transaction.amount).toBe(u64(15000)); + expect(transaction.confirmedOwnerList.length).toBe(0); + expect(transaction.confirmationWeightedSum).toBe(0); + expect(transaction.isAlreadyConfirmed(new Address(owners[0]))).toBe(false); + expect(transaction.isValidated()).toBe(false); + }); + + // indexes here refer to owner number: owner1, owner2, ... + //confirmTransaction([0], true, 2); + //confirmTransaction([1], false, 3); + //confirmTransaction([1, 2], true, 4); + //confirmTransaction([2], false, 5); + + test('confirm transaction [owners[0]]', () => { + let confirmingOwnersIndexes : Array; + let txIndex : u64; + let totalWeight : u8; + + confirmingOwnersIndexes = [0]; + txIndex = 2; + + expect(ms1_submitTransaction(new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize() + )).toBe(txIndex); + + totalWeight = 0; + for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { + let ownerAddress = owners[confirmingOwnersIndexes[i]]; + switchUser(ownerAddress); + ms1_confirmTransaction(new Args().add(txIndex).serialize()); + totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; + + // retrieve the transaction in its current state in Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + + expect(transaction.toAddress).toBe(new Address(destination)); + expect(transaction.amount).toBe(u64(15000)); + expect(transaction.confirmedOwnerList.length).toBe(i + 1); + expect(transaction.confirmationWeightedSum).toBe(totalWeight); + expect(transaction.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + } + + switchUser(deployerAddress); + // retrieve the transaction in its final state in Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + expect(transaction.isValidated()).toBe(true); + }); + + + test('confirm transaction [owners[1]]', () => { + let confirmingOwnersIndexes : Array; + let txIndex : u64; + let totalWeight : u8; + + confirmingOwnersIndexes = [1]; + txIndex = 3; + + expect(ms1_submitTransaction(new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize() + )).toBe(txIndex); + + totalWeight = 0; + for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { + let ownerAddress = owners[confirmingOwnersIndexes[i]]; + switchUser(ownerAddress); + ms1_confirmTransaction(new Args().add(txIndex).serialize()); + totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; + + // retrieve the transaction in its current state in Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + + expect(transaction.toAddress).toBe(new Address(destination)); + expect(transaction.amount).toBe(u64(15000)); + expect(transaction.confirmedOwnerList.length).toBe(i + 1); + expect(transaction.confirmationWeightedSum).toBe(totalWeight); + expect(transaction.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + } + + switchUser(deployerAddress); + // retrieve the transaction in its final state in Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + expect(transaction.isValidated()).toBe(false); + }); + + test('confirm transaction [owners[1], owners[2]]', () => { + let confirmingOwnersIndexes : Array; + let txIndex : u64; + let totalWeight : u8; + + confirmingOwnersIndexes = [1,2]; + txIndex = 4; + + expect(ms1_submitTransaction(new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize() + )).toBe(txIndex); + + totalWeight = 0; + for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { + let ownerAddress = owners[confirmingOwnersIndexes[i]]; + switchUser(ownerAddress); + ms1_confirmTransaction(new Args().add(txIndex).serialize()); + totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; + + // retrieve the transaction in its current state in Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + + expect(transaction.toAddress).toBe(new Address(destination)); + expect(transaction.amount).toBe(u64(15000)); + expect(transaction.confirmedOwnerList.length).toBe(i + 1); + expect(transaction.confirmationWeightedSum).toBe(totalWeight); + expect(transaction.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + } + + switchUser(deployerAddress); + // retrieve the transaction in its final state in Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + expect(transaction.isValidated()).toBe(true); + }); + + test('confirm transaction [owners[2]]', () => { + let confirmingOwnersIndexes : Array; + let txIndex : u64; + let totalWeight : u8; + + confirmingOwnersIndexes = [2]; + txIndex = 5; + + expect(ms1_submitTransaction(new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize() + )).toBe(txIndex); + + totalWeight = 0; + for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { + let ownerAddress = owners[confirmingOwnersIndexes[i]]; + switchUser(ownerAddress); + ms1_confirmTransaction(new Args().add(txIndex).serialize()); + totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; + + // retrieve the transaction in its current state in Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + + expect(transaction.toAddress).toBe(new Address(destination)); + expect(transaction.amount).toBe(u64(15000)); + expect(transaction.confirmedOwnerList.length).toBe(i + 1); + expect(transaction.confirmationWeightedSum).toBe(totalWeight); + expect(transaction.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + } + + switchUser(deployerAddress); + // retrieve the transaction in its final state in Storage + let transaction = retrieveTransaction(txIndex).unwrap(); + expect(transaction.isValidated()).toBe(false); + }); + +}); diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 1a29ffd..f4e4817 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -63,7 +63,7 @@ function makeOwnerKey(address: Address) : StaticArray { bytesToString(address.serialize())); } -class Transaction { +export class Transaction { toAddress: Address; // the destination address amount: u64; // the amount confirmedOwnerList: Array
; // the Array listing the owners who have already signed @@ -136,14 +136,14 @@ class Transaction { } } -function storeTransaction(txIndex: u64, +export function storeTransaction(txIndex: u64, transaction: Transaction): void { // we simply use the transaction index as a key to store it Storage.set(makeTransactionKey(txIndex), transaction.serialize()); } -function retrieveTransaction(txIndex: u64): Result { +export function retrieveTransaction(txIndex: u64): Result { const transactionKey = makeTransactionKey(txIndex); @@ -219,10 +219,10 @@ export function constructor(stringifyArgs: StaticArray): void { assert(nbConfirmationsRequired as i32 <= ownerAddresses.length, 'The number of confirmations cannot exceed the number of owners of the multisig'); - let ownerWeight = new Map(); + let ownerWeight = new Map(); for (let i = 0; i < ownerAddresses.length; i++) { - let address = ownerAddresses[i]; - assert(address.toString(), "null address is not a valid owner"); + let address = ownerAddresses[i].toString(); + assert(address, "null address is not a valid owner"); let currentWeight : u8 = 0; if (ownerWeight.has(address)) currentWeight = ownerWeight.get(address); @@ -232,7 +232,7 @@ export function constructor(stringifyArgs: StaticArray): void { for (let i = 0; i < ownerAddresses.length; i++) { let address = ownerAddresses[i]; // we store directly each address weight in the Storage - Storage.set(makeOwnerKey(address), u8toByte(ownerWeight.get(address))); + Storage.set(makeOwnerKey(address), u8toByte(ownerWeight.get(address.toString()))); } // We store the list of owners to be queries later if needed @@ -285,9 +285,9 @@ export function ms1_deposit(_: StaticArray): void { * ```typescript * ms1_submitTransaction( * new Args() - * .add
(Address("...")) // destination address + * .add
(new Address("...")) // destination address * .add(150000) // amount - * .serialize(), + * .serialize() * ); * ``` * @@ -323,7 +323,7 @@ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { Context.caller().toString(), txIndex.toString(), toAddress.toString(), - amount.toString(), + amount.toString() ]), ); From 75692fd69af8f5c445dafbdef2357c7493b227b8 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Sat, 19 Aug 2023 17:12:05 +0400 Subject: [PATCH 06/25] more test, use transferCoinsOf instead of transferCoins --- .../multisig/__tests__/multisig.spec.ts | 140 ++++++++++++++++-- .../assembly/contracts/multisig/multisig.ts | 2 +- 2 files changed, 128 insertions(+), 14 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 1e7ebb9..2259e92 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -15,6 +15,7 @@ import { Storage, mockAdminContext, Address, createEvent, + Coins, generateEvent } from '@massalabs/massa-as-sdk'; import { @@ -190,12 +191,7 @@ describe('Multisig contract tests', () => { expect(transaction.isValidated()).toBe(false); }); - // indexes here refer to owner number: owner1, owner2, ... - //confirmTransaction([0], true, 2); - //confirmTransaction([1], false, 3); - //confirmTransaction([1, 2], true, 4); - //confirmTransaction([2], false, 5); - + // validated transaction test('confirm transaction [owners[0]]', () => { let confirmingOwnersIndexes : Array; let txIndex : u64; @@ -233,7 +229,7 @@ describe('Multisig contract tests', () => { expect(transaction.isValidated()).toBe(true); }); - + // non validated transaction test('confirm transaction [owners[1]]', () => { let confirmingOwnersIndexes : Array; let txIndex : u64; @@ -271,12 +267,13 @@ describe('Multisig contract tests', () => { expect(transaction.isValidated()).toBe(false); }); - test('confirm transaction [owners[1], owners[2]]', () => { + // non validated transaction + test('confirm transaction [owners[2]]', () => { let confirmingOwnersIndexes : Array; let txIndex : u64; let totalWeight : u8; - confirmingOwnersIndexes = [1,2]; + confirmingOwnersIndexes = [2]; txIndex = 4; expect(ms1_submitTransaction(new Args() @@ -305,15 +302,16 @@ describe('Multisig contract tests', () => { switchUser(deployerAddress); // retrieve the transaction in its final state in Storage let transaction = retrieveTransaction(txIndex).unwrap(); - expect(transaction.isValidated()).toBe(true); + expect(transaction.isValidated()).toBe(false); }); - test('confirm transaction [owners[2]]', () => { + // validated transaction + test('confirm transaction [owners[1], owners[2]]', () => { let confirmingOwnersIndexes : Array; let txIndex : u64; let totalWeight : u8; - confirmingOwnersIndexes = [2]; + confirmingOwnersIndexes = [1,2]; txIndex = 5; expect(ms1_submitTransaction(new Args() @@ -342,7 +340,123 @@ describe('Multisig contract tests', () => { switchUser(deployerAddress); // retrieve the transaction in its final state in Storage let transaction = retrieveTransaction(txIndex).unwrap(); - expect(transaction.isValidated()).toBe(false); + expect(transaction.isValidated()).toBe(true); + }); + + // transaction 5 is validated, let's execute it + test('execute transaction with success', () => { + let destinationBalance = Coins.balanceOf(destination); + let contractBalance = Coins.balanceOf(contractAddr); + let initDestinationBalance = destinationBalance; + let initContractBalance = contractBalance; + + generateEvent( + createEvent("BALANCES BEFORE", + [initDestinationBalance.toString(), initContractBalance.toString()] + )); + + expect(() => { + ms1_executeTransaction(new Args().add(u64(5)).serialize()); + }).not.toThrow(); + + // once executed, the transaction is deleted + expect(() => { + ms1_getTransaction(new Args().add(u64(5)).serialize()) + }).toThrow(); + + destinationBalance = Coins.balanceOf(destination); + contractBalance = Coins.balanceOf(contractAddr); + generateEvent( + createEvent("BALANCES AFTER", + [destinationBalance.toString(), contractBalance.toString()] + )); + + // check that the transfer has been done + expect(destinationBalance).toBe(initDestinationBalance + 15000); + expect(contractBalance + 15000).toBe(initContractBalance); + }); + + // transaction 4 is not validated, let's try to execute it + test('execute transaction with failure', () => { + let destinationBalance = Coins.balanceOf(destination); + let contractBalance = Coins.balanceOf(contractAddr); + let initDestinationBalance = destinationBalance; + let initContractBalance = contractBalance; + + generateEvent( + createEvent("BALANCES BEFORE", + [initDestinationBalance.toString(), initContractBalance.toString()] + )); + + expect(() => { + ms1_executeTransaction(new Args().add(u64(4)).serialize()); + }).toThrow(); + + // the transaction is not supposed to be deleted + expect(() => { + ms1_getTransaction(new Args().add(u64(4)).serialize()) + }).not.toThrow(); + + destinationBalance = Coins.balanceOf(destination); + contractBalance = Coins.balanceOf(contractAddr); + generateEvent( + createEvent("BALANCES AFTER", + [destinationBalance.toString(), contractBalance.toString()] + )); + + // check that the transfer has not been done + expect(destinationBalance).toBe(initDestinationBalance); + expect(contractBalance).toBe(initContractBalance); }); + // transaction 2 is validated by owners[0]. + // now owners[0] will revoke it and we will try to execute it. + test('revoke transaction', () => { + let destinationBalance = Coins.balanceOf(destination); + let contractBalance = Coins.balanceOf(contractAddr); + let initDestinationBalance = destinationBalance; + let initContractBalance = contractBalance; + + switchUser(owners[0]); + expect(() => { + ms1_revokeConfirmation(new Args().add(u64(2)).serialize()); + }).not.toThrow(); + + switchUser(deployerAddress); + generateEvent( + createEvent("BALANCES BEFORE", + [initDestinationBalance.toString(), initContractBalance.toString()] + )); + + expect(() => { + ms1_executeTransaction(new Args().add(u64(2)).serialize()); + }).toThrow(); + + // the transaction should not have been deleted + expect(() => { + ms1_getTransaction(new Args().add(u64(2)).serialize()) + }).not.toThrow(); + + + // retrieve the transaction in its current state in Storage + let transaction = retrieveTransaction(u64(2)).unwrap(); + + expect(transaction.toAddress).toBe(new Address(destination)); + expect(transaction.amount).toBe(u64(15000)); + expect(transaction.confirmedOwnerList.length).toBe(0); + expect(transaction.confirmationWeightedSum).toBe(0); + expect(transaction.isAlreadyConfirmed(new Address(owners[0]))).toBe(false); + expect(transaction.isValidated()).toBe(false); + + destinationBalance = Coins.balanceOf(destination); + contractBalance = Coins.balanceOf(contractAddr); + generateEvent( + createEvent("BALANCES AFTER", + [destinationBalance.toString(), contractBalance.toString()] + )); + + // check that the transfer has not been done + expect(destinationBalance).toBe(initDestinationBalance); + expect(contractBalance).toBe(initContractBalance); + }); }); diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index f4e4817..6d0ac0f 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -410,7 +410,7 @@ export function ms1_executeTransaction(stringifyArgs: StaticArray): void { // if the transaction is sufficiently confirmed, execute it assert(transaction.isValidated(), "The transaction is unsufficiently confirmed, cannot execute"); - Coins.transferCoins(transaction.toAddress, transaction.amount); + Coins.transferCoinsOf(Context.callee(), transaction.toAddress, transaction.amount); // clean up Storage and remove executed transaction // NB: we could decide to keep it for archive purposes but then the From 59be132c4f3182bb738dee953c81d3ac8f999e4e Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Sun, 20 Aug 2023 10:38:23 +0400 Subject: [PATCH 07/25] add a wrapper for multisig --- .../assembly/contracts/multisig/index.ts | 1 + .../assembly/contracts/multisig/multisig.ts | 33 +++++ .../contracts/multisig/multisigWrapper.ts | 136 ++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 smart-contracts/assembly/contracts/multisig/multisigWrapper.ts diff --git a/smart-contracts/assembly/contracts/multisig/index.ts b/smart-contracts/assembly/contracts/multisig/index.ts index 8c0ee08..898917c 100644 --- a/smart-contracts/assembly/contracts/multisig/index.ts +++ b/smart-contracts/assembly/contracts/multisig/index.ts @@ -1 +1,2 @@ export * from './multisig'; +export * from './multisigWrapper'; \ No newline at end of file diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 6d0ac0f..af052bd 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -28,6 +28,7 @@ import { Args, bytesToString, bytesToU64, byteToU8, + boolToByte, bytesToSerializableObjectArray, serializableObjectsArrayToBytes } from '@massalabs/as-types'; @@ -156,6 +157,11 @@ export function retrieveTransaction(txIndex: u64): Result { return new Result(new Transaction(), "unknown or already executed transaction index") } +export function hasTransaction(txIndex: u64): bool { + + return Storage.has(makeTransactionKey(txIndex)); +} + function deleteTransaction(txIndex: u64): void { const transactionKey = makeTransactionKey(txIndex); @@ -540,3 +546,30 @@ export function ms1_getTransaction(stringifyArgs : StaticArray) : StaticArra return transaction.serialize(); } + +/** + * Check if the transaction defined by its index is a currently stored + * transaction. + * @example + * ```typescript + * ms1_hasTransaction( + * new Args() + * .add(index) // the transaction index + * .serialize(), + * ); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the transaction index (u64) + */ +export function ms1_hasTransaction(stringifyArgs : StaticArray) : StaticArray { + + const args = new Args(stringifyArgs); + + // initialize transaction index + const txIndex = args + .nextU64() + .expect('Error while initializing transaction index'); + + return boolToByte(hasTransaction(txIndex)); +} diff --git a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts new file mode 100644 index 0000000..9581376 --- /dev/null +++ b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts @@ -0,0 +1,136 @@ +import { Address, call } from '@massalabs/massa-as-sdk'; +import { Args, + NoArg, + bytesToString, + byteToBool, + bytesToSerializableObjectArray, + bytesToU64 } from '@massalabs/as-types'; + +/** + * The Massa's standard multisig implementation wrapper. + * + * @remarks + * This class can be used to wrap a smart contract implementing + * Massa standard multisig. + * All the serialization/deserialization will be handled here. + * + * @example + * ```typescript + * const multisig = new MultisigWrapper(MultisigAddr); + * multisig.confirmTransaction(txIndex); + * ``` + */ +export class MultisigWrapper { + _origin: Address; + + /** + * Wraps a smart contract exposing standard multisig. + * + * @param at - Address of the contract + */ + constructor(at: Address) { + this._origin = at; + } + + /** + * Deposit tokens in the multisig + */ + deposit(amount: u64): void { + call(this._origin, 'ms1_deposit', NoArg, amount); + } + + /** + * Submit a transaction, and retrieve its index to be used by + * the multisig owners to confirm it. + * + * @param toAddress - recipient address + * @param amount - amount to transfer + * + * @returns the transaction index + */ + submitTransaction(toAddress: Address, amount: u64): u64 { + return bytesToU64( + call(this._origin, + 'ms1_submitTransaction', + new Args().add
(toAddress).add(amount).serialize(), + 0)); + } + + /** + * Confirm a transaction identified by its index. + * + * @param txIndex - the transaction index + */ + confirmTransaction(txIndex: u64): void { + call(this._origin, + 'ms1_confirmTransaction', + new Args().add(txIndex).serialize(), + 0); + } + + /** + * Execute a transaction if it has enough validation from owners + * + * @param txIndex - the transaction index + */ + executeTransaction(txIndex: u64): void { + call(this._origin, + 'ms1_executeTransaction', + new Args().add(txIndex).serialize(), + 0); + } + + /** + * Revoke a transaction identified by its index. + * + * @param txIndex - the transaction index + */ + revokeTransaction(txIndex: u64): void { + call(this._origin, + 'ms1_revokeTransaction', + new Args().add(txIndex).serialize(), + 0); + } + + /** + * Get the list of owners of the multisig. + * + * @returns the list of owners addresses + */ + getOwners(): Array
{ + return bytesToSerializableObjectArray
( + call(this._origin, 'ms1_getOwners', NoArgs, 0)) + .unwrap(); + } + + /** + * Get a transaction identified by its index. + * Will throw if the transaction index does not exist. + * + * @returns the transaction + */ + getTransaction(txIndex: u64): Transaction { + let transaction = new Transaction(); + transaction.deserialize( + call(this._origin, + 'ms1_getTransaction', + new Args().add(txIndex).serialize(), + 0)); + + return transaction; + } + + /** + * Check if a transaction identified by its index is pending. + * + * @returns true if the transaction is defined and pending execution. + */ + hasTransaction(txIndex: u64): bool { + return byteToBool ( + call(this._origin, + 'ms1_hasTransaction', + new Args().add(txIndex).serialize(), + 0)); + } + +} From e119801cdfb08a7482e9a00be7e98a66ccba68dc Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Sun, 20 Aug 2023 17:04:59 +0400 Subject: [PATCH 08/25] rename Transaction->Operation to prepare for generalization to sc calls --- .../multisig/__tests__/multisig.spec.ts | 244 +++++++-------- .../assembly/contracts/multisig/multisig.ts | 280 +++++++++--------- .../contracts/multisig/multisigWrapper.ts | 73 +++-- 3 files changed, 298 insertions(+), 299 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 2259e92..2899be1 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -1,14 +1,14 @@ import { ms1_deposit, - ms1_submitTransaction, - ms1_confirmTransaction, - ms1_executeTransaction, + ms1_submitOperation, + ms1_confirmOperation, + ms1_executeOperation, ms1_revokeConfirmation, ms1_getOwners, - ms1_getTransaction, + ms1_getOperation, constructor, - Transaction, - retrieveTransaction + Operation, + retrieveOperation } from '../multisig'; import { Storage, @@ -51,22 +51,22 @@ const owners : Array = [ 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb' ]; -// where transaction funds are sent +// where operation funds are sent const destination = 'AU155TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb'; const ownerList = [owners[0], owners[0], owners[1], owners[2]]; const ownerWeight : Array = [2, 1, 1]; -export const TRANSACTION_INDEX_PREFIX_KEY = '00'; +export const OPERATION_INDEX_PREFIX_KEY = '00'; export const OWNER_PREFIX_KEY = '01'; export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes('NB CONFIRMATIONS REQUIRED'); export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); -export const TRANSACTION_INDEX_KEY = stringToBytes('TRANSACTION INDEX'); +export const OPERATION_INDEX_KEY = stringToBytes('OPERATION INDEX'); -function makeTransactionKey(txIndex: u64) : StaticArray { - return stringToBytes(TRANSACTION_INDEX_PREFIX_KEY + - bytesToString(u64ToBytes(txIndex))); +function makeOperationKey(opIndex: u64) : StaticArray { + return stringToBytes(OPERATION_INDEX_PREFIX_KEY + + bytesToString(u64ToBytes(opIndex))); } function makeOwnerKey(owner: string) : StaticArray { @@ -136,8 +136,8 @@ describe('Multisig contract tests', () => { resetStorage(); - //--------------------------- - // define a valid constructor + //------------------------------------------------------- + // define a valid constructor for a transaction operation const serializedArgs = new Args() .add(nbConfirmations) .add>(ownerList) @@ -164,187 +164,187 @@ describe('Multisig contract tests', () => { expect(byteToU8(Storage.get(makeOwnerKey(owners[1])))).toBe(1); expect(byteToU8(Storage.get(makeOwnerKey(owners[2])))).toBe(1); - // check that the index transaction is set to 0 - expect(bytesToU64(Storage.get(TRANSACTION_INDEX_KEY))).toBe(0); + // check that the operation index is set to 0 + expect(bytesToU64(Storage.get(OPERATION_INDEX_KEY))).toBe(0); }); - test('submit transaction', () => { + test('submit transaction operation', () => { - // expect the transaction index to be 1 - expect(ms1_submitTransaction(new Args() + // expect the operation index to be 1 + expect(ms1_submitOperation(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() )).toBe(1); - // check that the transaction is correctly stored - let transactionResult = retrieveTransaction(1); - expect(transactionResult.isOk()).toBe(true); - - // check the transaction content - let transaction = transactionResult.unwrap(); - expect(transaction.toAddress).toBe(new Address(destination)); - expect(transaction.amount).toBe(u64(15000)); - expect(transaction.confirmedOwnerList.length).toBe(0); - expect(transaction.confirmationWeightedSum).toBe(0); - expect(transaction.isAlreadyConfirmed(new Address(owners[0]))).toBe(false); - expect(transaction.isValidated()).toBe(false); + // check that the operation is correctly stored + let operationResult = retrieveOperation(1); + expect(operationResult.isOk()).toBe(true); + + // check the operation content + let operation = operationResult.unwrap(); + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(0); + expect(operation.confirmationWeightedSum).toBe(0); + expect(operation.isAlreadyConfirmed(new Address(owners[0]))).toBe(false); + expect(operation.isValidated()).toBe(false); }); - // validated transaction - test('confirm transaction [owners[0]]', () => { + // validated operation + test('confirm transaction operation [owners[0]]', () => { let confirmingOwnersIndexes : Array; - let txIndex : u64; + let opIndex : u64; let totalWeight : u8; confirmingOwnersIndexes = [0]; - txIndex = 2; + opIndex = 2; - expect(ms1_submitTransaction(new Args() + expect(ms1_submitOperation(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() - )).toBe(txIndex); + )).toBe(opIndex); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { let ownerAddress = owners[confirmingOwnersIndexes[i]]; switchUser(ownerAddress); - ms1_confirmTransaction(new Args().add(txIndex).serialize()); + ms1_confirmOperation(new Args().add(opIndex).serialize()); totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; - // retrieve the transaction in its current state in Storage - let transaction = retrieveTransaction(txIndex).unwrap(); + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(opIndex).unwrap(); - expect(transaction.toAddress).toBe(new Address(destination)); - expect(transaction.amount).toBe(u64(15000)); - expect(transaction.confirmedOwnerList.length).toBe(i + 1); - expect(transaction.confirmationWeightedSum).toBe(totalWeight); - expect(transaction.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(i + 1); + expect(operation.confirmationWeightedSum).toBe(totalWeight); + expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); } switchUser(deployerAddress); - // retrieve the transaction in its final state in Storage - let transaction = retrieveTransaction(txIndex).unwrap(); - expect(transaction.isValidated()).toBe(true); + // retrieve the operation in its final state in Storage + let operation = retrieveOperation(opIndex).unwrap(); + expect(operation.isValidated()).toBe(true); }); - // non validated transaction - test('confirm transaction [owners[1]]', () => { + // non validated operation + test('confirm transaction operation [owners[1]]', () => { let confirmingOwnersIndexes : Array; - let txIndex : u64; + let opIndex : u64; let totalWeight : u8; confirmingOwnersIndexes = [1]; - txIndex = 3; + opIndex = 3; - expect(ms1_submitTransaction(new Args() + expect(ms1_submitOperation(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() - )).toBe(txIndex); + )).toBe(opIndex); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { let ownerAddress = owners[confirmingOwnersIndexes[i]]; switchUser(ownerAddress); - ms1_confirmTransaction(new Args().add(txIndex).serialize()); + ms1_confirmOperation(new Args().add(opIndex).serialize()); totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; - // retrieve the transaction in its current state in Storage - let transaction = retrieveTransaction(txIndex).unwrap(); + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(opIndex).unwrap(); - expect(transaction.toAddress).toBe(new Address(destination)); - expect(transaction.amount).toBe(u64(15000)); - expect(transaction.confirmedOwnerList.length).toBe(i + 1); - expect(transaction.confirmationWeightedSum).toBe(totalWeight); - expect(transaction.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(i + 1); + expect(operation.confirmationWeightedSum).toBe(totalWeight); + expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); } switchUser(deployerAddress); - // retrieve the transaction in its final state in Storage - let transaction = retrieveTransaction(txIndex).unwrap(); - expect(transaction.isValidated()).toBe(false); + // retrieve the operation in its final state in Storage + let operation = retrieveOperation(opIndex).unwrap(); + expect(operation.isValidated()).toBe(false); }); - // non validated transaction - test('confirm transaction [owners[2]]', () => { + // non validated operation + test('confirm transaction operation [owners[2]]', () => { let confirmingOwnersIndexes : Array; - let txIndex : u64; + let opIndex : u64; let totalWeight : u8; confirmingOwnersIndexes = [2]; - txIndex = 4; + opIndex = 4; - expect(ms1_submitTransaction(new Args() + expect(ms1_submitOperation(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() - )).toBe(txIndex); + )).toBe(opIndex); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { let ownerAddress = owners[confirmingOwnersIndexes[i]]; switchUser(ownerAddress); - ms1_confirmTransaction(new Args().add(txIndex).serialize()); + ms1_confirmOperation(new Args().add(opIndex).serialize()); totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; - // retrieve the transaction in its current state in Storage - let transaction = retrieveTransaction(txIndex).unwrap(); + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(opIndex).unwrap(); - expect(transaction.toAddress).toBe(new Address(destination)); - expect(transaction.amount).toBe(u64(15000)); - expect(transaction.confirmedOwnerList.length).toBe(i + 1); - expect(transaction.confirmationWeightedSum).toBe(totalWeight); - expect(transaction.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(i + 1); + expect(operation.confirmationWeightedSum).toBe(totalWeight); + expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); } switchUser(deployerAddress); - // retrieve the transaction in its final state in Storage - let transaction = retrieveTransaction(txIndex).unwrap(); - expect(transaction.isValidated()).toBe(false); + // retrieve the operation in its final state in Storage + let operation = retrieveOperation(opIndex).unwrap(); + expect(operation.isValidated()).toBe(false); }); - // validated transaction - test('confirm transaction [owners[1], owners[2]]', () => { + // validated operation + test('confirm transaction operation [owners[1], owners[2]]', () => { let confirmingOwnersIndexes : Array; - let txIndex : u64; + let opIndex : u64; let totalWeight : u8; confirmingOwnersIndexes = [1,2]; - txIndex = 5; + opIndex = 5; - expect(ms1_submitTransaction(new Args() + expect(ms1_submitOperation(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() - )).toBe(txIndex); + )).toBe(opIndex); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { let ownerAddress = owners[confirmingOwnersIndexes[i]]; switchUser(ownerAddress); - ms1_confirmTransaction(new Args().add(txIndex).serialize()); + ms1_confirmOperation(new Args().add(opIndex).serialize()); totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; - // retrieve the transaction in its current state in Storage - let transaction = retrieveTransaction(txIndex).unwrap(); + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(opIndex).unwrap(); - expect(transaction.toAddress).toBe(new Address(destination)); - expect(transaction.amount).toBe(u64(15000)); - expect(transaction.confirmedOwnerList.length).toBe(i + 1); - expect(transaction.confirmationWeightedSum).toBe(totalWeight); - expect(transaction.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(i + 1); + expect(operation.confirmationWeightedSum).toBe(totalWeight); + expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); } switchUser(deployerAddress); - // retrieve the transaction in its final state in Storage - let transaction = retrieveTransaction(txIndex).unwrap(); - expect(transaction.isValidated()).toBe(true); + // retrieve the operation in its final state in Storage + let operation = retrieveOperation(opIndex).unwrap(); + expect(operation.isValidated()).toBe(true); }); - // transaction 5 is validated, let's execute it - test('execute transaction with success', () => { + // operation 5 is validated, let's execute it + test('execute transaction operation with success', () => { let destinationBalance = Coins.balanceOf(destination); let contractBalance = Coins.balanceOf(contractAddr); let initDestinationBalance = destinationBalance; @@ -356,12 +356,12 @@ describe('Multisig contract tests', () => { )); expect(() => { - ms1_executeTransaction(new Args().add(u64(5)).serialize()); + ms1_executeOperation(new Args().add(u64(5)).serialize()); }).not.toThrow(); - // once executed, the transaction is deleted + // once executed, the operation is deleted expect(() => { - ms1_getTransaction(new Args().add(u64(5)).serialize()) + ms1_getOperation(new Args().add(u64(5)).serialize()) }).toThrow(); destinationBalance = Coins.balanceOf(destination); @@ -376,8 +376,8 @@ describe('Multisig contract tests', () => { expect(contractBalance + 15000).toBe(initContractBalance); }); - // transaction 4 is not validated, let's try to execute it - test('execute transaction with failure', () => { + // operation 4 is not validated, let's try to execute it + test('execute transaction operation with failure', () => { let destinationBalance = Coins.balanceOf(destination); let contractBalance = Coins.balanceOf(contractAddr); let initDestinationBalance = destinationBalance; @@ -389,12 +389,12 @@ describe('Multisig contract tests', () => { )); expect(() => { - ms1_executeTransaction(new Args().add(u64(4)).serialize()); + ms1_executeOperation(new Args().add(u64(4)).serialize()); }).toThrow(); - // the transaction is not supposed to be deleted + // the operation is not supposed to be deleted expect(() => { - ms1_getTransaction(new Args().add(u64(4)).serialize()) + ms1_getOperation(new Args().add(u64(4)).serialize()) }).not.toThrow(); destinationBalance = Coins.balanceOf(destination); @@ -409,9 +409,9 @@ describe('Multisig contract tests', () => { expect(contractBalance).toBe(initContractBalance); }); - // transaction 2 is validated by owners[0]. + // operation 2 is validated by owners[0]. // now owners[0] will revoke it and we will try to execute it. - test('revoke transaction', () => { + test('revoke operation', () => { let destinationBalance = Coins.balanceOf(destination); let contractBalance = Coins.balanceOf(contractAddr); let initDestinationBalance = destinationBalance; @@ -429,24 +429,24 @@ describe('Multisig contract tests', () => { )); expect(() => { - ms1_executeTransaction(new Args().add(u64(2)).serialize()); + ms1_executeOperation(new Args().add(u64(2)).serialize()); }).toThrow(); - // the transaction should not have been deleted + // the operation should not have been deleted expect(() => { - ms1_getTransaction(new Args().add(u64(2)).serialize()) + ms1_getOperation(new Args().add(u64(2)).serialize()) }).not.toThrow(); - // retrieve the transaction in its current state in Storage - let transaction = retrieveTransaction(u64(2)).unwrap(); + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(u64(2)).unwrap(); - expect(transaction.toAddress).toBe(new Address(destination)); - expect(transaction.amount).toBe(u64(15000)); - expect(transaction.confirmedOwnerList.length).toBe(0); - expect(transaction.confirmationWeightedSum).toBe(0); - expect(transaction.isAlreadyConfirmed(new Address(owners[0]))).toBe(false); - expect(transaction.isValidated()).toBe(false); + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(0); + expect(operation.confirmationWeightedSum).toBe(0); + expect(operation.isAlreadyConfirmed(new Address(owners[0]))).toBe(false); + expect(operation.isValidated()).toBe(false); destinationBalance = Coins.balanceOf(destination); contractBalance = Coins.balanceOf(contractAddr); diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index af052bd..7f80bc3 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -33,29 +33,29 @@ import { Args, serializableObjectsArrayToBytes } from '@massalabs/as-types'; const DEPOSIT_EVENT_NAME = 'DEPOSIT'; -const SUBMIT_TRANSACTION_EVENT_NAME = 'SUBMIT_TRANSACTION'; -const CONFIRM_TRANSACTION_EVENT_NAME = 'CONFIRM_TRANSACTION'; -const EXECUTE_TRANSACTION_EVENT_NAME = 'EXECUTE_TRANSACTION'; -const REVOKE_TRANSACTION_EVENT_NAME = 'REVOKE_TRANSACTION'; -const RETRIEVE_TRANSACTION_EVENT_NAME = 'RETRIEVE_TRANSACTION'; +const SUBMIT_OPERATION_EVENT_NAME = 'SUBMIT_OPERATION'; +const CONFIRM_OPERATION_EVENT_NAME = 'CONFIRM_OPERATION'; +const EXECUTE_OPERATION_EVENT_NAME = 'EXECUTE_OPERATION'; +const REVOKE_OPERATION_EVENT_NAME = 'REVOKE_OPERATION'; +const RETRIEVE_OPERATION_EVENT_NAME = 'RETRIEVE_OPERATION'; const GET_OWNERS_EVENT_NAME = 'GET_OWNERS'; -export const TRANSACTION_INDEX_PREFIX_KEY = '00'; +export const OPERATION_INDEX_PREFIX_KEY = '00'; export const OWNER_PREFIX_KEY = '01'; export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes('NB CONFIRMATIONS REQUIRED'); export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); -export const TRANSACTION_INDEX_KEY = stringToBytes('TRANSACTION INDEX'); +export const OPERATION_INDEX_KEY = stringToBytes('OPERATION INDEX'); // ======================================================== // // ==== HELPER FUNCTIONS & TYPES ==== // // ======================================================== // -function makeTransactionKey(txIndex: u64) : StaticArray { +function makeOperationKey(opIndex: u64) : StaticArray { - return stringToBytes(TRANSACTION_INDEX_PREFIX_KEY + - bytesToString(u64ToBytes(txIndex))); + return stringToBytes(OPERATION_INDEX_PREFIX_KEY + + bytesToString(u64ToBytes(opIndex))); } function makeOwnerKey(address: Address) : StaticArray { @@ -64,14 +64,14 @@ function makeOwnerKey(address: Address) : StaticArray { bytesToString(address.serialize())); } -export class Transaction { - toAddress: Address; // the destination address +export class Operation { + address: Address; // the destination address amount: u64; // the amount confirmedOwnerList: Array
; // the Array listing the owners who have already signed confirmationWeightedSum: u8; // the confirmation total weight sum, for easy check - constructor(toAddress: Address = new Address(), amount: u64 = 0) { - this.toAddress = toAddress; + constructor(address: Address = new Address(), amount: u64 = 0) { + this.address = address; this.amount = amount; this.confirmedOwnerList = new Array
(); this.confirmationWeightedSum = 0; @@ -79,34 +79,34 @@ export class Transaction { serialize() : StaticArray { - // create a serializable transaction record to store in Storage + // create a serializable Operation record to store in Storage // Here we store some redundant information, like the confirmation // total weight sum, trading some Storage space for Compute space, - // knowing that the transaction will be erased from Storage once executed - const argTransaction = new Args() - .add
(this.toAddress) + // knowing that the Operation will be erased from Storage once executed + const argOperation = new Args() + .add
(this.address) .add(this.amount) .addSerializableObjectArray>(this.confirmedOwnerList) .add(this.confirmationWeightedSum); - return argTransaction.serialize(); + return argOperation.serialize(); } deserialize(data: StaticArray) : void { const args = new Args(data); - this.toAddress = args + this.address = args .nextSerializable
() - .expect('Error while deserializing transaction toAddress'); + .expect('Error while deserializing Operation address'); this.amount = args .nextU64() - .expect('Error while deserializing transaction amount'); + .expect('Error while deserializing Operation amount'); this.confirmedOwnerList = args .nextSerializableObjectArray
() - .expect('Error while deserializing transaction confirmedOwnerList'); + .expect('Error while deserializing Operation confirmedOwnerList'); this.confirmationWeightedSum = args .nextU8() - .expect('Error while deserializing transaction confirmationWeightedSum'); + .expect('Error while deserializing Operation confirmationWeightedSum'); } isAlreadyConfirmed(owner: Address) : bool { @@ -137,35 +137,35 @@ export class Transaction { } } -export function storeTransaction(txIndex: u64, - transaction: Transaction): void { +export function storeOperation(opIndex: u64, + operation: Operation): void { - // we simply use the transaction index as a key to store it - Storage.set(makeTransactionKey(txIndex), transaction.serialize()); + // we simply use the Operation index as a key to store it + Storage.set(makeOperationKey(opIndex), operation.serialize()); } -export function retrieveTransaction(txIndex: u64): Result { +export function retrieveOperation(opIndex: u64): Result { - const transactionKey = makeTransactionKey(txIndex); + const operationKey = makeOperationKey(opIndex); - if (Storage.has(transactionKey)) { - let transaction = new Transaction(); - transaction.deserialize(Storage.get(transactionKey)); - return new Result(transaction); + if (Storage.has(operationKey)) { + let operation = new Operation(); + operation.deserialize(Storage.get(operationKey)); + return new Result(operation); } - return new Result(new Transaction(), "unknown or already executed transaction index") + return new Result(new Operation(), "unknown or already executed Operation index") } -export function hasTransaction(txIndex: u64): bool { +export function hasOperation(opIndex: u64): bool { - return Storage.has(makeTransactionKey(txIndex)); + return Storage.has(makeOperationKey(opIndex)); } -function deleteTransaction(txIndex: u64): void { +function deleteOperation(opIndex: u64): void { - const transactionKey = makeTransactionKey(txIndex); - Storage.del(transactionKey); + const operationKey = makeOperationKey(opIndex); + Storage.del(operationKey); } // ======================================================== // @@ -244,8 +244,8 @@ export function constructor(stringifyArgs: StaticArray): void { // We store the list of owners to be queries later if needed Storage.set(OWNERS_ADDRESSES_KEY, serializableObjectsArrayToBytes(ownerAddresses)); - // initialize transaction index - Storage.set(TRANSACTION_INDEX_KEY, u64ToBytes(0)); + // initialize operation index + Storage.set(OPERATION_INDEX_KEY, u64ToBytes(0)); } /** @@ -281,15 +281,15 @@ export function ms1_deposit(_: StaticArray): void { } // ======================================================== // -// ==== TRANSACTIONS ==== // +// ==== OPERATIONS ==== // // ======================================================== // /** - * Submit a transaction and generate an event with its index number + * Submit an operation and generate an event with its index number * * @example * ```typescript - * ms1_submitTransaction( + * ms1_submitOperation( * new Args() * .add
(new Address("...")) // destination address * .add(150000) // amount @@ -299,66 +299,66 @@ export function ms1_deposit(_: StaticArray): void { * * @param stringifyArgs - Args object serialized as a string containing: * - the destination address for the transfert (Address) - * - the amount of the transaction (u64). - * @returns transaction index. + * - the amount of the operation (u64). + * @returns operation index. */ -export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { +export function ms1_submitOperation(stringifyArgs: StaticArray): u64 { const args = new Args(stringifyArgs); // initialize address - const toAddress = args + const address = args .nextSerializable
() - .expect('Error while initializing transaction address'); + .expect('Error while initializing operation address'); // initialize amount const amount = args .nextU64() - .expect('Error while initializing transaction amount'); + .expect('Error while initializing operation amount'); - let txIndex = bytesToU64(Storage.get(TRANSACTION_INDEX_KEY)); - txIndex++; + let opIndex = bytesToU64(Storage.get(OPERATION_INDEX_KEY)); + opIndex++; - storeTransaction(txIndex, new Transaction(toAddress, amount)); + storeOperation(opIndex, new Operation(address, amount)); - // update the new txIndex value for the next transaction - Storage.set(TRANSACTION_INDEX_KEY, u64ToBytes(txIndex)); + // update the new opIndex value for the next operation + Storage.set(OPERATION_INDEX_KEY, u64ToBytes(opIndex)); generateEvent( - createEvent(SUBMIT_TRANSACTION_EVENT_NAME, [ + createEvent(SUBMIT_OPERATION_EVENT_NAME, [ Context.caller().toString(), - txIndex.toString(), - toAddress.toString(), + opIndex.toString(), + address.toString(), amount.toString() ]), ); - return txIndex; + return opIndex; } /** - * Confirms a transaction by an owner, and generate an event + * Confirms an operation by an owner, and generate an event * * @example * ```typescript - * ms1_confirmTransaction( + * ms1_confirmOperation( * new Args() - * .add(index) // the transaction index + * .add(index) // the operation index * .serialize(), * ); * ``` * * @param stringifyArgs - Args object serialized as a string containing: - * - the transaction index (u64) + * - the operation index (u64) */ -export function ms1_confirmTransaction(stringifyArgs: StaticArray): void { +export function ms1_confirmOperation(stringifyArgs: StaticArray): void { const args = new Args(stringifyArgs); - // initialize transaction index - const txIndex = args + // initialize operation index + const opIndex = args .nextU64() - .expect('Error while initializing transaction index'); + .expect('Error while initializing operation index'); let owner = Context.caller(); @@ -367,95 +367,95 @@ export function ms1_confirmTransaction(stringifyArgs: StaticArray): void { assert(Storage.has(ownerKey), "Caller address is not an owner"); let weight = byteToU8(Storage.get(ownerKey)); - // check the transaction exists and retrieve it from Storage - let transaction = retrieveTransaction(txIndex).unwrap(); + // check the operation exists and retrieve it from Storage + let operation = retrieveOperation(opIndex).unwrap(); // did we already confirm it? - assert(!transaction.isAlreadyConfirmed(owner), - "The caller address has already confirmed this transaction"); + assert(!operation.isAlreadyConfirmed(owner), + "The caller address has already confirmed this operation"); // confirm it and update the Storage - transaction.confirm(owner, weight); - storeTransaction(txIndex, transaction); + operation.confirm(owner, weight); + storeOperation(opIndex, operation); generateEvent( - createEvent(CONFIRM_TRANSACTION_EVENT_NAME, [ + createEvent(CONFIRM_OPERATION_EVENT_NAME, [ owner.toString(), - txIndex.toString() + opIndex.toString() ]), ); } /** - * Execute a transaction and generate an event in case of success + * Execute an operation and generate an event in case of success * * @example * ```typescript - * ms1_executeTransaction( + * ms1_executeOperation( * new Args() - * .add(index) // the transaction index + * .add(index) // the operation index * .serialize(), * ); * ``` * * @param stringifyArgs - Args object serialized as a string containing: - * - the transaction index (u64) + * - the operation index (u64) */ -export function ms1_executeTransaction(stringifyArgs: StaticArray): void { +export function ms1_executeOperation(stringifyArgs: StaticArray): void { const args = new Args(stringifyArgs); - // initialize transaction index - const txIndex = args + // initialize operation index + const opIndex = args .nextU64() - .expect('Error while initializing transaction index'); + .expect('Error while initializing operation index'); - // check the transaction exists and retrieve it from Storage - let transaction = retrieveTransaction(txIndex).unwrap(); + // check the operation exists and retrieve it from Storage + let operation = retrieveOperation(opIndex).unwrap(); - // if the transaction is sufficiently confirmed, execute it - assert(transaction.isValidated(), - "The transaction is unsufficiently confirmed, cannot execute"); - Coins.transferCoinsOf(Context.callee(), transaction.toAddress, transaction.amount); + // if the operation is sufficiently confirmed, execute it + assert(operation.isValidated(), + "The operation is unsufficiently confirmed, cannot execute"); + Coins.transferCoinsOf(Context.callee(), operation.address, operation.amount); - // clean up Storage and remove executed transaction + // clean up Storage and remove executed operation // NB: we could decide to keep it for archive purposes but then the // Storage cost would increase forever. - deleteTransaction(txIndex); + deleteOperation(opIndex); generateEvent( - createEvent(EXECUTE_TRANSACTION_EVENT_NAME, [ + createEvent(EXECUTE_OPERATION_EVENT_NAME, [ Context.caller().toString(), - txIndex.toString(), - transaction.toAddress.toString(), - transaction.amount.toString(), + opIndex.toString(), + operation.address.toString(), + operation.amount.toString(), ]), ); } /** - * Revoke a transaction confirmation by an owner, and generate an event + * Revoke an operation confirmation by an owner, and generate an event * * @example * ```typescript * ms1_revokeConfirmation( * new Args() - * .add(index) // the transaction index + * .add(index) // the operation index * .serialize(), * ); * ``` * * @param stringifyArgs - Args object serialized as a string containing: - * - the transaction index (u64) + * - the operation index (u64) */ export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { const args = new Args(stringifyArgs); - // initialize transaction index - const txIndex = args + // initialize operation index + const opIndex = args .nextU64() - .expect('Error while initializing transaction index'); + .expect('Error while initializing operation index'); let owner = Context.caller(); @@ -464,21 +464,21 @@ export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { assert(Storage.has(ownerKey), "Caller address is not an owner"); let weight = byteToU8(Storage.get(ownerKey)); - // check the transaction exists and retrieve it from Storage - let transaction = retrieveTransaction(txIndex).unwrap(); + // check the operation exists and retrieve it from Storage + let operation = retrieveOperation(opIndex).unwrap(); // did we actually already confirmed it? - assert(transaction.isAlreadyConfirmed(owner), - "The caller address has not yet confirmed this transaction"); + assert(operation.isAlreadyConfirmed(owner), + "The caller address has not yet confirmed this operation"); // revoke it and update the Storage - transaction.revoke(owner, weight); - storeTransaction(txIndex, transaction); + operation.revoke(owner, weight); + storeOperation(opIndex, operation); generateEvent( - createEvent(REVOKE_TRANSACTION_EVENT_NAME, [ + createEvent(REVOKE_OPERATION_EVENT_NAME, [ owner.toString(), - txIndex.toString() + opIndex.toString() ]), ); } @@ -507,69 +507,69 @@ export function ms1_getOwners(_ : StaticArray) : StaticArray { } /** - * Retrieve a currently stored transaction and generate an event + * Retrieve a currently stored operation and generate an event * * @example * ```typescript - * let transaction = new Transaction(); - * transaction.deserialize(ms1_getTransaction( + * let operation = new Operation(); + * operation.deserialize(ms1_getOperation( * new Args() - * .add(index) // the transaction index + * .add(index) // the operation index * .serialize() * )); * ``` * * @param stringifyArgs - Args object serialized as a string containing: - * - the transaction index (u64) + * - the operation index (u64) */ -export function ms1_getTransaction(stringifyArgs : StaticArray) : StaticArray { +export function ms1_getOperation(stringifyArgs : StaticArray) : StaticArray { const args = new Args(stringifyArgs); - // initialize transaction index - const txIndex = args + // initialize operation index + const opIndex = args .nextU64() - .expect('Error while initializing transaction index'); + .expect('Error while initializing operation index'); - // check the transaction exists and retrieve it from Storage - let transaction = retrieveTransaction(txIndex).unwrap(); + // check the operation exists and retrieve it from Storage + let operation = retrieveOperation(opIndex).unwrap(); // generate the event with the list of confirmed owners let eventPayLoad : Array = [ - txIndex.toString(), - transaction.toAddress.toString(), - transaction.amount.toString(), - transaction.confirmationWeightedSum.toString()]; - for (let i = 0; i < transaction.confirmedOwnerList.length; i++) - eventPayLoad.push(transaction.confirmedOwnerList[i].toString()); - generateEvent(createEvent(RETRIEVE_TRANSACTION_EVENT_NAME, eventPayLoad)); - - return transaction.serialize(); + opIndex.toString(), + operation.address.toString(), + operation.amount.toString(), + operation.confirmationWeightedSum.toString()]; + for (let i = 0; i < operation.confirmedOwnerList.length; i++) + eventPayLoad.push(operation.confirmedOwnerList[i].toString()); + generateEvent(createEvent(RETRIEVE_OPERATION_EVENT_NAME, eventPayLoad)); + + return operation.serialize(); } /** - * Check if the transaction defined by its index is a currently stored - * transaction. + * Check if the operation defined by its index is a currently stored + * operation. * @example * ```typescript - * ms1_hasTransaction( + * ms1_hasOperation( * new Args() - * .add(index) // the transaction index + * .add(index) // the operation index * .serialize(), * ); * ``` * * @param stringifyArgs - Args object serialized as a string containing: - * - the transaction index (u64) + * - the operation index (u64) */ -export function ms1_hasTransaction(stringifyArgs : StaticArray) : StaticArray { +export function ms1_hasOperation(stringifyArgs : StaticArray) : StaticArray { const args = new Args(stringifyArgs); - // initialize transaction index - const txIndex = args + // initialize operation index + const opIndex = args .nextU64() - .expect('Error while initializing transaction index'); + .expect('Error while initializing operation index'); - return boolToByte(hasTransaction(txIndex)); + return boolToByte(hasOperation(opIndex)); } diff --git a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts index 9581376..fecf101 100644 --- a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts +++ b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts @@ -17,7 +17,7 @@ import { Args, * @example * ```typescript * const multisig = new MultisigWrapper(MultisigAddr); - * multisig.confirmTransaction(txIndex); + * multisig.confirmOperation(opIndex); * ``` */ export class MultisigWrapper { @@ -40,55 +40,55 @@ export class MultisigWrapper { } /** - * Submit a transaction, and retrieve its index to be used by + * Submit an operation, and retrieve its index to be used by * the multisig owners to confirm it. * - * @param toAddress - recipient address + * @param address - recipient address * @param amount - amount to transfer * - * @returns the transaction index + * @returns the operation index */ - submitTransaction(toAddress: Address, amount: u64): u64 { + submitOperation(address: Address, amount: u64): u64 { return bytesToU64( call(this._origin, - 'ms1_submitTransaction', - new Args().add
(toAddress).add(amount).serialize(), + 'ms1_submitOperation', + new Args().add
(address).add(amount).serialize(), 0)); } /** - * Confirm a transaction identified by its index. + * Confirm an operation identified by its index. * - * @param txIndex - the transaction index + * @param opIndex - the operation index */ - confirmTransaction(txIndex: u64): void { + confirmOperation(opIndex: u64): void { call(this._origin, - 'ms1_confirmTransaction', - new Args().add(txIndex).serialize(), + 'ms1_confirmOperation', + new Args().add(opIndex).serialize(), 0); } /** - * Execute a transaction if it has enough validation from owners + * Execute an operation if it has enough validation from owners * - * @param txIndex - the transaction index + * @param opIndex - the operation index */ - executeTransaction(txIndex: u64): void { + executeOperation(opIndex: u64): void { call(this._origin, - 'ms1_executeTransaction', - new Args().add(txIndex).serialize(), + 'ms1_executeOperation', + new Args().add(opIndex).serialize(), 0); } /** - * Revoke a transaction identified by its index. + * Revoke a operation identified by its index. * - * @param txIndex - the transaction index + * @param opIndex - the operation index */ - revokeTransaction(txIndex: u64): void { + revokeOperation(opIndex: u64): void { call(this._origin, - 'ms1_revokeTransaction', - new Args().add(txIndex).serialize(), + 'ms1_revokeOperation', + new Args().add(opIndex).serialize(), 0); } @@ -104,33 +104,32 @@ export class MultisigWrapper { } /** - * Get a transaction identified by its index. - * Will throw if the transaction index does not exist. + * Get an operation identified by its index. + * Will throw if the operation index does not exist. * - * @returns the transaction + * @returns the operation */ - getTransaction(txIndex: u64): Transaction { - let transaction = new Transaction(); - transaction.deserialize( + getOperation(opIndex: u64): Operation { + let operation = new Operation(); + operation.deserialize( call(this._origin, - 'ms1_getTransaction', - new Args().add(txIndex).serialize(), + 'ms1_getOperation', + new Args().add(opIndex).serialize(), 0)); - return transaction; + return operation; } /** - * Check if a transaction identified by its index is pending. + * Check if an operation identified by its index is pending. * - * @returns true if the transaction is defined and pending execution. + * @returns true if the operation is defined and pending execution. */ - hasTransaction(txIndex: u64): bool { + hasOperation(opIndex: u64): bool { return byteToBool ( call(this._origin, - 'ms1_hasTransaction', - new Args().add(txIndex).serialize(), + 'ms1_hasOperation', + new Args().add(opIndex).serialize(), 0)); } - } From 36fc42d19fd5182abf15808a6d24d33aab9b20d2 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Sun, 20 Aug 2023 19:49:12 +0400 Subject: [PATCH 09/25] add the possibility to submit sc call operations, not only transactions. --- .../multisig/__tests__/multisig.spec.ts | 13 +- .../assembly/contracts/multisig/multisig.ts | 129 ++++++++++++++++-- .../contracts/multisig/multisigWrapper.ts | 30 +++- 3 files changed, 153 insertions(+), 19 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 2899be1..3e64bde 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -1,6 +1,7 @@ import { ms1_deposit, - ms1_submitOperation, + ms1_submitTransaction, + ms1_submitCall, ms1_confirmOperation, ms1_executeOperation, ms1_revokeConfirmation, @@ -171,7 +172,7 @@ describe('Multisig contract tests', () => { test('submit transaction operation', () => { // expect the operation index to be 1 - expect(ms1_submitOperation(new Args() + expect(ms1_submitTransaction(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() @@ -200,7 +201,7 @@ describe('Multisig contract tests', () => { confirmingOwnersIndexes = [0]; opIndex = 2; - expect(ms1_submitOperation(new Args() + expect(ms1_submitTransaction(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() @@ -238,7 +239,7 @@ describe('Multisig contract tests', () => { confirmingOwnersIndexes = [1]; opIndex = 3; - expect(ms1_submitOperation(new Args() + expect(ms1_submitTransaction(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() @@ -276,7 +277,7 @@ describe('Multisig contract tests', () => { confirmingOwnersIndexes = [2]; opIndex = 4; - expect(ms1_submitOperation(new Args() + expect(ms1_submitTransaction(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() @@ -314,7 +315,7 @@ describe('Multisig contract tests', () => { confirmingOwnersIndexes = [1,2]; opIndex = 5; - expect(ms1_submitOperation(new Args() + expect(ms1_submitTransaction(new Args() .add
(new Address(destination)) .add(u64(15000)) .serialize() diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 7f80bc3..9836a9b 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -13,6 +13,7 @@ import { Address, Context, + Contract, Coins, generateEvent, Storage, @@ -33,7 +34,8 @@ import { Args, serializableObjectsArrayToBytes } from '@massalabs/as-types'; const DEPOSIT_EVENT_NAME = 'DEPOSIT'; -const SUBMIT_OPERATION_EVENT_NAME = 'SUBMIT_OPERATION'; +const SUBMIT_TRANSACTION_EVENT_NAME = 'SUBMIT_TRANSACTION'; +const SUBMIT_CALL_EVENT_NAME = 'SUBMIT_CALL'; const CONFIRM_OPERATION_EVENT_NAME = 'CONFIRM_OPERATION'; const EXECUTE_OPERATION_EVENT_NAME = 'EXECUTE_OPERATION'; const REVOKE_OPERATION_EVENT_NAME = 'REVOKE_OPERATION'; @@ -64,15 +66,36 @@ function makeOwnerKey(address: Address) : StaticArray { bytesToString(address.serialize())); } + +/** + * Operation represent either a transfer of coins to a given address (a "transaction"), + * or a call to a smart contract. + * + * When the name is empty, the operation is a simple transaction. The amount is + * credited to the given address. + * + * When the name is not empty, the operation is a call to the function of that + * name, from the smart contract at the given address. Amount can be used to + * transfer coin as part of the call. The list of arguments to the call can be + * specified in 'args' + * + */ export class Operation { - address: Address; // the destination address + address: Address; // the destination address (the recipient of coins or the smart contract to call) amount: u64; // the amount + name: string; // the function call name, if any ("" means the operation is a simple coin transfer) + args: Args; // the args of the function call, if any confirmedOwnerList: Array
; // the Array listing the owners who have already signed confirmationWeightedSum: u8; // the confirmation total weight sum, for easy check - constructor(address: Address = new Address(), amount: u64 = 0) { + constructor(address: Address = new Address(), + amount: u64 = 0, + name: string = "", + args: Args = new Args()) { this.address = address; this.amount = amount; + this.name = name; + this.args = args; this.confirmedOwnerList = new Array
(); this.confirmationWeightedSum = 0; } @@ -86,6 +109,8 @@ export class Operation { const argOperation = new Args() .add
(this.address) .add(this.amount) + .add(this.name) + .add>(this.args.serialize()) .addSerializableObjectArray>(this.confirmedOwnerList) .add(this.confirmationWeightedSum); return argOperation.serialize(); @@ -101,6 +126,13 @@ export class Operation { this.amount = args .nextU64() .expect('Error while deserializing Operation amount'); + this.name = args + .nextString() + .expect('Error while deserializing Operation name'); + let argData = args + .nextBytes() + .expect('Error while deserializing Operation args'); + this.args = new Args(argData); this.confirmedOwnerList = args .nextSerializableObjectArray
() .expect('Error while deserializing Operation confirmedOwnerList'); @@ -135,6 +167,15 @@ export class Operation { let nbConfirmationsRequired = byteToU8(Storage.get(NB_CONFIRMATIONS_REQUIRED_KEY)); return this.confirmationWeightedSum >= nbConfirmationsRequired; } + + execute(callee: Address) : void { + if (this.name.length == 0) + // we have a transaction + Coins.transferCoinsOf(Context.callee(), this.address, this.amount); + else + // We have a call operation + Contract.call(this.address, this.name, this.args, this.amount); + } } export function storeOperation(opIndex: u64, @@ -285,11 +326,11 @@ export function ms1_deposit(_: StaticArray): void { // ======================================================== // /** - * Submit an operation and generate an event with its index number + * Submit a transaction operation and generate an event with its index number * * @example * ```typescript - * ms1_submitOperation( + * ms1_submitTransaction( * new Args() * .add
(new Address("...")) // destination address * .add(150000) // amount @@ -302,19 +343,19 @@ export function ms1_deposit(_: StaticArray): void { * - the amount of the operation (u64). * @returns operation index. */ -export function ms1_submitOperation(stringifyArgs: StaticArray): u64 { +export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { const args = new Args(stringifyArgs); // initialize address const address = args .nextSerializable
() - .expect('Error while initializing operation address'); + .expect('Error while initializing transaction operation address'); // initialize amount const amount = args .nextU64() - .expect('Error while initializing operation amount'); + .expect('Error while initializing transaction operation amount'); let opIndex = bytesToU64(Storage.get(OPERATION_INDEX_KEY)); opIndex++; @@ -325,7 +366,7 @@ export function ms1_submitOperation(stringifyArgs: StaticArray): u64 { Storage.set(OPERATION_INDEX_KEY, u64ToBytes(opIndex)); generateEvent( - createEvent(SUBMIT_OPERATION_EVENT_NAME, [ + createEvent(SUBMIT_TRANSACTION_EVENT_NAME, [ Context.caller().toString(), opIndex.toString(), address.toString(), @@ -336,6 +377,74 @@ export function ms1_submitOperation(stringifyArgs: StaticArray): u64 { return opIndex; } +/** + * Submit a call operation and generate an event with its index number + * + * @example + * ```typescript + * ms1_submitCall( + * new Args() + * .add
(new Address("...")) // smart contract address for the call + * .add(150000) // amount to transfer as part of the call + * .add("fun_name") // the name of the function to call + * .add(new Args()...) // the arguments to the call + * .serialize() + * ); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the smart contract address for the operation (Address) + * - the transfered amount attached to the call (u64). + * - the name of the function to call (string). + * - the function arguments (Args). + * @returns operation index. + */ +export function ms1_submitCall(stringifyArgs: StaticArray): u64 { + + const args = new Args(stringifyArgs); + + // initialize address + const address = args + .nextSerializable
() + .expect('Error while initializing call operation address'); + + // initialize amount + const amount = args + .nextU64() + .expect('Error while initializing call operation amount'); + + // initialize amount + const name = args + .nextString() + .expect('Error while initializing call operation function name'); + + // initialize amount + const callArgsData = args + .nextBytes() + .expect('Error while initializing call operation function args'); + const callArgs = new Args(callArgsData); + + let opIndex = bytesToU64(Storage.get(OPERATION_INDEX_KEY)); + opIndex++; + + storeOperation(opIndex, new Operation(address, amount, name, callArgs)); + + // update the new opIndex value for the next operation + Storage.set(OPERATION_INDEX_KEY, u64ToBytes(opIndex)); + + generateEvent( + createEvent(SUBMIT_CALL_EVENT_NAME, [ + Context.caller().toString(), + opIndex.toString(), + address.toString(), + amount.toString(), + name + ]), + ); + + return opIndex; +} + /** * Confirms an operation by an owner, and generate an event * @@ -416,7 +525,7 @@ export function ms1_executeOperation(stringifyArgs: StaticArray): void { // if the operation is sufficiently confirmed, execute it assert(operation.isValidated(), "The operation is unsufficiently confirmed, cannot execute"); - Coins.transferCoinsOf(Context.callee(), operation.address, operation.amount); + operation.execute(Context.callee()); // clean up Storage and remove executed operation // NB: we could decide to keep it for archive purposes but then the diff --git a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts index fecf101..e40b5cc 100644 --- a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts +++ b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts @@ -40,7 +40,7 @@ export class MultisigWrapper { } /** - * Submit an operation, and retrieve its index to be used by + * Submit a transaction operation, and retrieve its index to be used by * the multisig owners to confirm it. * * @param address - recipient address @@ -48,14 +48,38 @@ export class MultisigWrapper { * * @returns the operation index */ - submitOperation(address: Address, amount: u64): u64 { + submitTransaction(address: Address, amount: u64): u64 { return bytesToU64( call(this._origin, - 'ms1_submitOperation', + 'ms1_submitTransaction', new Args().add
(address).add(amount).serialize(), 0)); } + /** + * Submit a smart contract call operation, and retrieve its index to be + * used by the multisig owners to confirm it. + * + * @param address - the smart contract address for the operation + * @param amount - the transfered amount attached to the call + * @param name - the name of the function to call + * @param args - the function arguments + * + * @returns the operation index + */ + submitCall(address: Address, amount: u64, name: string, args: Args): u64 { + return bytesToU64( + call(this._origin, + 'ms1_submitCall', + new Args() + .add
(address) + .add(amount) + .add(name) + .add>(args.serialize()) + .serialize(), + 0)); + } + /** * Confirm an operation identified by its index. * From e4841cc57a1fd4c56f0584e28fa204e76e662666 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Tue, 22 Aug 2023 16:01:03 +0400 Subject: [PATCH 10/25] add a test for multisig call operations --- .../multisig/__tests__/multisig.spec.ts | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 3e64bde..86aad16 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -52,19 +52,21 @@ const owners : Array = [ 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb' ]; -// where operation funds are sent +// where operation funds are sent when a transaction operation is executed const destination = 'AU155TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb'; +// owners declared to the constructor for testing. Note that owners[0] has +// a weight of 2 since it is mentionned twice in the list: const ownerList = [owners[0], owners[0], owners[1], owners[2]]; const ownerWeight : Array = [2, 1, 1]; -export const OPERATION_INDEX_PREFIX_KEY = '00'; -export const OWNER_PREFIX_KEY = '01'; - export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes('NB CONFIRMATIONS REQUIRED'); export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); export const OPERATION_INDEX_KEY = stringToBytes('OPERATION INDEX'); +export const OPERATION_INDEX_PREFIX_KEY = '00'; +export const OWNER_PREFIX_KEY = '01'; + function makeOperationKey(opIndex: u64) : StaticArray { return stringToBytes(OPERATION_INDEX_PREFIX_KEY + bytesToString(u64ToBytes(opIndex))); @@ -75,6 +77,7 @@ function makeOwnerKey(owner: string) : StaticArray { bytesToString(new Args().add(owner).serialize())); } +// string are not serializable by default, we need this helper class class SerializableString implements Serializable { s: string; @@ -138,14 +141,14 @@ describe('Multisig contract tests', () => { resetStorage(); //------------------------------------------------------- - // define a valid constructor for a transaction operation + // define a valid constructor for a 2:4 multisig const serializedArgs = new Args() .add(nbConfirmations) .add>(ownerList) .serialize(); constructor(serializedArgs); - // check the nb of confirmation required is properly stored + // check the nb of confirmations required is properly stored expect(byteToU8(Storage.get(NB_CONFIRMATIONS_REQUIRED_KEY))).toBe(nbConfirmations); // compare the array of addresses as string to the array of Address in storage @@ -344,6 +347,34 @@ describe('Multisig contract tests', () => { expect(operation.isValidated()).toBe(true); }); + // test of the call operation constructor + test('submit call operation', () => { + + // expect the operation index to be 1 + expect(ms1_submitCall(new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .add("getValueAt") + .add>(new Args().add(42).serialize()) + .serialize() + )).toBe(6); + + // check that the operation is correctly stored + let operationResult = retrieveOperation(6); + expect(operationResult.isOk()).toBe(true); + + // check the operation content + let operation = operationResult.unwrap(); + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.name).toBe("getValueAt"); + expect(operation.args).toStrictEqual(new Args().add(42)); + expect(operation.confirmedOwnerList.length).toBe(0); + expect(operation.confirmationWeightedSum).toBe(0); + expect(operation.isAlreadyConfirmed(new Address(owners[0]))).toBe(false); + expect(operation.isValidated()).toBe(false); + }); + // operation 5 is validated, let's execute it test('execute transaction operation with success', () => { let destinationBalance = Coins.balanceOf(destination); From 72100c1a9025b5b0f02ba94d4af6c70a863300d1 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Tue, 22 Aug 2023 16:22:57 +0400 Subject: [PATCH 11/25] npm run fmt --- smart-contracts/assembly/contracts/NFT/NFT.ts | 1 - .../contracts/NFT/__tests__/NFT.spec.ts | 3 +- .../multisig/__tests__/multisig.spec.ts | 387 ++++++++++-------- .../assembly/contracts/multisig/index.ts | 2 +- .../assembly/contracts/multisig/multisig.ts | 362 ++++++++-------- .../contracts/multisig/multisigWrapper.ts | 103 +++-- 6 files changed, 465 insertions(+), 393 deletions(-) diff --git a/smart-contracts/assembly/contracts/NFT/NFT.ts b/smart-contracts/assembly/contracts/NFT/NFT.ts index 444e723..5a42d4b 100644 --- a/smart-contracts/assembly/contracts/NFT/NFT.ts +++ b/smart-contracts/assembly/contracts/NFT/NFT.ts @@ -10,7 +10,6 @@ import { stringToBytes, bytesToU256, u256ToBytes, - boolToByte, u32ToBytes, } from '@massalabs/as-types'; import { u256 } from 'as-bignum/assembly'; diff --git a/smart-contracts/assembly/contracts/NFT/__tests__/NFT.spec.ts b/smart-contracts/assembly/contracts/NFT/__tests__/NFT.spec.ts index 827869f..1891019 100644 --- a/smart-contracts/assembly/contracts/NFT/__tests__/NFT.spec.ts +++ b/smart-contracts/assembly/contracts/NFT/__tests__/NFT.spec.ts @@ -35,7 +35,8 @@ import { u256 } from 'as-bignum/assembly'; const callerAddress = 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'; -const userAddress = 'A12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8'; +// never used +// const userAddress = 'A12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8'; const NFTName = 'MASSA_NFT'; const NFTSymbol = 'NFT'; diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 86aad16..23fdb55 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -1,34 +1,34 @@ import { - ms1_deposit, ms1_submitTransaction, ms1_submitCall, ms1_confirmOperation, ms1_executeOperation, ms1_revokeConfirmation, - ms1_getOwners, ms1_getOperation, constructor, Operation, - retrieveOperation + retrieveOperation, } from '../multisig'; -import { Storage, - mockAdminContext, - Address, - createEvent, - Coins, - generateEvent } from '@massalabs/massa-as-sdk'; +import { + Storage, + mockAdminContext, + Address, + createEvent, + Coins, + generateEvent, +} from '@massalabs/massa-as-sdk'; import { Args, - byteToBool, byteToU8, bytesToU64, stringToBytes, bytesToString, serializableObjectsArrayToBytes, bytesToSerializableObjectArray, - Serializable + Serializable, + Result, } from '@massalabs/as-types'; import { @@ -43,13 +43,13 @@ const deployerAddress = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'; const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT'; // nb of confirmations required -const nbConfirmations : u8 = 2; +const nbConfirmations: u8 = 2; // the multisig owners -const owners : Array = [ +const owners: Array = [ 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', 'AU1qDAxGJ387ETi9JRQzZWSPKYq4YPXrFvdiE4VoXUaiAt38JFEC', - 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb' + 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb', ]; // where operation funds are sent when a transaction operation is executed @@ -58,23 +58,21 @@ const destination = 'AU155TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb'; // owners declared to the constructor for testing. Note that owners[0] has // a weight of 2 since it is mentionned twice in the list: const ownerList = [owners[0], owners[0], owners[1], owners[2]]; -const ownerWeight : Array = [2, 1, 1]; +const ownerWeight: Array = [2, 1, 1]; -export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes('NB CONFIRMATIONS REQUIRED'); +export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes( + 'NB CONFIRMATIONS REQUIRED', +); export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); export const OPERATION_INDEX_KEY = stringToBytes('OPERATION INDEX'); export const OPERATION_INDEX_PREFIX_KEY = '00'; export const OWNER_PREFIX_KEY = '01'; -function makeOperationKey(opIndex: u64) : StaticArray { - return stringToBytes(OPERATION_INDEX_PREFIX_KEY + - bytesToString(u64ToBytes(opIndex))); -} - -function makeOwnerKey(owner: string) : StaticArray { - return stringToBytes(OWNER_PREFIX_KEY + - bytesToString(new Args().add(owner).serialize())); +function makeOwnerKey(owner: string): StaticArray { + return stringToBytes( + OWNER_PREFIX_KEY + bytesToString(new Args().add(owner).serialize()), + ); } // string are not serializable by default, we need this helper class @@ -86,17 +84,12 @@ class SerializableString implements Serializable { } public serialize(): StaticArray { - return stringToBytes(this.s); } - public deserialize( - data: StaticArray, - offset: i32, - ): Result { - + public deserialize(data: StaticArray, _offset: i32): Result { this.s = bytesToString(data); - return Result(0); + return new Result(0); } } @@ -111,8 +104,7 @@ beforeAll(() => { describe('Multisig contract tests', () => { test('constructor', () => { - - //--------------------------- + // --------------------------- // check invalid constructors // 0 confirmations @@ -140,7 +132,7 @@ describe('Multisig contract tests', () => { resetStorage(); - //------------------------------------------------------- + // ------------------------------------------------------- // define a valid constructor for a 2:4 multisig const serializedArgs = new Args() .add(nbConfirmations) @@ -149,19 +141,31 @@ describe('Multisig contract tests', () => { constructor(serializedArgs); // check the nb of confirmations required is properly stored - expect(byteToU8(Storage.get(NB_CONFIRMATIONS_REQUIRED_KEY))).toBe(nbConfirmations); + expect(byteToU8(Storage.get(NB_CONFIRMATIONS_REQUIRED_KEY))).toBe( + nbConfirmations, + ); // compare the array of addresses as string to the array of Address in storage - let serializableStringList : Array = []; + let serializableStringList: Array = []; for (let i = 0; i < ownerList.length; ++i) - serializableStringList.push(new SerializableString(ownerList[i])); + serializableStringList.push(new SerializableString(ownerList[i])); let ownersFromStorage = bytesToSerializableObjectArray
( - Storage.get(OWNERS_ADDRESSES_KEY)).unwrap(); - let serializableOwnerStringList : Array = []; + Storage.get(OWNERS_ADDRESSES_KEY), + ).unwrap(); + let serializableOwnerStringList: Array = []; for (let i = 0; i < ownersFromStorage.length; ++i) - serializableOwnerStringList.push(new SerializableString(ownersFromStorage[i].toString())); - expect(serializableObjectsArrayToBytes(serializableOwnerStringList)) - .toStrictEqual(serializableObjectsArrayToBytes(serializableStringList)); + serializableOwnerStringList.push( + new SerializableString(ownersFromStorage[i].toString()), + ); + expect( + serializableObjectsArrayToBytes( + serializableOwnerStringList, + ), + ).toStrictEqual( + serializableObjectsArrayToBytes( + serializableStringList, + ), + ); // check the weight of each owner expect(byteToU8(Storage.get(makeOwnerKey(owners[0])))).toBe(2); @@ -173,13 +177,15 @@ describe('Multisig contract tests', () => { }); test('submit transaction operation', () => { - // expect the operation index to be 1 - expect(ms1_submitTransaction(new Args() - .add
(new Address(destination)) - .add(u64(15000)) - .serialize() - )).toBe(1); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toBe(1); // check that the operation is correctly stored let operationResult = retrieveOperation(1); @@ -197,34 +203,39 @@ describe('Multisig contract tests', () => { // validated operation test('confirm transaction operation [owners[0]]', () => { - let confirmingOwnersIndexes : Array; - let opIndex : u64; - let totalWeight : u8; + let confirmingOwnersIndexes: Array; + let opIndex: u64; + let totalWeight: u8; confirmingOwnersIndexes = [0]; opIndex = 2; - expect(ms1_submitTransaction(new Args() - .add
(new Address(destination)) - .add(u64(15000)) - .serialize() - )).toBe(opIndex); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toBe(opIndex); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { - let ownerAddress = owners[confirmingOwnersIndexes[i]]; - switchUser(ownerAddress); - ms1_confirmOperation(new Args().add(opIndex).serialize()); - totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; - - // retrieve the operation in its current state in Storage - let operation = retrieveOperation(opIndex).unwrap(); - - expect(operation.address).toBe(new Address(destination)); - expect(operation.amount).toBe(u64(15000)); - expect(operation.confirmedOwnerList.length).toBe(i + 1); - expect(operation.confirmationWeightedSum).toBe(totalWeight); - expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + let ownerAddress = owners[confirmingOwnersIndexes[i]]; + switchUser(ownerAddress); + ms1_confirmOperation(new Args().add(opIndex).serialize()); + totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; + + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(opIndex).unwrap(); + + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(i + 1); + expect(operation.confirmationWeightedSum).toBe(totalWeight); + expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe( + true, + ); } switchUser(deployerAddress); @@ -235,72 +246,82 @@ describe('Multisig contract tests', () => { // non validated operation test('confirm transaction operation [owners[1]]', () => { - let confirmingOwnersIndexes : Array; - let opIndex : u64; - let totalWeight : u8; + let confirmingOwnersIndexes: Array; + let opIndex: u64; + let totalWeight: u8; confirmingOwnersIndexes = [1]; opIndex = 3; - expect(ms1_submitTransaction(new Args() - .add
(new Address(destination)) - .add(u64(15000)) - .serialize() - )).toBe(opIndex); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toBe(opIndex); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { - let ownerAddress = owners[confirmingOwnersIndexes[i]]; - switchUser(ownerAddress); - ms1_confirmOperation(new Args().add(opIndex).serialize()); - totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; - - // retrieve the operation in its current state in Storage - let operation = retrieveOperation(opIndex).unwrap(); - - expect(operation.address).toBe(new Address(destination)); - expect(operation.amount).toBe(u64(15000)); - expect(operation.confirmedOwnerList.length).toBe(i + 1); - expect(operation.confirmationWeightedSum).toBe(totalWeight); - expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + let ownerAddress = owners[confirmingOwnersIndexes[i]]; + switchUser(ownerAddress); + ms1_confirmOperation(new Args().add(opIndex).serialize()); + totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; + + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(opIndex).unwrap(); + + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(i + 1); + expect(operation.confirmationWeightedSum).toBe(totalWeight); + expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe( + true, + ); } switchUser(deployerAddress); // retrieve the operation in its final state in Storage - let operation = retrieveOperation(opIndex).unwrap(); + let operation: Operation = retrieveOperation(opIndex).unwrap(); expect(operation.isValidated()).toBe(false); }); // non validated operation test('confirm transaction operation [owners[2]]', () => { - let confirmingOwnersIndexes : Array; - let opIndex : u64; - let totalWeight : u8; + let confirmingOwnersIndexes: Array; + let opIndex: u64; + let totalWeight: u8; confirmingOwnersIndexes = [2]; opIndex = 4; - expect(ms1_submitTransaction(new Args() - .add
(new Address(destination)) - .add(u64(15000)) - .serialize() - )).toBe(opIndex); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toBe(opIndex); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { - let ownerAddress = owners[confirmingOwnersIndexes[i]]; - switchUser(ownerAddress); - ms1_confirmOperation(new Args().add(opIndex).serialize()); - totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; - - // retrieve the operation in its current state in Storage - let operation = retrieveOperation(opIndex).unwrap(); - - expect(operation.address).toBe(new Address(destination)); - expect(operation.amount).toBe(u64(15000)); - expect(operation.confirmedOwnerList.length).toBe(i + 1); - expect(operation.confirmationWeightedSum).toBe(totalWeight); - expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + let ownerAddress = owners[confirmingOwnersIndexes[i]]; + switchUser(ownerAddress); + ms1_confirmOperation(new Args().add(opIndex).serialize()); + totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; + + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(opIndex).unwrap(); + + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(i + 1); + expect(operation.confirmationWeightedSum).toBe(totalWeight); + expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe( + true, + ); } switchUser(deployerAddress); @@ -311,34 +332,39 @@ describe('Multisig contract tests', () => { // validated operation test('confirm transaction operation [owners[1], owners[2]]', () => { - let confirmingOwnersIndexes : Array; - let opIndex : u64; - let totalWeight : u8; + let confirmingOwnersIndexes: Array; + let opIndex: u64; + let totalWeight: u8; - confirmingOwnersIndexes = [1,2]; + confirmingOwnersIndexes = [1, 2]; opIndex = 5; - expect(ms1_submitTransaction(new Args() - .add
(new Address(destination)) - .add(u64(15000)) - .serialize() - )).toBe(opIndex); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toBe(opIndex); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { - let ownerAddress = owners[confirmingOwnersIndexes[i]]; - switchUser(ownerAddress); - ms1_confirmOperation(new Args().add(opIndex).serialize()); - totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; - - // retrieve the operation in its current state in Storage - let operation = retrieveOperation(opIndex).unwrap(); - - expect(operation.address).toBe(new Address(destination)); - expect(operation.amount).toBe(u64(15000)); - expect(operation.confirmedOwnerList.length).toBe(i + 1); - expect(operation.confirmationWeightedSum).toBe(totalWeight); - expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe(true); + let ownerAddress = owners[confirmingOwnersIndexes[i]]; + switchUser(ownerAddress); + ms1_confirmOperation(new Args().add(opIndex).serialize()); + totalWeight += ownerWeight[confirmingOwnersIndexes[i]]; + + // retrieve the operation in its current state in Storage + let operation = retrieveOperation(opIndex).unwrap(); + + expect(operation.address).toBe(new Address(destination)); + expect(operation.amount).toBe(u64(15000)); + expect(operation.confirmedOwnerList.length).toBe(i + 1); + expect(operation.confirmationWeightedSum).toBe(totalWeight); + expect(operation.isAlreadyConfirmed(new Address(ownerAddress))).toBe( + true, + ); } switchUser(deployerAddress); @@ -349,15 +375,17 @@ describe('Multisig contract tests', () => { // test of the call operation constructor test('submit call operation', () => { - // expect the operation index to be 1 - expect(ms1_submitCall(new Args() - .add
(new Address(destination)) - .add(u64(15000)) - .add("getValueAt") - .add>(new Args().add(42).serialize()) - .serialize() - )).toBe(6); + expect( + ms1_submitCall( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .add('getValueAt') + .add>(new Args().add(42).serialize()) + .serialize(), + ), + ).toBe(6); // check that the operation is correctly stored let operationResult = retrieveOperation(6); @@ -367,7 +395,7 @@ describe('Multisig contract tests', () => { let operation = operationResult.unwrap(); expect(operation.address).toBe(new Address(destination)); expect(operation.amount).toBe(u64(15000)); - expect(operation.name).toBe("getValueAt"); + expect(operation.name).toBe('getValueAt'); expect(operation.args).toStrictEqual(new Args().add(42)); expect(operation.confirmedOwnerList.length).toBe(0); expect(operation.confirmationWeightedSum).toBe(0); @@ -383,25 +411,29 @@ describe('Multisig contract tests', () => { let initContractBalance = contractBalance; generateEvent( - createEvent("BALANCES BEFORE", - [initDestinationBalance.toString(), initContractBalance.toString()] - )); + createEvent('BALANCES BEFORE', [ + initDestinationBalance.toString(), + initContractBalance.toString(), + ]), + ); expect(() => { - ms1_executeOperation(new Args().add(u64(5)).serialize()); - }).not.toThrow(); + ms1_executeOperation(new Args().add(u64(5)).serialize()); + }).not.toThrow(); // once executed, the operation is deleted expect(() => { - ms1_getOperation(new Args().add(u64(5)).serialize()) - }).toThrow(); + ms1_getOperation(new Args().add(u64(5)).serialize()); + }).toThrow(); destinationBalance = Coins.balanceOf(destination); contractBalance = Coins.balanceOf(contractAddr); generateEvent( - createEvent("BALANCES AFTER", - [destinationBalance.toString(), contractBalance.toString()] - )); + createEvent('BALANCES AFTER', [ + destinationBalance.toString(), + contractBalance.toString(), + ]), + ); // check that the transfer has been done expect(destinationBalance).toBe(initDestinationBalance + 15000); @@ -416,25 +448,29 @@ describe('Multisig contract tests', () => { let initContractBalance = contractBalance; generateEvent( - createEvent("BALANCES BEFORE", - [initDestinationBalance.toString(), initContractBalance.toString()] - )); + createEvent('BALANCES BEFORE', [ + initDestinationBalance.toString(), + initContractBalance.toString(), + ]), + ); expect(() => { - ms1_executeOperation(new Args().add(u64(4)).serialize()); - }).toThrow(); + ms1_executeOperation(new Args().add(u64(4)).serialize()); + }).toThrow(); // the operation is not supposed to be deleted expect(() => { - ms1_getOperation(new Args().add(u64(4)).serialize()) - }).not.toThrow(); + ms1_getOperation(new Args().add(u64(4)).serialize()); + }).not.toThrow(); destinationBalance = Coins.balanceOf(destination); contractBalance = Coins.balanceOf(contractAddr); generateEvent( - createEvent("BALANCES AFTER", - [destinationBalance.toString(), contractBalance.toString()] - )); + createEvent('BALANCES AFTER', [ + destinationBalance.toString(), + contractBalance.toString(), + ]), + ); // check that the transfer has not been done expect(destinationBalance).toBe(initDestinationBalance); @@ -456,19 +492,20 @@ describe('Multisig contract tests', () => { switchUser(deployerAddress); generateEvent( - createEvent("BALANCES BEFORE", - [initDestinationBalance.toString(), initContractBalance.toString()] - )); + createEvent('BALANCES BEFORE', [ + initDestinationBalance.toString(), + initContractBalance.toString(), + ]), + ); expect(() => { - ms1_executeOperation(new Args().add(u64(2)).serialize()); - }).toThrow(); + ms1_executeOperation(new Args().add(u64(2)).serialize()); + }).toThrow(); // the operation should not have been deleted expect(() => { - ms1_getOperation(new Args().add(u64(2)).serialize()) - }).not.toThrow(); - + ms1_getOperation(new Args().add(u64(2)).serialize()); + }).not.toThrow(); // retrieve the operation in its current state in Storage let operation = retrieveOperation(u64(2)).unwrap(); @@ -483,9 +520,11 @@ describe('Multisig contract tests', () => { destinationBalance = Coins.balanceOf(destination); contractBalance = Coins.balanceOf(contractAddr); generateEvent( - createEvent("BALANCES AFTER", - [destinationBalance.toString(), contractBalance.toString()] - )); + createEvent('BALANCES AFTER', [ + destinationBalance.toString(), + contractBalance.toString(), + ]), + ); // check that the transfer has not been done expect(destinationBalance).toBe(initDestinationBalance); diff --git a/smart-contracts/assembly/contracts/multisig/index.ts b/smart-contracts/assembly/contracts/multisig/index.ts index 898917c..5d5037d 100644 --- a/smart-contracts/assembly/contracts/multisig/index.ts +++ b/smart-contracts/assembly/contracts/multisig/index.ts @@ -1,2 +1,2 @@ export * from './multisig'; -export * from './multisigWrapper'; \ No newline at end of file +export * from './multisigWrapper'; diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 9836a9b..ab7283e 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -21,17 +21,19 @@ import { callerHasWriteAccess, } from '@massalabs/massa-as-sdk'; -import { Args, - Result, - u8toByte, - u64ToBytes, - stringToBytes, - bytesToString, - bytesToU64, - byteToU8, - boolToByte, - bytesToSerializableObjectArray, - serializableObjectsArrayToBytes } from '@massalabs/as-types'; +import { + Args, + Result, + u8toByte, + u64ToBytes, + stringToBytes, + bytesToString, + bytesToU64, + byteToU8, + boolToByte, + bytesToSerializableObjectArray, + serializableObjectsArrayToBytes, +} from '@massalabs/as-types'; const DEPOSIT_EVENT_NAME = 'DEPOSIT'; const SUBMIT_TRANSACTION_EVENT_NAME = 'SUBMIT_TRANSACTION'; @@ -45,28 +47,26 @@ const GET_OWNERS_EVENT_NAME = 'GET_OWNERS'; export const OPERATION_INDEX_PREFIX_KEY = '00'; export const OWNER_PREFIX_KEY = '01'; -export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes('NB CONFIRMATIONS REQUIRED'); +export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes( + 'NB CONFIRMATIONS REQUIRED', +); export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); export const OPERATION_INDEX_KEY = stringToBytes('OPERATION INDEX'); - // ======================================================== // // ==== HELPER FUNCTIONS & TYPES ==== // // ======================================================== // -function makeOperationKey(opIndex: u64) : StaticArray { - - return stringToBytes(OPERATION_INDEX_PREFIX_KEY + - bytesToString(u64ToBytes(opIndex))); +function makeOperationKey(opIndex: u64): StaticArray { + return stringToBytes( + OPERATION_INDEX_PREFIX_KEY + bytesToString(u64ToBytes(opIndex)), + ); } -function makeOwnerKey(address: Address) : StaticArray { - - return stringToBytes(OWNER_PREFIX_KEY + - bytesToString(address.serialize())); +function makeOwnerKey(address: Address): StaticArray { + return stringToBytes(OWNER_PREFIX_KEY + bytesToString(address.serialize())); } - /** * Operation represent either a transfer of coins to a given address (a "transaction"), * or a call to a smart contract. @@ -81,130 +81,130 @@ function makeOwnerKey(address: Address) : StaticArray { * */ export class Operation { - address: Address; // the destination address (the recipient of coins or the smart contract to call) - amount: u64; // the amount - name: string; // the function call name, if any ("" means the operation is a simple coin transfer) - args: Args; // the args of the function call, if any - confirmedOwnerList: Array
; // the Array listing the owners who have already signed - confirmationWeightedSum: u8; // the confirmation total weight sum, for easy check - - constructor(address: Address = new Address(), - amount: u64 = 0, - name: string = "", - args: Args = new Args()) { - this.address = address; - this.amount = amount; - this.name = name; - this.args = args; - this.confirmedOwnerList = new Array
(); - this.confirmationWeightedSum = 0; - } + address: Address; // the destination address (the recipient of coins or the smart contract to call) + amount: u64; // the amount + name: string; // the function call name, if any ("" means the operation is a simple coin transfer) + args: Args; // the args of the function call, if any + confirmedOwnerList: Array
; // the Array listing the owners who have already signed + confirmationWeightedSum: u8; // the confirmation total weight sum, for easy check + + constructor( + address: Address = new Address(), + amount: u64 = 0, + name: string = '', + args: Args = new Args(), + ) { + this.address = address; + this.amount = amount; + this.name = name; + this.args = args; + this.confirmedOwnerList = new Array
(0); + this.confirmationWeightedSum = 0; + } - serialize() : StaticArray { - - // create a serializable Operation record to store in Storage - // Here we store some redundant information, like the confirmation - // total weight sum, trading some Storage space for Compute space, - // knowing that the Operation will be erased from Storage once executed - const argOperation = new Args() - .add
(this.address) - .add(this.amount) - .add(this.name) - .add>(this.args.serialize()) - .addSerializableObjectArray>(this.confirmedOwnerList) - .add(this.confirmationWeightedSum); - return argOperation.serialize(); - } + serialize(): StaticArray { + // create a serializable Operation record to store in Storage + // Here we store some redundant information, like the confirmation + // total weight sum, trading some Storage space for Compute space, + // knowing that the Operation will be erased from Storage once executed + const argOperation = new Args() + .add
(this.address) + .add(this.amount) + .add(this.name) + .add>(this.args.serialize()) + .addSerializableObjectArray>(this.confirmedOwnerList) + .add(this.confirmationWeightedSum); + return argOperation.serialize(); + } - deserialize(data: StaticArray) : void { - - const args = new Args(data); - - this.address = args - .nextSerializable
() - .expect('Error while deserializing Operation address'); - this.amount = args - .nextU64() - .expect('Error while deserializing Operation amount'); - this.name = args - .nextString() - .expect('Error while deserializing Operation name'); - let argData = args - .nextBytes() - .expect('Error while deserializing Operation args'); - this.args = new Args(argData); - this.confirmedOwnerList = args - .nextSerializableObjectArray
() - .expect('Error while deserializing Operation confirmedOwnerList'); - this.confirmationWeightedSum = args - .nextU8() - .expect('Error while deserializing Operation confirmationWeightedSum'); - } + deserialize(data: StaticArray): void { + const args = new Args(data); + + this.address = args + .nextSerializable
() + .expect('Error while deserializing Operation address'); + this.amount = args + .nextU64() + .expect('Error while deserializing Operation amount'); + this.name = args + .nextString() + .expect('Error while deserializing Operation name'); + let argData = args + .nextBytes() + .expect('Error while deserializing Operation args'); + this.args = new Args(argData); + this.confirmedOwnerList = args + .nextSerializableObjectArray
() + .expect('Error while deserializing Operation confirmedOwnerList'); + this.confirmationWeightedSum = args + .nextU8() + .expect('Error while deserializing Operation confirmationWeightedSum'); + } - isAlreadyConfirmed(owner: Address) : bool { - return this.confirmedOwnerList.includes(owner); - } + isAlreadyConfirmed(owner: Address): bool { + return this.confirmedOwnerList.includes(owner); + } - confirm(owner: Address, weight: u8) : void { - this.confirmedOwnerList.push(owner); - this.confirmationWeightedSum += weight; - } + confirm(owner: Address, weight: u8): void { + this.confirmedOwnerList.push(owner); + this.confirmationWeightedSum += weight; + } - revoke(owner: Address, weight: u8) : void { - let newConfirmedOwnerList = new Array
(); - for (let i = 0; i < this.confirmedOwnerList.length; i++) { - let address = this.confirmedOwnerList[i]; - if (address != owner) - newConfirmedOwnerList.push(address); - } - this.confirmedOwnerList = newConfirmedOwnerList; - assert(this.confirmationWeightedSum >= weight, - "fatal error: confirmationWeightedSum is less than revoked weight!"); - this.confirmationWeightedSum -= weight; + revoke(owner: Address, weight: u8): void { + let newConfirmedOwnerList = new Array
(0); + for (let i = 0; i < this.confirmedOwnerList.length; i++) { + let address = this.confirmedOwnerList[i]; + if (address != owner) newConfirmedOwnerList.push(address); } + this.confirmedOwnerList = newConfirmedOwnerList; + assert( + this.confirmationWeightedSum >= weight, + 'fatal error: confirmationWeightedSum is less than revoked weight!', + ); + this.confirmationWeightedSum -= weight; + } - isValidated() : bool { - let nbConfirmationsRequired = byteToU8(Storage.get(NB_CONFIRMATIONS_REQUIRED_KEY)); - return this.confirmationWeightedSum >= nbConfirmationsRequired; - } + isValidated(): bool { + let nbConfirmationsRequired = byteToU8( + Storage.get(NB_CONFIRMATIONS_REQUIRED_KEY), + ); + return this.confirmationWeightedSum >= nbConfirmationsRequired; + } - execute(callee: Address) : void { - if (this.name.length == 0) - // we have a transaction - Coins.transferCoinsOf(Context.callee(), this.address, this.amount); - else - // We have a call operation - Contract.call(this.address, this.name, this.args, this.amount); - } + execute(): void { + if (this.name.length == 0) + // we have a transaction + Coins.transferCoinsOf(Context.callee(), this.address, this.amount); + // We have a call operation + else Contract.call(this.address, this.name, this.args, this.amount); + } } -export function storeOperation(opIndex: u64, - operation: Operation): void { - +export function storeOperation(opIndex: u64, operation: Operation): void { // we simply use the Operation index as a key to store it Storage.set(makeOperationKey(opIndex), operation.serialize()); } export function retrieveOperation(opIndex: u64): Result { - const operationKey = makeOperationKey(opIndex); if (Storage.has(operationKey)) { - let operation = new Operation(); - operation.deserialize(Storage.get(operationKey)); - return new Result(operation); + let operation = new Operation(); + operation.deserialize(Storage.get(operationKey)); + return new Result(operation); } - return new Result(new Operation(), "unknown or already executed Operation index") + return new Result( + new Operation(), + 'unknown or already executed Operation index', + ); } export function hasOperation(opIndex: u64): bool { - return Storage.has(makeOperationKey(opIndex)); } function deleteOperation(opIndex: u64): void { - const operationKey = makeOperationKey(opIndex); Storage.del(operationKey); } @@ -213,7 +213,6 @@ function deleteOperation(opIndex: u64): void { // ==== CONSTRUCTOR ==== // // ======================================================== // - /** * Initialize the multisig wallet * Can be called only once @@ -238,52 +237,63 @@ export function constructor(stringifyArgs: StaticArray): void { assert(callerHasWriteAccess()); const args = new Args(stringifyArgs); - const MAX_OWNERS : i32 = 255; + const MAX_OWNERS: i32 = 255; // initialize nb of confirmations required const nbConfirmationsRequired = args .nextU8() .expect('Error while initializing nbConfirmationsRequired'); - assert(nbConfirmationsRequired > 0 , - 'The number of confirmations required must be at least 1'); + assert( + nbConfirmationsRequired > 0, + 'The number of confirmations required must be at least 1', + ); Storage.set(NB_CONFIRMATIONS_REQUIRED_KEY, u8toByte(nbConfirmationsRequired)); // initialize array of owners addresses - const ownerStringAddresses : Array = args + const ownerStringAddresses: Array = args .nextStringArray() .expect('Error while initializing owners addresses array'); // convert to actual Addresses - const ownerAddresses : Array
= []; + const ownerAddresses: Array
= []; for (let i = 0; i < ownerStringAddresses.length; i++) ownerAddresses.push(new Address(ownerStringAddresses[i])); - assert(ownerAddresses.length > 0 && ownerAddresses.length <= MAX_OWNERS, - 'The multisig must have between one and 255 owners'); + assert( + ownerAddresses.length > 0 && ownerAddresses.length <= MAX_OWNERS, + 'The multisig must have between one and 255 owners', + ); - assert(nbConfirmationsRequired as i32 <= ownerAddresses.length, - 'The number of confirmations cannot exceed the number of owners of the multisig'); + assert( + (nbConfirmationsRequired as i32) <= ownerAddresses.length, + 'The number of confirmations cannot exceed the number of owners of the multisig', + ); let ownerWeight = new Map(); for (let i = 0; i < ownerAddresses.length; i++) { - let address = ownerAddresses[i].toString(); - assert(address, "null address is not a valid owner"); - let currentWeight : u8 = 0; - if (ownerWeight.has(address)) - currentWeight = ownerWeight.get(address); - ownerWeight.set(address, currentWeight + 1); + let address = ownerAddresses[i].toString(); + assert(address, 'null address is not a valid owner'); + let currentWeight: u8 = 0; + if (ownerWeight.has(address)) currentWeight = ownerWeight.get(address); + ownerWeight.set(address, currentWeight + 1); } for (let i = 0; i < ownerAddresses.length; i++) { - let address = ownerAddresses[i]; - // we store directly each address weight in the Storage - Storage.set(makeOwnerKey(address), u8toByte(ownerWeight.get(address.toString()))); + let address = ownerAddresses[i]; + // we store directly each address weight in the Storage + Storage.set( + makeOwnerKey(address), + u8toByte(ownerWeight.get(address.toString())), + ); } // We store the list of owners to be queries later if needed - Storage.set(OWNERS_ADDRESSES_KEY, serializableObjectsArrayToBytes(ownerAddresses)); + Storage.set( + OWNERS_ADDRESSES_KEY, + serializableObjectsArrayToBytes(ownerAddresses), + ); // initialize operation index Storage.set(OPERATION_INDEX_KEY, u64ToBytes(0)); @@ -311,7 +321,6 @@ export function ms1_version(_: StaticArray): StaticArray { * @returns token name. */ export function ms1_deposit(_: StaticArray): void { - generateEvent( createEvent(DEPOSIT_EVENT_NAME, [ Context.caller().toString(), @@ -344,7 +353,6 @@ export function ms1_deposit(_: StaticArray): void { * @returns operation index. */ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { - const args = new Args(stringifyArgs); // initialize address @@ -370,7 +378,7 @@ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { Context.caller().toString(), opIndex.toString(), address.toString(), - amount.toString() + amount.toString(), ]), ); @@ -400,7 +408,6 @@ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { * @returns operation index. */ export function ms1_submitCall(stringifyArgs: StaticArray): u64 { - const args = new Args(stringifyArgs); // initialize address @@ -438,7 +445,7 @@ export function ms1_submitCall(stringifyArgs: StaticArray): u64 { opIndex.toString(), address.toString(), amount.toString(), - name + name, ]), ); @@ -461,7 +468,6 @@ export function ms1_submitCall(stringifyArgs: StaticArray): u64 { * - the operation index (u64) */ export function ms1_confirmOperation(stringifyArgs: StaticArray): void { - const args = new Args(stringifyArgs); // initialize operation index @@ -473,15 +479,17 @@ export function ms1_confirmOperation(stringifyArgs: StaticArray): void { // check owner is legit and retrieve the weight let ownerKey = makeOwnerKey(owner); - assert(Storage.has(ownerKey), "Caller address is not an owner"); + assert(Storage.has(ownerKey), 'Caller address is not an owner'); let weight = byteToU8(Storage.get(ownerKey)); // check the operation exists and retrieve it from Storage let operation = retrieveOperation(opIndex).unwrap(); // did we already confirm it? - assert(!operation.isAlreadyConfirmed(owner), - "The caller address has already confirmed this operation"); + assert( + !operation.isAlreadyConfirmed(owner), + 'The caller address has already confirmed this operation', + ); // confirm it and update the Storage operation.confirm(owner, weight); @@ -490,7 +498,7 @@ export function ms1_confirmOperation(stringifyArgs: StaticArray): void { generateEvent( createEvent(CONFIRM_OPERATION_EVENT_NAME, [ owner.toString(), - opIndex.toString() + opIndex.toString(), ]), ); } @@ -511,7 +519,6 @@ export function ms1_confirmOperation(stringifyArgs: StaticArray): void { * - the operation index (u64) */ export function ms1_executeOperation(stringifyArgs: StaticArray): void { - const args = new Args(stringifyArgs); // initialize operation index @@ -523,9 +530,11 @@ export function ms1_executeOperation(stringifyArgs: StaticArray): void { let operation = retrieveOperation(opIndex).unwrap(); // if the operation is sufficiently confirmed, execute it - assert(operation.isValidated(), - "The operation is unsufficiently confirmed, cannot execute"); - operation.execute(Context.callee()); + assert( + operation.isValidated(), + 'The operation is unsufficiently confirmed, cannot execute', + ); + operation.execute(); // clean up Storage and remove executed operation // NB: we could decide to keep it for archive purposes but then the @@ -558,7 +567,6 @@ export function ms1_executeOperation(stringifyArgs: StaticArray): void { * - the operation index (u64) */ export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { - const args = new Args(stringifyArgs); // initialize operation index @@ -570,15 +578,17 @@ export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { // check owner is legit and retrieve the weight let ownerKey = makeOwnerKey(owner); - assert(Storage.has(ownerKey), "Caller address is not an owner"); + assert(Storage.has(ownerKey), 'Caller address is not an owner'); let weight = byteToU8(Storage.get(ownerKey)); // check the operation exists and retrieve it from Storage let operation = retrieveOperation(opIndex).unwrap(); // did we actually already confirmed it? - assert(operation.isAlreadyConfirmed(owner), - "The caller address has not yet confirmed this operation"); + assert( + operation.isAlreadyConfirmed(owner), + 'The caller address has not yet confirmed this operation', + ); // revoke it and update the Storage operation.revoke(owner, weight); @@ -587,7 +597,7 @@ export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { generateEvent( createEvent(REVOKE_OPERATION_EVENT_NAME, [ owner.toString(), - opIndex.toString() + opIndex.toString(), ]), ); } @@ -601,15 +611,16 @@ export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { * ``` * */ -export function ms1_getOwners(_ : StaticArray) : StaticArray { - - let serializedOwnerAddresses = Storage.get(OWNERS_ADDRESSES_KEY) - let owners = bytesToSerializableObjectArray
(serializedOwnerAddresses).unwrap(); +export function ms1_getOwners(_: StaticArray): StaticArray { + let serializedOwnerAddresses = Storage.get(OWNERS_ADDRESSES_KEY); + let owners = bytesToSerializableObjectArray
( + serializedOwnerAddresses, + ).unwrap(); // generate the event with the list of owners - let eventPayLoad : Array = []; + let eventPayLoad: Array = []; for (let i = 0; i < owners.length; i++) - eventPayLoad.push(owners[i].toString()); + eventPayLoad.push(owners[i].toString()); generateEvent(createEvent(GET_OWNERS_EVENT_NAME, eventPayLoad)); return serializedOwnerAddresses; @@ -631,8 +642,9 @@ export function ms1_getOwners(_ : StaticArray) : StaticArray { * @param stringifyArgs - Args object serialized as a string containing: * - the operation index (u64) */ -export function ms1_getOperation(stringifyArgs : StaticArray) : StaticArray { - +export function ms1_getOperation( + stringifyArgs: StaticArray, +): StaticArray { const args = new Args(stringifyArgs); // initialize operation index @@ -644,13 +656,14 @@ export function ms1_getOperation(stringifyArgs : StaticArray) : StaticArray< let operation = retrieveOperation(opIndex).unwrap(); // generate the event with the list of confirmed owners - let eventPayLoad : Array = [ - opIndex.toString(), - operation.address.toString(), - operation.amount.toString(), - operation.confirmationWeightedSum.toString()]; + let eventPayLoad: Array = [ + opIndex.toString(), + operation.address.toString(), + operation.amount.toString(), + operation.confirmationWeightedSum.toString(), + ]; for (let i = 0; i < operation.confirmedOwnerList.length; i++) - eventPayLoad.push(operation.confirmedOwnerList[i].toString()); + eventPayLoad.push(operation.confirmedOwnerList[i].toString()); generateEvent(createEvent(RETRIEVE_OPERATION_EVENT_NAME, eventPayLoad)); return operation.serialize(); @@ -671,8 +684,9 @@ export function ms1_getOperation(stringifyArgs : StaticArray) : StaticArray< * @param stringifyArgs - Args object serialized as a string containing: * - the operation index (u64) */ -export function ms1_hasOperation(stringifyArgs : StaticArray) : StaticArray { - +export function ms1_hasOperation( + stringifyArgs: StaticArray, +): StaticArray { const args = new Args(stringifyArgs); // initialize operation index diff --git a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts index e40b5cc..ed9b57a 100644 --- a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts +++ b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts @@ -1,10 +1,11 @@ import { Address, call } from '@massalabs/massa-as-sdk'; -import { Args, - NoArg, - bytesToString, - byteToBool, - bytesToSerializableObjectArray, - bytesToU64 } from '@massalabs/as-types'; +import { + Args, + NoArg, + byteToBool, + bytesToSerializableObjectArray, + bytesToU64, +} from '@massalabs/as-types'; /** * The Massa's standard multisig implementation wrapper. @@ -50,10 +51,13 @@ export class MultisigWrapper { */ submitTransaction(address: Address, amount: u64): u64 { return bytesToU64( - call(this._origin, - 'ms1_submitTransaction', - new Args().add
(address).add(amount).serialize(), - 0)); + call( + this._origin, + 'ms1_submitTransaction', + new Args().add
(address).add(amount).serialize(), + 0, + ), + ); } /** @@ -69,15 +73,18 @@ export class MultisigWrapper { */ submitCall(address: Address, amount: u64, name: string, args: Args): u64 { return bytesToU64( - call(this._origin, - 'ms1_submitCall', - new Args() - .add
(address) - .add(amount) - .add(name) - .add>(args.serialize()) - .serialize(), - 0)); + call( + this._origin, + 'ms1_submitCall', + new Args() + .add
(address) + .add(amount) + .add(name) + .add>(args.serialize()) + .serialize(), + 0, + ), + ); } /** @@ -86,10 +93,12 @@ export class MultisigWrapper { * @param opIndex - the operation index */ confirmOperation(opIndex: u64): void { - call(this._origin, - 'ms1_confirmOperation', - new Args().add(opIndex).serialize(), - 0); + call( + this._origin, + 'ms1_confirmOperation', + new Args().add(opIndex).serialize(), + 0, + ); } /** @@ -98,10 +107,12 @@ export class MultisigWrapper { * @param opIndex - the operation index */ executeOperation(opIndex: u64): void { - call(this._origin, - 'ms1_executeOperation', - new Args().add(opIndex).serialize(), - 0); + call( + this._origin, + 'ms1_executeOperation', + new Args().add(opIndex).serialize(), + 0, + ); } /** @@ -110,10 +121,12 @@ export class MultisigWrapper { * @param opIndex - the operation index */ revokeOperation(opIndex: u64): void { - call(this._origin, - 'ms1_revokeOperation', - new Args().add(opIndex).serialize(), - 0); + call( + this._origin, + 'ms1_revokeOperation', + new Args().add(opIndex).serialize(), + 0, + ); } /** @@ -123,8 +136,8 @@ export class MultisigWrapper { */ getOwners(): Array
{ return bytesToSerializableObjectArray
( - call(this._origin, 'ms1_getOwners', NoArgs, 0)) - .unwrap(); + call(this._origin, 'ms1_getOwners', NoArgs, 0), + ).unwrap(); } /** @@ -136,10 +149,13 @@ export class MultisigWrapper { getOperation(opIndex: u64): Operation { let operation = new Operation(); operation.deserialize( - call(this._origin, - 'ms1_getOperation', - new Args().add(opIndex).serialize(), - 0)); + call( + this._origin, + 'ms1_getOperation', + new Args().add(opIndex).serialize(), + 0, + ), + ); return operation; } @@ -150,10 +166,13 @@ export class MultisigWrapper { * @returns true if the operation is defined and pending execution. */ hasOperation(opIndex: u64): bool { - return byteToBool ( - call(this._origin, - 'ms1_hasOperation', - new Args().add(opIndex).serialize(), - 0)); + return byteToBool( + call( + this._origin, + 'ms1_hasOperation', + new Args().add(opIndex).serialize(), + 0, + ), + ); } } From a8ab87f90c7f98053d58eaa2a3085df8a7babf76 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Tue, 22 Aug 2023 17:04:35 +0400 Subject: [PATCH 12/25] add support for operation cancelling. --- .../multisig/__tests__/multisig.spec.ts | 45 ++++++++++- .../assembly/contracts/multisig/multisig.ts | 74 ++++++++++++++++++- .../contracts/multisig/multisigWrapper.ts | 14 ++++ 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 23fdb55..f1d12f1 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -5,9 +5,11 @@ import { ms1_executeOperation, ms1_revokeConfirmation, ms1_getOperation, + ms1_cancelOperation, constructor, Operation, retrieveOperation, + hasOperation, } from '../multisig'; import { @@ -375,7 +377,6 @@ describe('Multisig contract tests', () => { // test of the call operation constructor test('submit call operation', () => { - // expect the operation index to be 1 expect( ms1_submitCall( new Args() @@ -403,6 +404,48 @@ describe('Multisig contract tests', () => { expect(operation.isValidated()).toBe(false); }); + // test of the operation cancelation + test('cancel operation by creator', () => { + switchUser(destination); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toBe(7); + + expect(() => { + ms1_cancelOperation(new Args().add(u64(7)).serialize()); + }).not.toThrow(); + + // check that the operation is indeed canceled + expect(hasOperation(7)).toBe(false); + switchUser(deployerAddress); + }); + + test('cancel operation by an owner', () => { + switchUser(destination); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toBe(8); + + switchUser(owners[1]); + expect(() => { + ms1_cancelOperation(new Args().add(u64(8)).serialize()); + }).not.toThrow(); + + // check that the operation is indeed canceled + expect(hasOperation(8)).toBe(false); + switchUser(deployerAddress); + }); + // operation 5 is validated, let's execute it test('execute transaction operation with success', () => { let destinationBalance = Coins.balanceOf(destination); diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index ab7283e..556d102 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -41,6 +41,7 @@ const SUBMIT_CALL_EVENT_NAME = 'SUBMIT_CALL'; const CONFIRM_OPERATION_EVENT_NAME = 'CONFIRM_OPERATION'; const EXECUTE_OPERATION_EVENT_NAME = 'EXECUTE_OPERATION'; const REVOKE_OPERATION_EVENT_NAME = 'REVOKE_OPERATION'; +const CANCEL_OPERATION_EVENT_NAME = 'CANCEL_OPERATION'; const RETRIEVE_OPERATION_EVENT_NAME = 'RETRIEVE_OPERATION'; const GET_OWNERS_EVENT_NAME = 'GET_OWNERS'; @@ -81,6 +82,7 @@ function makeOwnerKey(address: Address): StaticArray { * */ export class Operation { + creator: Address; // the destination creator of the operation. Is allowed to later cancel it. address: Address; // the destination address (the recipient of coins or the smart contract to call) amount: u64; // the amount name: string; // the function call name, if any ("" means the operation is a simple coin transfer) @@ -89,11 +91,13 @@ export class Operation { confirmationWeightedSum: u8; // the confirmation total weight sum, for easy check constructor( + creator: Address = new Address(), address: Address = new Address(), amount: u64 = 0, name: string = '', args: Args = new Args(), ) { + this.creator = creator; this.address = address; this.amount = amount; this.name = name; @@ -108,6 +112,7 @@ export class Operation { // total weight sum, trading some Storage space for Compute space, // knowing that the Operation will be erased from Storage once executed const argOperation = new Args() + .add
(this.creator) .add
(this.address) .add(this.amount) .add(this.name) @@ -120,6 +125,9 @@ export class Operation { deserialize(data: StaticArray): void { const args = new Args(data); + this.creator = args + .nextSerializable
() + .expect('Error while deserializing Operation creator'); this.address = args .nextSerializable
() .expect('Error while deserializing Operation address'); @@ -209,6 +217,20 @@ function deleteOperation(opIndex: u64): void { Storage.del(operationKey); } +/** + * Helper function to check if a given address is an owner of the multisig + * + */ +function isOwner(address: Address): bool { + let serializedOwnerAddresses = Storage.get(OWNERS_ADDRESSES_KEY); + let owners = bytesToSerializableObjectArray
( + serializedOwnerAddresses, + ).unwrap(); + + for (let i = 0; i < owners.length; i++) if (owners[i] == address) return true; + return false; +} + // ======================================================== // // ==== CONSTRUCTOR ==== // // ======================================================== // @@ -368,7 +390,7 @@ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { let opIndex = bytesToU64(Storage.get(OPERATION_INDEX_KEY)); opIndex++; - storeOperation(opIndex, new Operation(address, amount)); + storeOperation(opIndex, new Operation(Context.caller(), address, amount)); // update the new opIndex value for the next operation Storage.set(OPERATION_INDEX_KEY, u64ToBytes(opIndex)); @@ -434,7 +456,10 @@ export function ms1_submitCall(stringifyArgs: StaticArray): u64 { let opIndex = bytesToU64(Storage.get(OPERATION_INDEX_KEY)); opIndex++; - storeOperation(opIndex, new Operation(address, amount, name, callArgs)); + storeOperation( + opIndex, + new Operation(Context.caller(), address, amount, name, callArgs), + ); // update the new opIndex value for the next operation Storage.set(OPERATION_INDEX_KEY, u64ToBytes(opIndex)); @@ -551,6 +576,51 @@ export function ms1_executeOperation(stringifyArgs: StaticArray): void { ); } +/** + * Cancel an operation and generate an event in case of success + * NB: only the operation creator or one of the owners of the multisig + * can cancel the operation. This is to avoid cancel-bombing attacks on + * pending operations. + * + * @example + * ```typescript + * ms1_cancelOperation( + * new Args() + * .add(index) // the operation index + * .serialize(), + * ); + * ``` + * + * @param stringifyArgs - Args object serialized as a string containing: + * - the operation index (u64) + */ +export function ms1_cancelOperation(stringifyArgs: StaticArray): void { + const args = new Args(stringifyArgs); + + // initialize operation index + const opIndex = args + .nextU64() + .expect('Error while initializing operation index'); + + // check the operation exists and retrieve it from Storage + let operation = retrieveOperation(opIndex).unwrap(); + + assert( + Context.caller() == operation.creator || isOwner(Context.caller()), + 'invalid caller to cancel the operation. Only the owners or the creator are allowed.', + ); + + // clean up Storage and remove executed operation + deleteOperation(opIndex); + + generateEvent( + createEvent(CANCEL_OPERATION_EVENT_NAME, [ + Context.caller().toString(), + opIndex.toString(), + ]), + ); +} + /** * Revoke an operation confirmation by an owner, and generate an event * diff --git a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts index ed9b57a..d97b185 100644 --- a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts +++ b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts @@ -115,6 +115,20 @@ export class MultisigWrapper { ); } + /** + * Cancel an operation (only the creator or an owner can do this) + * + * @param opIndex - the operation index + */ + cancelOperation(opIndex: u64): void { + call( + this._origin, + 'ms1_cancelOperation', + new Args().add(opIndex).serialize(), + 0, + ); + } + /** * Revoke a operation identified by its index. * From b4de2ecde8521b3c032a7dd736bd7e5e87371bdd Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Wed, 23 Aug 2023 09:38:53 +0400 Subject: [PATCH 13/25] add a test for operation cancelation by no owner/creator (must fail) --- .../multisig/__tests__/multisig.spec.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index f1d12f1..12b58d4 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -446,6 +446,26 @@ describe('Multisig contract tests', () => { switchUser(deployerAddress); }); + test('cancel operation by no owner/creator (will fail)', () => { + switchUser(destination); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toBe(9); + + switchUser(deployerAddress); + expect(() => { + ms1_cancelOperation(new Args().add(u64(9)).serialize()); + }).toThrow(); + + // check that the operation is indeed canceled + expect(hasOperation(9)).toBe(true); + }); + // operation 5 is validated, let's execute it test('execute transaction operation with success', () => { let destinationBalance = Coins.balanceOf(destination); From d7219c0aa641ebcd65649a5e2d12d87de495cc1a Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Fri, 25 Aug 2023 17:07:35 +0400 Subject: [PATCH 14/25] modify cancellation behavior: now only the operation creator can cancel it (this avoids attacks from hostile/compromised owners to cancel-bomb any pending operation) --- .../contracts/multisig/__tests__/multisig.spec.ts | 4 ++-- .../assembly/contracts/multisig/multisig.ts | 10 +++++----- .../assembly/contracts/multisig/multisigWrapper.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 12b58d4..0261c2e 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -439,10 +439,10 @@ describe('Multisig contract tests', () => { switchUser(owners[1]); expect(() => { ms1_cancelOperation(new Args().add(u64(8)).serialize()); - }).not.toThrow(); + }).toThrow(); // check that the operation is indeed canceled - expect(hasOperation(8)).toBe(false); + expect(hasOperation(8)).toBe(true); switchUser(deployerAddress); }); diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 556d102..76ade3e 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -578,9 +578,9 @@ export function ms1_executeOperation(stringifyArgs: StaticArray): void { /** * Cancel an operation and generate an event in case of success - * NB: only the operation creator or one of the owners of the multisig - * can cancel the operation. This is to avoid cancel-bombing attacks on - * pending operations. + * NB: only the operation creator can cancel the operation. This + * is to avoid cancel-bombing attacks on pending operations by + * antagonist or compromised owners. * * @example * ```typescript @@ -606,8 +606,8 @@ export function ms1_cancelOperation(stringifyArgs: StaticArray): void { let operation = retrieveOperation(opIndex).unwrap(); assert( - Context.caller() == operation.creator || isOwner(Context.caller()), - 'invalid caller to cancel the operation. Only the owners or the creator are allowed.', + Context.caller() == operation.creator, + 'invalid caller to cancel the operation. Only the creator is allowed.', ); // clean up Storage and remove executed operation diff --git a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts index d97b185..ecd6f7a 100644 --- a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts +++ b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts @@ -116,7 +116,7 @@ export class MultisigWrapper { } /** - * Cancel an operation (only the creator or an owner can do this) + * Cancel an operation (only the creator can do this) * * @param opIndex - the operation index */ From 8804665650cf25057ec7e2f5f5e3b5b738ff3ffc Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Fri, 25 Aug 2023 17:11:02 +0400 Subject: [PATCH 15/25] export isOwner. --- smart-contracts/assembly/contracts/multisig/multisig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 76ade3e..2379ceb 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -221,7 +221,7 @@ function deleteOperation(opIndex: u64): void { * Helper function to check if a given address is an owner of the multisig * */ -function isOwner(address: Address): bool { +export function isOwner(address: Address): bool { let serializedOwnerAddresses = Storage.get(OWNERS_ADDRESSES_KEY); let owners = bytesToSerializableObjectArray
( serializedOwnerAddresses, From 77bbd00f504ccf2e7746302aea0f107f4c84d066 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Tue, 29 Aug 2023 12:02:06 +0400 Subject: [PATCH 16/25] only owners can submit an operation now: fix tests, add comments. --- .../multisig/__tests__/multisig.spec.ts | 43 ++++++++++++++++--- .../assembly/contracts/multisig/multisig.ts | 16 ++++++- .../contracts/multisig/multisigWrapper.ts | 2 + 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 0261c2e..d41526a 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -178,7 +178,22 @@ describe('Multisig contract tests', () => { expect(bytesToU64(Storage.get(OPERATION_INDEX_KEY))).toBe(0); }); + test('submit operation by non owner', () => { + // expect the operation submission to fail + expect(() => { + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ); + }).toThrow(); + }); + test('submit transaction operation', () => { + // pick owners[1] as the operation creator + switchUser(owners[1]); + // expect the operation index to be 1 expect( ms1_submitTransaction( @@ -205,6 +220,9 @@ describe('Multisig contract tests', () => { // validated operation test('confirm transaction operation [owners[0]]', () => { + // pick owners[1] as the operation creator + switchUser(owners[1]); + let confirmingOwnersIndexes: Array; let opIndex: u64; let totalWeight: u8; @@ -248,6 +266,9 @@ describe('Multisig contract tests', () => { // non validated operation test('confirm transaction operation [owners[1]]', () => { + // pick owners[1] as the operation creator + switchUser(owners[1]); + let confirmingOwnersIndexes: Array; let opIndex: u64; let totalWeight: u8; @@ -291,6 +312,9 @@ describe('Multisig contract tests', () => { // non validated operation test('confirm transaction operation [owners[2]]', () => { + // pick owners[1] as the operation creator + switchUser(owners[1]); + let confirmingOwnersIndexes: Array; let opIndex: u64; let totalWeight: u8; @@ -334,6 +358,9 @@ describe('Multisig contract tests', () => { // validated operation test('confirm transaction operation [owners[1], owners[2]]', () => { + // pick owners[1] as the operation creator + switchUser(owners[1]); + let confirmingOwnersIndexes: Array; let opIndex: u64; let totalWeight: u8; @@ -377,6 +404,9 @@ describe('Multisig contract tests', () => { // test of the call operation constructor test('submit call operation', () => { + // pick owners[1] as the operation creator + switchUser(owners[1]); + expect( ms1_submitCall( new Args() @@ -406,7 +436,8 @@ describe('Multisig contract tests', () => { // test of the operation cancelation test('cancel operation by creator', () => { - switchUser(destination); + // pick owners[1] as the operation creator + switchUser(owners[1]); expect( ms1_submitTransaction( new Args() @@ -425,8 +456,9 @@ describe('Multisig contract tests', () => { switchUser(deployerAddress); }); - test('cancel operation by an owner', () => { - switchUser(destination); + test('cancel operation by non creator', () => { + // pick owners[1] as the operation creator + switchUser(owners[1]); expect( ms1_submitTransaction( new Args() @@ -436,7 +468,7 @@ describe('Multisig contract tests', () => { ), ).toBe(8); - switchUser(owners[1]); + switchUser(owners[2]); expect(() => { ms1_cancelOperation(new Args().add(u64(8)).serialize()); }).toThrow(); @@ -447,7 +479,8 @@ describe('Multisig contract tests', () => { }); test('cancel operation by no owner/creator (will fail)', () => { - switchUser(destination); + // pick owners[1] as the operation creator + switchUser(owners[1]); expect( ms1_submitTransaction( new Args() diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 2379ceb..c7b66dc 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -357,7 +357,8 @@ export function ms1_deposit(_: StaticArray): void { // ======================================================== // /** - * Submit a transaction operation and generate an event with its index number + * Submit a transaction operation and generate an event with its index number. + * For security reasons, only owners can submit operations. * * @example * ```typescript @@ -375,6 +376,11 @@ export function ms1_deposit(_: StaticArray): void { * @returns operation index. */ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { + assert( + isOwner(Context.caller()), + 'Invalid caller to submit an operation. Only owners are allowed.', + ); + const args = new Args(stringifyArgs); // initialize address @@ -408,7 +414,8 @@ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { } /** - * Submit a call operation and generate an event with its index number + * Submit a call operation and generate an event with its index number. + * For security reasons, only owners can submit operations. * * @example * ```typescript @@ -430,6 +437,11 @@ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { * @returns operation index. */ export function ms1_submitCall(stringifyArgs: StaticArray): u64 { + assert( + isOwner(Context.caller()), + 'Invalid caller to submit an operation. Only owners are allowed.', + ); + const args = new Args(stringifyArgs); // initialize address diff --git a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts index ecd6f7a..53c7266 100644 --- a/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts +++ b/smart-contracts/assembly/contracts/multisig/multisigWrapper.ts @@ -43,6 +43,7 @@ export class MultisigWrapper { /** * Submit a transaction operation, and retrieve its index to be used by * the multisig owners to confirm it. + * For security reasons, only owners can submit operations. * * @param address - recipient address * @param amount - amount to transfer @@ -63,6 +64,7 @@ export class MultisigWrapper { /** * Submit a smart contract call operation, and retrieve its index to be * used by the multisig owners to confirm it. + * For security reasons, only owners can submit operations. * * @param address - the smart contract address for the operation * @param amount - the transfered amount attached to the call From bec0a8914ef43f2dd0f7e209bb5421fa5d108c53 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Tue, 5 Sep 2023 13:03:53 +0400 Subject: [PATCH 17/25] add a function to query the list of currently pending operation indexes --- .../assembly/contracts/multisig/multisig.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index c7b66dc..f16e370 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -33,6 +33,8 @@ import { boolToByte, bytesToSerializableObjectArray, serializableObjectsArrayToBytes, + bytesToFixedSizeArray, + fixedSizeArrayToBytes, } from '@massalabs/as-types'; const DEPOSIT_EVENT_NAME = 'DEPOSIT'; @@ -53,6 +55,7 @@ export const NB_CONFIRMATIONS_REQUIRED_KEY = stringToBytes( ); export const OWNERS_ADDRESSES_KEY = stringToBytes('OWNERS ADDRESSES'); export const OPERATION_INDEX_KEY = stringToBytes('OPERATION INDEX'); +export const OPERATION_LIST_KEY = stringToBytes('OPERATION LIST'); // ======================================================== // // ==== HELPER FUNCTIONS & TYPES ==== // @@ -191,6 +194,15 @@ export class Operation { export function storeOperation(opIndex: u64, operation: Operation): void { // we simply use the Operation index as a key to store it Storage.set(makeOperationKey(opIndex), operation.serialize()); + + let operationIndexList = bytesToFixedSizeArray( + Storage.get(OPERATION_LIST_KEY), + ); + operationIndexList.push(opIndex); + Storage.set( + OPERATION_LIST_KEY, + fixedSizeArrayToBytes(operationIndexList), + ); } export function retrieveOperation(opIndex: u64): Result { @@ -215,6 +227,19 @@ export function hasOperation(opIndex: u64): bool { function deleteOperation(opIndex: u64): void { const operationKey = makeOperationKey(opIndex); Storage.del(operationKey); + + let operationIndexList = bytesToFixedSizeArray( + Storage.get(OPERATION_LIST_KEY), + ); + let index = operationIndexList.indexOf(opIndex); + if (index !== -1) { + operationIndexList.splice(index, 1); + } + + Storage.set( + OPERATION_LIST_KEY, + fixedSizeArrayToBytes(operationIndexList), + ); } /** @@ -317,8 +342,9 @@ export function constructor(stringifyArgs: StaticArray): void { serializableObjectsArrayToBytes(ownerAddresses), ); - // initialize operation index + // initialize operation index and operation list Storage.set(OPERATION_INDEX_KEY, u64ToBytes(0)); + Storage.set(OPERATION_LIST_KEY, fixedSizeArrayToBytes([])); } /** @@ -778,3 +804,16 @@ export function ms1_hasOperation( return boolToByte(hasOperation(opIndex)); } + +/** + * Returns the list of all currently pending operations indexes (which + * can then be queried by ms1_getOperation to display a detail list of + * operations currently running) + * @example + * ```typescript + * let operationIndexList = bytesToFixedSizeArray(ms1_getOperationIndexList()); + * ``` + */ +export function ms1_getOperationIndexList(_: StaticArray): StaticArray { + return Storage.get(OPERATION_LIST_KEY); +} From 09f8a96a4cb1d2a74e8adcf29c48650438156e8d Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Tue, 5 Sep 2023 14:22:08 +0400 Subject: [PATCH 18/25] add some test, fix a bug in counting certain operations twice in the list. --- .../multisig/__tests__/multisig.spec.ts | 23 ++++++++++++++++--- .../assembly/contracts/multisig/multisig.ts | 8 ++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index d41526a..08a9d86 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -6,6 +6,7 @@ import { ms1_revokeConfirmation, ms1_getOperation, ms1_cancelOperation, + ms1_getOperationIndexList, constructor, Operation, retrieveOperation, @@ -29,6 +30,7 @@ import { bytesToString, serializableObjectsArrayToBytes, bytesToSerializableObjectArray, + bytesToFixedSizeArray, Serializable, Result, } from '@massalabs/as-types'; @@ -176,6 +178,12 @@ describe('Multisig contract tests', () => { // check that the operation index is set to 0 expect(bytesToU64(Storage.get(OPERATION_INDEX_KEY))).toBe(0); + + // check that there are no operation registered yet + let operationList = bytesToFixedSizeArray( + ms1_getOperationIndexList([]), + ); + expect(operationList.length).toBe(0); }); test('submit operation by non owner', () => { @@ -456,7 +464,7 @@ describe('Multisig contract tests', () => { switchUser(deployerAddress); }); - test('cancel operation by non creator', () => { + test('cancel operation by non creator (will fail)', () => { // pick owners[1] as the operation creator switchUser(owners[1]); expect( @@ -473,7 +481,7 @@ describe('Multisig contract tests', () => { ms1_cancelOperation(new Args().add(u64(8)).serialize()); }).toThrow(); - // check that the operation is indeed canceled + // check that the operation is indeed not canceled expect(hasOperation(8)).toBe(true); switchUser(deployerAddress); }); @@ -495,7 +503,7 @@ describe('Multisig contract tests', () => { ms1_cancelOperation(new Args().add(u64(9)).serialize()); }).toThrow(); - // check that the operation is indeed canceled + // check that the operation is indeed not canceled expect(hasOperation(9)).toBe(true); }); @@ -626,4 +634,13 @@ describe('Multisig contract tests', () => { expect(destinationBalance).toBe(initDestinationBalance); expect(contractBalance).toBe(initContractBalance); }); + + // check that the list of operations is now [1,3,4,6,8,9] + test('check operation index list', () => { + let operationList = bytesToFixedSizeArray( + ms1_getOperationIndexList([]), + ); + expect(operationList.length).toBe(7); + expect(operationList).toStrictEqual([1, 2, 3, 4, 6, 8, 9]); + }); }); diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index f16e370..bcf32ec 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -195,10 +195,15 @@ export function storeOperation(opIndex: u64, operation: Operation): void { // we simply use the Operation index as a key to store it Storage.set(makeOperationKey(opIndex), operation.serialize()); + // update the list of operation index if needed let operationIndexList = bytesToFixedSizeArray( Storage.get(OPERATION_LIST_KEY), ); - operationIndexList.push(opIndex); + let index = operationIndexList.indexOf(opIndex); + if (index == -1) { + operationIndexList.push(opIndex); + } + Storage.set( OPERATION_LIST_KEY, fixedSizeArrayToBytes(operationIndexList), @@ -228,6 +233,7 @@ function deleteOperation(opIndex: u64): void { const operationKey = makeOperationKey(opIndex); Storage.del(operationKey); + // update the list of operation index let operationIndexList = bytesToFixedSizeArray( Storage.get(OPERATION_LIST_KEY), ); From dcbd4c5b6180a13cbaf966d113a04efd26d1239c Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Wed, 6 Sep 2023 16:31:06 +0400 Subject: [PATCH 19/25] smart contract functions must always return StaticArray --- .../multisig/__tests__/multisig.spec.ts | 19 ++++++++++--------- .../assembly/contracts/multisig/multisig.ts | 12 ++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 08a9d86..f4a4d02 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -26,6 +26,7 @@ import { Args, byteToU8, bytesToU64, + u64ToBytes, stringToBytes, bytesToString, serializableObjectsArrayToBytes, @@ -210,7 +211,7 @@ describe('Multisig contract tests', () => { .add(u64(15000)) .serialize(), ), - ).toBe(1); + ).toStrictEqual(u64ToBytes(1)); // check that the operation is correctly stored let operationResult = retrieveOperation(1); @@ -245,7 +246,7 @@ describe('Multisig contract tests', () => { .add(u64(15000)) .serialize(), ), - ).toBe(opIndex); + ).toStrictEqual(u64ToBytes(opIndex)); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { @@ -291,7 +292,7 @@ describe('Multisig contract tests', () => { .add(u64(15000)) .serialize(), ), - ).toBe(opIndex); + ).toStrictEqual(u64ToBytes(opIndex)); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { @@ -337,7 +338,7 @@ describe('Multisig contract tests', () => { .add(u64(15000)) .serialize(), ), - ).toBe(opIndex); + ).toStrictEqual(u64ToBytes(opIndex)); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { @@ -383,7 +384,7 @@ describe('Multisig contract tests', () => { .add(u64(15000)) .serialize(), ), - ).toBe(opIndex); + ).toStrictEqual(u64ToBytes(opIndex)); totalWeight = 0; for (let i = 0; i < confirmingOwnersIndexes.length; ++i) { @@ -424,7 +425,7 @@ describe('Multisig contract tests', () => { .add>(new Args().add(42).serialize()) .serialize(), ), - ).toBe(6); + ).toStrictEqual(u64ToBytes(6)); // check that the operation is correctly stored let operationResult = retrieveOperation(6); @@ -453,7 +454,7 @@ describe('Multisig contract tests', () => { .add(u64(15000)) .serialize(), ), - ).toBe(7); + ).toStrictEqual(u64ToBytes(7)); expect(() => { ms1_cancelOperation(new Args().add(u64(7)).serialize()); @@ -474,7 +475,7 @@ describe('Multisig contract tests', () => { .add(u64(15000)) .serialize(), ), - ).toBe(8); + ).toStrictEqual(u64ToBytes(8)); switchUser(owners[2]); expect(() => { @@ -496,7 +497,7 @@ describe('Multisig contract tests', () => { .add(u64(15000)) .serialize(), ), - ).toBe(9); + ).toStrictEqual(u64ToBytes(9)); switchUser(deployerAddress); expect(() => { diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index bcf32ec..fbe615b 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -407,7 +407,9 @@ export function ms1_deposit(_: StaticArray): void { * - the amount of the operation (u64). * @returns operation index. */ -export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { +export function ms1_submitTransaction( + stringifyArgs: StaticArray, +): StaticArray { assert( isOwner(Context.caller()), 'Invalid caller to submit an operation. Only owners are allowed.', @@ -442,7 +444,7 @@ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { ]), ); - return opIndex; + return u64ToBytes(opIndex); } /** @@ -468,7 +470,9 @@ export function ms1_submitTransaction(stringifyArgs: StaticArray): u64 { * - the function arguments (Args). * @returns operation index. */ -export function ms1_submitCall(stringifyArgs: StaticArray): u64 { +export function ms1_submitCall( + stringifyArgs: StaticArray, +): StaticArray { assert( isOwner(Context.caller()), 'Invalid caller to submit an operation. Only owners are allowed.', @@ -518,7 +522,7 @@ export function ms1_submitCall(stringifyArgs: StaticArray): u64 { ]), ); - return opIndex; + return u64ToBytes(opIndex); } /** From d289d7f2bea5035f2037652110eba9445a6cf1d1 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Thu, 7 Sep 2023 16:15:05 +0400 Subject: [PATCH 20/25] fix typo in comments --- smart-contracts/assembly/contracts/multisig/multisig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index fbe615b..4592f10 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -490,12 +490,12 @@ export function ms1_submitCall( .nextU64() .expect('Error while initializing call operation amount'); - // initialize amount + // initialize function name const name = args .nextString() .expect('Error while initializing call operation function name'); - // initialize amount + // initialize args const callArgsData = args .nextBytes() .expect('Error while initializing call operation function args'); From d5dcf8a34e88b58b01bd71a5beaf0c4c3926d448 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Fri, 22 Sep 2023 17:24:00 +0400 Subject: [PATCH 21/25] do not export helper functions, copy them in the test code instead. --- .../multisig/__tests__/multisig.spec.ts | 31 +++++++++++++++++-- .../assembly/contracts/multisig/multisig.ts | 8 ++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index f4a4d02..e3d4dde 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -9,8 +9,6 @@ import { ms1_getOperationIndexList, constructor, Operation, - retrieveOperation, - hasOperation, } from '../multisig'; import { @@ -74,12 +72,41 @@ export const OPERATION_INDEX_KEY = stringToBytes('OPERATION INDEX'); export const OPERATION_INDEX_PREFIX_KEY = '00'; export const OWNER_PREFIX_KEY = '01'; +// ======================================================== // +// ==== HELPER FUNCTIONS ==== // +// ======================================================== // + function makeOwnerKey(owner: string): StaticArray { return stringToBytes( OWNER_PREFIX_KEY + bytesToString(new Args().add(owner).serialize()), ); } +function makeOperationKey(opIndex: u64): StaticArray { + return stringToBytes( + OPERATION_INDEX_PREFIX_KEY + bytesToString(u64ToBytes(opIndex)), + ); +} + +function retrieveOperation(opIndex: u64): Result { + const operationKey = makeOperationKey(opIndex); + + if (Storage.has(operationKey)) { + let operation = new Operation(); + operation.deserialize(Storage.get(operationKey)); + return new Result(operation); + } + + return new Result( + new Operation(), + 'unknown or already executed Operation index', + ); +} + +function hasOperation(opIndex: u64): bool { + return Storage.has(makeOperationKey(opIndex)); +} + // string are not serializable by default, we need this helper class class SerializableString implements Serializable { s: string; diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 4592f10..ed29064 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -191,7 +191,7 @@ export class Operation { } } -export function storeOperation(opIndex: u64, operation: Operation): void { +function storeOperation(opIndex: u64, operation: Operation): void { // we simply use the Operation index as a key to store it Storage.set(makeOperationKey(opIndex), operation.serialize()); @@ -210,7 +210,7 @@ export function storeOperation(opIndex: u64, operation: Operation): void { ); } -export function retrieveOperation(opIndex: u64): Result { +function retrieveOperation(opIndex: u64): Result { const operationKey = makeOperationKey(opIndex); if (Storage.has(operationKey)) { @@ -225,7 +225,7 @@ export function retrieveOperation(opIndex: u64): Result { ); } -export function hasOperation(opIndex: u64): bool { +function hasOperation(opIndex: u64): bool { return Storage.has(makeOperationKey(opIndex)); } @@ -252,7 +252,7 @@ function deleteOperation(opIndex: u64): void { * Helper function to check if a given address is an owner of the multisig * */ -export function isOwner(address: Address): bool { +function isOwner(address: Address): bool { let serializedOwnerAddresses = Storage.get(OWNERS_ADDRESSES_KEY); let owners = bytesToSerializableObjectArray
( serializedOwnerAddresses, From 775447a12262c8c9adbf342a2964175e182a8d56 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Fri, 22 Sep 2023 19:36:31 +0400 Subject: [PATCH 22/25] follow standard name for coin deposit --- smart-contracts/assembly/contracts/multisig/multisig.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index ed29064..90aa878 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -370,11 +370,12 @@ export function ms1_version(_: StaticArray): StaticArray { /** * Accepts funds to credit the multisig wallet + * Follow specs here: https://github.com/massalabs/massa-standards/issues/106 * * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18 * @returns token name. */ -export function ms1_deposit(_: StaticArray): void { +export function cr1_receive_coins(_: StaticArray): void { generateEvent( createEvent(DEPOSIT_EVENT_NAME, [ Context.caller().toString(), From a2ef0f5b58f3188f68cd91c850158a24424d9259 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Fri, 22 Sep 2023 19:43:32 +0400 Subject: [PATCH 23/25] executeOperation can only be run by owners. --- .../assembly/contracts/multisig/__tests__/multisig.spec.ts | 1 + smart-contracts/assembly/contracts/multisig/multisig.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index e3d4dde..7b81bc7 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -542,6 +542,7 @@ describe('Multisig contract tests', () => { let initDestinationBalance = destinationBalance; let initContractBalance = contractBalance; + switchUser(owners[1]); generateEvent( createEvent('BALANCES BEFORE', [ initDestinationBalance.toString(), diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 90aa878..6be1127 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -593,6 +593,11 @@ export function ms1_confirmOperation(stringifyArgs: StaticArray): void { * - the operation index (u64) */ export function ms1_executeOperation(stringifyArgs: StaticArray): void { + assert( + isOwner(Context.caller()), + 'Invalid caller to execute an operation. Only owners are allowed.', + ); + const args = new Args(stringifyArgs); // initialize operation index From 632d6653174462c4970349fa7fe8823bfdf5c0eb Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Sat, 23 Sep 2023 15:10:56 +0400 Subject: [PATCH 24/25] Executed operations are kept in storage. Operation cancel is renamed into "delete". Owners can delete eecuted operations and creator can delete at any time. --- .../multisig/__tests__/multisig.spec.ts | 44 +++++++++----- .../assembly/contracts/multisig/multisig.ts | 57 +++++++++++++------ 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 7b81bc7..451a8a9 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -5,7 +5,7 @@ import { ms1_executeOperation, ms1_revokeConfirmation, ms1_getOperation, - ms1_cancelOperation, + ms1_deleteOperation, ms1_getOperationIndexList, constructor, Operation, @@ -470,8 +470,8 @@ describe('Multisig contract tests', () => { expect(operation.isValidated()).toBe(false); }); - // test of the operation cancelation - test('cancel operation by creator', () => { + // test of the operation deletion + test('delete operation by creator', () => { // pick owners[1] as the operation creator switchUser(owners[1]); expect( @@ -484,15 +484,16 @@ describe('Multisig contract tests', () => { ).toStrictEqual(u64ToBytes(7)); expect(() => { - ms1_cancelOperation(new Args().add(u64(7)).serialize()); + ms1_deleteOperation(new Args().add(u64(7)).serialize()); }).not.toThrow(); - // check that the operation is indeed canceled + // check that the operation is indeed deleted expect(hasOperation(7)).toBe(false); switchUser(deployerAddress); }); - test('cancel operation by non creator (will fail)', () => { + test('delete operation by non creator (will fail because' + + 'the operation is not yet executed)', () => { // pick owners[1] as the operation creator switchUser(owners[1]); expect( @@ -506,15 +507,15 @@ describe('Multisig contract tests', () => { switchUser(owners[2]); expect(() => { - ms1_cancelOperation(new Args().add(u64(8)).serialize()); + ms1_deleteOperation(new Args().add(u64(8)).serialize()); }).toThrow(); - // check that the operation is indeed not canceled + // check that the operation is indeed not deleted expect(hasOperation(8)).toBe(true); switchUser(deployerAddress); }); - test('cancel operation by no owner/creator (will fail)', () => { + test('delete operation by no owner/creator (will fail)', () => { // pick owners[1] as the operation creator switchUser(owners[1]); expect( @@ -528,10 +529,10 @@ describe('Multisig contract tests', () => { switchUser(deployerAddress); expect(() => { - ms1_cancelOperation(new Args().add(u64(9)).serialize()); + ms1_deleteOperation(new Args().add(u64(9)).serialize()); }).toThrow(); - // check that the operation is indeed not canceled + // check that the operation is indeed not deleted expect(hasOperation(9)).toBe(true); }); @@ -554,10 +555,9 @@ describe('Multisig contract tests', () => { ms1_executeOperation(new Args().add(u64(5)).serialize()); }).not.toThrow(); - // once executed, the operation is deleted - expect(() => { - ms1_getOperation(new Args().add(u64(5)).serialize()); - }).toThrow(); + // retrieve the operation and check that it is marked as executed + let operation = retrieveOperation(u64(5)).unwrap(); + expect(operation.isExecuted).toBe(true); destinationBalance = Coins.balanceOf(destination); contractBalance = Coins.balanceOf(contractAddr); @@ -573,6 +573,19 @@ describe('Multisig contract tests', () => { expect(contractBalance + 15000).toBe(initContractBalance); }); + // test of the operation deletion after execution + test('delete operation by owner (non creator) once executed', () => { + // owners[1] is the operation creator for operation 5, let's pick owner2 + switchUser(owners[2]); + expect(() => { + ms1_deleteOperation(new Args().add(u64(5)).serialize()); + }).not.toThrow(); + + // check that the operation is indeed deleted + expect(hasOperation(5)).toBe(false); + switchUser(deployerAddress); + }); + // operation 4 is not validated, let's try to execute it test('execute transaction operation with failure', () => { let destinationBalance = Coins.balanceOf(destination); @@ -580,6 +593,7 @@ describe('Multisig contract tests', () => { let initDestinationBalance = destinationBalance; let initContractBalance = contractBalance; + switchUser(owners[1]); generateEvent( createEvent('BALANCES BEFORE', [ initDestinationBalance.toString(), diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index 6be1127..e3a5bf1 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -43,7 +43,7 @@ const SUBMIT_CALL_EVENT_NAME = 'SUBMIT_CALL'; const CONFIRM_OPERATION_EVENT_NAME = 'CONFIRM_OPERATION'; const EXECUTE_OPERATION_EVENT_NAME = 'EXECUTE_OPERATION'; const REVOKE_OPERATION_EVENT_NAME = 'REVOKE_OPERATION'; -const CANCEL_OPERATION_EVENT_NAME = 'CANCEL_OPERATION'; +const DELETE_OPERATION_EVENT_NAME = 'DELETE_OPERATION'; const RETRIEVE_OPERATION_EVENT_NAME = 'RETRIEVE_OPERATION'; const GET_OWNERS_EVENT_NAME = 'GET_OWNERS'; @@ -85,13 +85,15 @@ function makeOwnerKey(address: Address): StaticArray { * */ export class Operation { - creator: Address; // the destination creator of the operation. Is allowed to later cancel it. + creator: Address; // the destination creator of the operation. Is allowed to later delete it even if + // has not yet been executed. address: Address; // the destination address (the recipient of coins or the smart contract to call) amount: u64; // the amount name: string; // the function call name, if any ("" means the operation is a simple coin transfer) args: Args; // the args of the function call, if any confirmedOwnerList: Array
; // the Array listing the owners who have already signed confirmationWeightedSum: u8; // the confirmation total weight sum, for easy check + isExecuted: bool; // true when the operation has been executed constructor( creator: Address = new Address(), @@ -107,6 +109,7 @@ export class Operation { this.args = args; this.confirmedOwnerList = new Array
(0); this.confirmationWeightedSum = 0; + this.isExecuted = false; } serialize(): StaticArray { @@ -121,7 +124,8 @@ export class Operation { .add(this.name) .add>(this.args.serialize()) .addSerializableObjectArray>(this.confirmedOwnerList) - .add(this.confirmationWeightedSum); + .add(this.confirmationWeightedSum) + .add(this.isExecuted); return argOperation.serialize(); } @@ -150,6 +154,9 @@ export class Operation { this.confirmationWeightedSum = args .nextU8() .expect('Error while deserializing Operation confirmationWeightedSum'); + this.isExecuted = args + .nextBool() + .expect('Error while deserializing Operation isExecuted'); } isAlreadyConfirmed(owner: Address): bool { @@ -183,6 +190,7 @@ export class Operation { } execute(): void { + this.isExecuted = true; if (this.name.length == 0) // we have a transaction Coins.transferCoinsOf(Context.callee(), this.address, this.amount); @@ -221,7 +229,7 @@ function retrieveOperation(opIndex: u64): Result { return new Result( new Operation(), - 'unknown or already executed Operation index', + 'unknown Operation index', ); } @@ -559,6 +567,9 @@ export function ms1_confirmOperation(stringifyArgs: StaticArray): void { // check the operation exists and retrieve it from Storage let operation = retrieveOperation(opIndex).unwrap(); + // don't allow changes on executed operations + assert(!operation.isExecuted,'cannot modify an executed operation'); + // did we already confirm it? assert( !operation.isAlreadyConfirmed(owner), @@ -608,17 +619,20 @@ export function ms1_executeOperation(stringifyArgs: StaticArray): void { // check the operation exists and retrieve it from Storage let operation = retrieveOperation(opIndex).unwrap(); - // if the operation is sufficiently confirmed, execute it + // if the operation is sufficiently confirmed and not already executed, + // then execute it. + assert( + !operation.isExecuted, + 'The operation has already been executed, cannot execute twice', + ); assert( operation.isValidated(), 'The operation is unsufficiently confirmed, cannot execute', ); operation.execute(); - // clean up Storage and remove executed operation - // NB: we could decide to keep it for archive purposes but then the - // Storage cost would increase forever. - deleteOperation(opIndex); + // update the operation in storage to reflect its new isExecuted state + storeOperation(opIndex, operation); generateEvent( createEvent(EXECUTE_OPERATION_EVENT_NAME, [ @@ -631,14 +645,14 @@ export function ms1_executeOperation(stringifyArgs: StaticArray): void { } /** - * Cancel an operation and generate an event in case of success - * NB: only the operation creator can cancel the operation. This - * is to avoid cancel-bombing attacks on pending operations by - * antagonist or compromised owners. + * Delete an operation and generate an event in case of success + * NB: only the operation creator can delete the operation if it + * has not yet been executed. This is to avoid delete-bombing + * attacks on pending operations by antagonist or compromised owners. * * @example * ```typescript - * ms1_cancelOperation( + * ms1_deleteOperation( * new Args() * .add(index) // the operation index * .serialize(), @@ -648,7 +662,7 @@ export function ms1_executeOperation(stringifyArgs: StaticArray): void { * @param stringifyArgs - Args object serialized as a string containing: * - the operation index (u64) */ -export function ms1_cancelOperation(stringifyArgs: StaticArray): void { +export function ms1_deleteOperation(stringifyArgs: StaticArray): void { const args = new Args(stringifyArgs); // initialize operation index @@ -659,16 +673,20 @@ export function ms1_cancelOperation(stringifyArgs: StaticArray): void { // check the operation exists and retrieve it from Storage let operation = retrieveOperation(opIndex).unwrap(); + // any owner can delete an executed operation, other wise it has to be the + // operation creator. assert( + (operation.isExecuted && isOwner(Context.caller())) || Context.caller() == operation.creator, - 'invalid caller to cancel the operation. Only the creator is allowed.', + 'invalid caller to delete the operation. Owner can delete executed operations, ' + + 'or the creator if the operation is not yet executed.', ); - // clean up Storage and remove executed operation + // clean up Storage and delete operation deleteOperation(opIndex); generateEvent( - createEvent(CANCEL_OPERATION_EVENT_NAME, [ + createEvent(DELETE_OPERATION_EVENT_NAME, [ Context.caller().toString(), opIndex.toString(), ]), @@ -714,6 +732,9 @@ export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { 'The caller address has not yet confirmed this operation', ); + // don't allow changes on executed operations + assert(!operation.isExecuted,'cannot modify an executed operation'); + // revoke it and update the Storage operation.revoke(owner, weight); storeOperation(opIndex, operation); From 2f4c7516ba8356fe72b9f2dc611a82f22fc30e60 Mon Sep 17 00:00:00 2001 From: JC Baillie Date: Sat, 23 Sep 2023 15:34:18 +0400 Subject: [PATCH 25/25] npm run fmt --- .../multisig/__tests__/multisig.spec.ts | 47 ++++++++++--------- .../assembly/contracts/multisig/multisig.ts | 16 +++---- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts index 451a8a9..30a1133 100644 --- a/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts +++ b/smart-contracts/assembly/contracts/multisig/__tests__/multisig.spec.ts @@ -492,28 +492,31 @@ describe('Multisig contract tests', () => { switchUser(deployerAddress); }); - test('delete operation by non creator (will fail because' + - 'the operation is not yet executed)', () => { - // pick owners[1] as the operation creator - switchUser(owners[1]); - expect( - ms1_submitTransaction( - new Args() - .add
(new Address(destination)) - .add(u64(15000)) - .serialize(), - ), - ).toStrictEqual(u64ToBytes(8)); - - switchUser(owners[2]); - expect(() => { - ms1_deleteOperation(new Args().add(u64(8)).serialize()); - }).toThrow(); - - // check that the operation is indeed not deleted - expect(hasOperation(8)).toBe(true); - switchUser(deployerAddress); - }); + test( + 'delete operation by non creator (will fail because' + + 'the operation is not yet executed)', + () => { + // pick owners[1] as the operation creator + switchUser(owners[1]); + expect( + ms1_submitTransaction( + new Args() + .add
(new Address(destination)) + .add(u64(15000)) + .serialize(), + ), + ).toStrictEqual(u64ToBytes(8)); + + switchUser(owners[2]); + expect(() => { + ms1_deleteOperation(new Args().add(u64(8)).serialize()); + }).toThrow(); + + // check that the operation is indeed not deleted + expect(hasOperation(8)).toBe(true); + switchUser(deployerAddress); + }, + ); test('delete operation by no owner/creator (will fail)', () => { // pick owners[1] as the operation creator diff --git a/smart-contracts/assembly/contracts/multisig/multisig.ts b/smart-contracts/assembly/contracts/multisig/multisig.ts index e3a5bf1..2627736 100644 --- a/smart-contracts/assembly/contracts/multisig/multisig.ts +++ b/smart-contracts/assembly/contracts/multisig/multisig.ts @@ -85,8 +85,7 @@ function makeOwnerKey(address: Address): StaticArray { * */ export class Operation { - creator: Address; // the destination creator of the operation. Is allowed to later delete it even if - // has not yet been executed. + creator: Address; // the creator of the operation. address: Address; // the destination address (the recipient of coins or the smart contract to call) amount: u64; // the amount name: string; // the function call name, if any ("" means the operation is a simple coin transfer) @@ -227,10 +226,7 @@ function retrieveOperation(opIndex: u64): Result { return new Result(operation); } - return new Result( - new Operation(), - 'unknown Operation index', - ); + return new Result(new Operation(), 'unknown Operation index'); } function hasOperation(opIndex: u64): bool { @@ -568,7 +564,7 @@ export function ms1_confirmOperation(stringifyArgs: StaticArray): void { let operation = retrieveOperation(opIndex).unwrap(); // don't allow changes on executed operations - assert(!operation.isExecuted,'cannot modify an executed operation'); + assert(!operation.isExecuted, 'cannot modify an executed operation'); // did we already confirm it? assert( @@ -677,9 +673,9 @@ export function ms1_deleteOperation(stringifyArgs: StaticArray): void { // operation creator. assert( (operation.isExecuted && isOwner(Context.caller())) || - Context.caller() == operation.creator, + Context.caller() == operation.creator, 'invalid caller to delete the operation. Owner can delete executed operations, ' + - 'or the creator if the operation is not yet executed.', + 'or the creator if the operation is not yet executed.', ); // clean up Storage and delete operation @@ -733,7 +729,7 @@ export function ms1_revokeConfirmation(stringifyArgs: StaticArray): void { ); // don't allow changes on executed operations - assert(!operation.isExecuted,'cannot modify an executed operation'); + assert(!operation.isExecuted, 'cannot modify an executed operation'); // revoke it and update the Storage operation.revoke(owner, weight);