diff --git a/Cargo.lock b/Cargo.lock index 9b8e462f..c8d74452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -960,9 +960,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "humantime" @@ -1286,6 +1286,25 @@ dependencies = [ "num-traits", ] +[[package]] +name = "multisig-improved" +version = "1.0.0" +dependencies = [ + "adder", + "factorial", + "multiversx-sc", + "multiversx-sc-modules", + "multiversx-sc-scenario", +] + +[[package]] +name = "multisig-improved-meta" +version = "0.0.0" +dependencies = [ + "multisig-improved", + "multiversx-sc-meta-lib", +] + [[package]] name = "multisig-interact" version = "0.0.0" @@ -2502,9 +2521,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" [[package]] name = "syn" @@ -2794,9 +2813,9 @@ checksum = "151ac09978d3c2862c4e39b557f4eceee2cc72150bc4cb4f16abf061b6e381fb" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2805,9 +2824,9 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" diff --git a/Cargo.toml b/Cargo.toml index 8fac2496..2a6d8f06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ members = [ "contracts/multisig", "contracts/multisig/meta", "contracts/multisig/interact", + "contracts/multisig-improved", + "contracts/multisig-improved/meta", "contracts/mystery-box", "contracts/mystery-box/meta", "contracts/nft-escrow", diff --git a/contracts/multisig-improved/.gitignore b/contracts/multisig-improved/.gitignore new file mode 100644 index 00000000..9494cb14 --- /dev/null +++ b/contracts/multisig-improved/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +*/target/ + +# The mxpy output +output diff --git a/contracts/multisig-improved/Cargo.toml b/contracts/multisig-improved/Cargo.toml new file mode 100644 index 00000000..e76b0de5 --- /dev/null +++ b/contracts/multisig-improved/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "multisig-improved" +version = "1.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[lib] +path = "src/multisig_improved.rs" + +[dependencies.multiversx-sc] +version = "0.51.1" + +[dependencies.multiversx-sc-modules] +version = "0.51.1" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.51.1" + +[dev-dependencies.adder] +path = "../adder" + +[dev-dependencies.factorial] +path = "../factorial" diff --git a/contracts/multisig-improved/README.md b/contracts/multisig-improved/README.md new file mode 100644 index 00000000..0e5e3b86 --- /dev/null +++ b/contracts/multisig-improved/README.md @@ -0,0 +1,82 @@ +# Multisig Smart Contract (MSC) + +## Abstract +Cryptocurrencies can be one of the safest ways to store and manage wealth and value. By safeguarding a short list of words, the so-called seed or recovery phrase, anyone can protect thousands or millions of dollars in wealth and rest assured that no hacker or government can take it from them. In practice, it’s never so easy. + +One problem is that single-signature addresses rely on protecting a single private key. + +A better solution would use for example 2-of-3 multisig (or any combination of M-of-N for M ≤ N) quorum consisting of three separate private keys, held by three separate people or entities, and requiring any two to sign. This provides both security and redundancy since compromising any one key/person does not break the quorum: if one key is stolen or lost, the other two keyholders can sweep funds to another address (protected by a new quorum) by mutually signing a transaction moving the funds. + +As an example, let us imagine the following scenario. An institution launches a stablecoin. For safety, it is required that 3 out of 5 designated addresses sign any mint or burn transaction. Alice deploys the multisig SC. She adds Bob, Charlie, Dave and Eve as signers to the contract and sets the quorum to a minimum number of signers to 3. A quorum of signatures is also required to add or remove signers after the initial deployment. If for some reason, Eve’s account is compromised, Alice proposes removing Eve’s address from the signers’ board. Charlie and Dave sign, causing Eve’s address to be removed. There are only 4 addresses now registered in the contract. By the same process, signers could add 2 more addresses to their ranks, and increase the required quorum signatures from 3 to 4. + +Thus, essentially the multisig SC (we will refer to it, from now on, as MSC) enables multiple parties to sign or approve an action that takes place - typically a requirement for certain wallets, accounts, and smart contracts to prevent a rogue or hacked individual from performing detrimental actions. + +## Multisig transaction flow +On-chain multisig wallets are made possible by the fact that smart contracts can call other smart contracts. To execute a multisig transaction the flow would be: + +* A proposer or board member proposes an action. +* The proposed action receives an unique id/hash. +* N board members are notified (off-chain) to review the action with the specific id/hash. +* M out of N board members sign and approve the action. +* Any proposer or board member “performs the action”. + +## Design guidelines + +The required guidelines are: +* **No external contracts.** Calling methods of other contracts from within the methods of your own MSC is an amazing feature but should not be required for our simple use case. This also avoids exposing us to bugs. Because any arbitrarily complex function call can be executed, the MSC functions exactly as a standard wallet, but requires multiple signatures. + +* **No libraries.** Extending the last guideline, our contract has no upstream dependencies other than itself. This minimizes the chance of us misunderstanding or misusing some piece of library code. It also forces us to stay simple and eases auditing and eventually formal verification. + +* **Minimal internal state.** Complex applications can be built inside of MultiversX smart contracts. Storing minimal internal state allows our contract’s code to be simpler, and to be written in a more functional style, which is easier to test and reason about. + +* **Uses cold-storage.** The proposer which creates an action or spends from the contract has no special rights or access to the MSC. Authorization is handled by directly signing messages by the board members’ wallets that can be hardware wallets (Trezor; Ledger, etc.) or software wallets. + +* **Complete end-to-end testing.** The contract itself is exhaustively unit tested, audited and formally verified. + +## Roles +* **Deployer** - This is the address that deploys the MSC. By default this address is also the owner of the SC, but the owner can be changed later if required, as this is by default supported by the MultiversX protocol itself. This is the address that initially set up the configuration of the SC: board members, quorum, etc. It is important to mention that at deployment a very important configuration parameter is the option to allow the SC to be upgradeable or not. It is recommended for most use cases the SC to be non-upgradeable. Leaving the SC upgradable will give the owner of the SC the possibility to upgrade the SC and bypass the board, defeating the purpose of a MSC. If keeping the SC upgradeable is desired, a possible approach would be to make the owner another MSC, and both SCs could maintain the same board, so an upgrade action would need the approval of the board. + +* **Owner** - The deployer is initially the owner of the MSC, but if desired can be changed later by the current owner to a different owner. If the SC is upgradeable, the owner can also upgrade the SC. + +* **Board and quorum** - Multiple addresses need to be previously registered in the MSC, forming its board. A board member needs to be specifically registered as a board member, meaning for example that the owner or deployer of the MSC is not automatically a board member as well. Board members can vote on every action that the MSC performs. Signing a proposed action means the board members agree. Customarily, not all board members will need to sign every action; the MSC will configure how many signatures will be necessary for an action to be performed, the quorum. For instance, such a contract could have 5 board members, but a quorum of 3 would be enough to perform any action (M-of-N or in this case 3-of-5). + +* **Proposer** - The proposer is an address whitelisted in the MSC that can propose any action. An action can be any transaction; for example: send 10 eGLD to the treasury, mint more ESDT, etc. All board members are proposers by default but non-board members can be added as well to the list of whitelisted proposers. The proposers can only propose actions that then need to be approved and signed by the board members. The board member that proposes an action doesn’t need to sign it anymore; it is considered signed. + +## Functionality +The MSC should be able to perform most tasks that a regular account is able to perform. It should also be as general as possible. This means that it should operate with a generic concept of “Action”, that the board needs to sign before being performed. Actions can interact with the MSC itself (let's call them **internal actions**) or with external addresses or other SC (**external actions**). + +External actions have one and only one function, which is to send the action as a transaction whose sender is the MSC. Because any arbitrarily complex function call can be executed, the MSC functions exactly as a standard wallet, but requires multiple signatures. + +The types of internal actions should be the following: + +* Add a new member to the board. +* Remove a member from the board. This is only allowed if the new board size remains larger than the number of required signatures (quorum). Otherwise a new member needs to be added first. +* Change the quorum: the required number of signatures. Restriction: 1 <= quorum <= board size. +* Add a proposer. +* Remove a proposer. +* Change multisig contract owner (might be relevant for upgrading the MSC). +* Pay functions - by default we recommend the MSC to not be set up as a payable SC and any deposit or send transaction of eGLD or ESDT towards the MSC will need to call the desired pay function (if a transaction is not a call to these 2 functions then it is rejected immediately and the value is sent back to original sender): Deposit and/or Send. By making the MSC not a payable MSC we reduce the risk of users sending into the MSC funds that then are locked in the MSC or need to be manually send back to the user (in case of a mistake). By making the MSC not a payable MSC it also means that any deposit or send transaction needs to explicitly call the deposit or send function of the MSC. + +Any external and internal action will follow these steps and process: + +* **Propose action:** this will generate an action id. The action id is unique. +* **View action:** the board members need to see the action proposed before they approve it. +* **Sign action:** board members are allowed to sign. We might add an expiration date until board members can sign (until block x…). +* **Un-sign action:** board members are allowed to un-sign, i.e. to remove their signature from an action. Actions with 0 signatures are cleared from storage. This is to allow mistakes to be cleared. +* **Perform action (by id/hash)** - can be activated by proposers or board members. It is successful only if enough signatures are present from the board members. Whoever calls “perform action” needs to provide any eGLD required by the target, as well as to pay for gas. If there is a move balance kind of action, who calls the action pays the gas and the amount to be moved is taken from MSC balance. But the gas is always taken from the balance of the one who creates the "perform action" transaction. + +Also the following view functions will be available: +* **Count pending Actions:** returns the number of existing Actions. +* **List latest N pending Actions:** provides hashes of the latest N pending Actions, most recent being 0 and oldest being N-1. Usually called in tandem with Count. + +## Initializing the MSC + +There are 2 ways to do it: +* Provide all board member addresses and the number of required signatures directly in the constructor. +* Deployer deploys with just herself on the board and required signatures = 1. Then adds all other N-1 signers and sets required signatures to M. This works, but requires many transactions, so the constructor-only approach might be preferred. + +MSC is a deployable SC written in Rust and compiled in WASM. + +## Conclusion + +Multisig accounts are a critical safety feature for all users of the MultiversX ecosystem. Decentralised applications will rely heavily upon multisig security. diff --git a/contracts/multisig-improved/meta/Cargo.toml b/contracts/multisig-improved/meta/Cargo.toml new file mode 100644 index 00000000..ab0e23ec --- /dev/null +++ b/contracts/multisig-improved/meta/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "multisig-improved-meta" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[dependencies.multisig-improved] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.51.1" +default-features = false diff --git a/contracts/multisig-improved/meta/src/main.rs b/contracts/multisig-improved/meta/src/main.rs new file mode 100644 index 00000000..722702e3 --- /dev/null +++ b/contracts/multisig-improved/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} diff --git a/contracts/multisig-improved/multiversx.json b/contracts/multisig-improved/multiversx.json new file mode 100644 index 00000000..8d77ca31 --- /dev/null +++ b/contracts/multisig-improved/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} diff --git a/contracts/multisig-improved/sc-config.toml b/contracts/multisig-improved/sc-config.toml new file mode 100644 index 00000000..a05e1578 --- /dev/null +++ b/contracts/multisig-improved/sc-config.toml @@ -0,0 +1,17 @@ +[settings] +main = "main" + +[contracts.main] +name = "multisig" +add-unlabelled = true + +[contracts.full] +name = "multisig-full" +add-unlabelled = true +add-labels = ["multisig-external-view"] + +[contracts.view] +name = "multisig-view" +external-view = true +add-unlabelled = false +add-labels = ["multisig-external-view"] diff --git a/contracts/multisig-improved/src/action_types/discard.rs b/contracts/multisig-improved/src/action_types/discard.rs new file mode 100644 index 00000000..443b03a5 --- /dev/null +++ b/contracts/multisig-improved/src/action_types/discard.rs @@ -0,0 +1,35 @@ +use crate::common_types::action::{ActionId, ActionStatus}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait DiscardActionModule: + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + super::external_module::ExternalModuleModule + + super::propose::ProposeModule + + super::sign::SignModule + + super::perform::PerformModule + + super::execute_action::ExecuteActionModule + + crate::ms_endpoints::callbacks::CallbacksModule + + crate::check_signature::CheckSignatureModule + + crate::external::events::EventsModule +{ + fn discard_action(&self, action_id: ActionId) { + require!( + self.get_action_valid_signer_count(action_id) == 0, + "cannot discard action with valid signatures" + ); + + self.abort_batch_of_action(action_id); + self.clear_action(action_id); + } + + fn abort_batch_of_action(&self, action_id: ActionId) { + let batch_id = self.group_for_action(action_id).get(); + if batch_id != 0 { + self.action_group_status(batch_id) + .set(ActionStatus::Aborted); + } + } +} diff --git a/contracts/multisig-improved/src/action_types/execute_action.rs b/contracts/multisig-improved/src/action_types/execute_action.rs new file mode 100644 index 00000000..b190b5d3 --- /dev/null +++ b/contracts/multisig-improved/src/action_types/execute_action.rs @@ -0,0 +1,299 @@ +use crate::common_types::{ + action::{Action, ActionId, CallActionData, DeployArgs, EsdtTransferExecuteData, GasLimit}, + user_role::{change_user_role, UserRole}, +}; + +use crate::ms_endpoints::callbacks::CallbackProxy as _; + +multiversx_sc::imports!(); + +/// Gas required to finish transaction after transfer-execute. +const PERFORM_ACTION_FINISH_GAS: u64 = 300_000; +pub const MAX_BOARD_MEMBERS: usize = 30; +pub const MAX_MODULES: usize = 5; + +pub static BOARD_SIZE_TOO_BIG_ERR_MSG: &[u8] = b"board size cannot exceed limit"; + +#[multiversx_sc::module] +pub trait ExecuteActionModule: + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + super::external_module::ExternalModuleModule + + crate::external::events::EventsModule + + crate::ms_endpoints::callbacks::CallbacksModule +{ + fn try_execute_deploy( + &self, + action_id: ActionId, + action: &Action, + ) -> OptionalValue { + if let Action::SCDeployFromSource(args) = action { + let new_address = self.deploy_from_source(action_id, args.clone()); + + return OptionalValue::Some(new_address); + } + + OptionalValue::None + } + + fn execute_action_by_type(&self, action_id: ActionId, action: Action) { + match action { + Action::Nothing => {} + Action::AddBoardMember(board_member_address) => { + self.add_board_member(action_id, board_member_address); + } + Action::AddProposer(proposer_address) => self.add_proposer(action_id, proposer_address), + Action::RemoveUser(user_address) => self.remove_user(action_id, user_address), + Action::ChangeQuorum(new_quorum) => self.change_quorum(action_id, new_quorum), + Action::AddModule(sc_address) => self.add_module(action_id, sc_address), + Action::RemoveModule(sc_address) => self.remove_module(action_id, sc_address), + _ => self.execute_external_call(action_id, action), + }; + } + + fn execute_external_call(&self, action_id: ActionId, action: Action) { + match action { + Action::SendTransferExecuteEgld(call_data) => { + self.send_transfer_execute_egld(action_id, call_data); + } + Action::SendTransferExecuteEsdt(call_data) => { + self.send_transfer_execute_esdt(action_id, call_data); + } + Action::SendAsyncCall(call_data) => { + self.send_async_call(action_id, call_data); + } + Action::SCUpgradeFromSource { sc_address, args } => { + self.upgrade_from_source(action_id, sc_address, args); + } + _ => {} // Deploy case handled in "try_execute_deploy" function + } + } + + fn add_board_member(&self, action_id: ActionId, board_member_address: ManagedAddress) { + require!( + self.num_board_members().get() < MAX_BOARD_MEMBERS, + BOARD_SIZE_TOO_BIG_ERR_MSG + ); + + change_user_role(self, action_id, board_member_address, UserRole::BoardMember); + } + + fn add_proposer(&self, action_id: ActionId, proposer_address: ManagedAddress) { + change_user_role(self, action_id, proposer_address, UserRole::Proposer); + + // validation required for the scenario when a board member becomes a proposer + let quorum = self.quorum().get(); + let board_members = self.num_board_members().get(); + self.require_valid_quorum(quorum, board_members); + } + + fn remove_user(&self, action_id: ActionId, user_address: ManagedAddress) { + change_user_role(self, action_id, user_address, UserRole::None); + + let num_board_members = self.num_board_members().get(); + let num_proposers = self.num_proposers().get(); + require!( + num_board_members + num_proposers > 0, + "cannot remove all board members and proposers" + ); + + let quorum = self.quorum().get(); + self.require_valid_quorum(quorum, num_board_members); + } + + fn change_quorum(&self, action_id: ActionId, new_quorum: usize) { + let board_members = self.num_board_members().get(); + self.require_valid_quorum(new_quorum, board_members); + + self.quorum().set(new_quorum); + self.perform_change_quorum_event(action_id, new_quorum); + } + + fn add_module(&self, action_id: ActionId, sc_address: ManagedAddress) { + self.nr_deployed_modules().update(|nr_deployed_modules| { + *nr_deployed_modules += 1; + + require!( + *nr_deployed_modules <= MAX_MODULES, + "May not add more modules" + ); + }); + + let module_id = self.module_id().insert_new(&sc_address); + let _ = self.active_modules_ids().insert(module_id); + + self.perform_add_module_event(action_id, &sc_address); + } + + fn remove_module(&self, action_id: ActionId, sc_address: ManagedAddress) { + let module_id = self.module_id().remove_by_address(&sc_address); + if module_id != NULL_ID { + let _ = self.active_modules_ids().swap_remove(&module_id); + + self.nr_deployed_modules() + .update(|nr_deployed_modules| *nr_deployed_modules -= 1); + } + + self.perform_remove_module_event(action_id, &sc_address); + } + + fn send_transfer_execute_egld( + &self, + action_id: ActionId, + call_data: CallActionData, + ) { + let gas = call_data + .opt_gas_limit + .unwrap_or_else(|| self.ensure_and_get_gas_for_transfer_exec()); + + self.perform_transfer_execute_egld_event( + action_id, + &call_data.to, + &call_data.egld_amount, + gas, + &call_data.endpoint_name, + call_data.arguments.as_multi(), + ); + + let result = self.send_raw().direct_egld_execute( + &call_data.to, + &call_data.egld_amount, + gas, + &call_data.endpoint_name, + &call_data.arguments.into(), + ); + if let Result::Err(e) = result { + sc_panic!(e); + } + } + + fn send_transfer_execute_esdt( + &self, + action_id: ActionId, + call_data: EsdtTransferExecuteData, + ) { + let gas = call_data + .opt_gas_limit + .unwrap_or_else(|| self.ensure_and_get_gas_for_transfer_exec()); + + self.perform_transfer_execute_esdt_event( + action_id, + &call_data.to, + &call_data.tokens, + gas, + &call_data.endpoint_name, + call_data.arguments.as_multi(), + ); + + let result = self.send_raw().multi_esdt_transfer_execute( + &call_data.to, + &call_data.tokens, + gas, + &call_data.endpoint_name, + &call_data.arguments.into(), + ); + if let Result::Err(e) = result { + sc_panic!(e); + } + } + + fn send_async_call(&self, action_id: ActionId, call_data: CallActionData) { + let gas = call_data + .opt_gas_limit + .unwrap_or_else(|| self.ensure_and_get_gas_for_transfer_exec()); + self.perform_async_call_event( + action_id, + &call_data.to, + &call_data.egld_amount, + gas, + &call_data.endpoint_name, + call_data.arguments.as_multi(), + ); + self.send() + .contract_call::<()>(call_data.to, call_data.endpoint_name) + .with_egld_transfer(call_data.egld_amount) + .with_raw_arguments(call_data.arguments.into()) + .with_gas_limit(gas) + .async_call() + .with_callback(self.callbacks().perform_async_call_callback()) + .call_and_exit(); + } + + fn deploy_from_source( + &self, + action_id: ActionId, + args: DeployArgs, + ) -> ManagedAddress { + let gas_left = self.blockchain().get_gas_left(); + self.perform_deploy_from_source_event( + action_id, + &args.amount, + &args.source, + args.code_metadata, + gas_left, + args.arguments.as_multi(), + ); + let (new_address, _) = self.send_raw().deploy_from_source_contract( + gas_left, + &args.amount, + &args.source, + args.code_metadata, + &args.arguments.into(), + ); + + new_address + } + + fn upgrade_from_source( + &self, + action_id: ActionId, + sc_address: ManagedAddress, + args: DeployArgs, + ) { + let gas_left = self.blockchain().get_gas_left(); + self.perform_upgrade_from_source_event( + action_id, + &sc_address, + &args.amount, + &args.source, + args.code_metadata, + gas_left, + args.arguments.as_multi(), + ); + self.send_raw().upgrade_from_source_contract( + &sc_address, + gas_left, + &args.amount, + &args.source, + args.code_metadata, + &args.arguments.into(), + ); + } + + fn clear_action(&self, action_id: ActionId) { + self.action_mapper().clear_entry_unchecked(action_id); + self.action_signer_ids(action_id).clear(); + + let group_id = self.group_for_action(action_id).take(); + if group_id != 0 { + let _ = self.action_groups(group_id).swap_remove(&action_id); + } + } + + fn ensure_and_get_gas_for_transfer_exec(&self) -> GasLimit { + let gas_left = self.blockchain().get_gas_left(); + require!( + gas_left > PERFORM_ACTION_FINISH_GAS, + "insufficient gas for call" + ); + + gas_left - PERFORM_ACTION_FINISH_GAS + } + + fn require_valid_quorum(&self, quorum: usize, num_board_members: usize) { + require!( + quorum <= num_board_members, + "quorum cannot exceed board size" + ); + } +} diff --git a/contracts/multisig-improved/src/action_types/external_module.rs b/contracts/multisig-improved/src/action_types/external_module.rs new file mode 100644 index 00000000..ebfd3bcb --- /dev/null +++ b/contracts/multisig-improved/src/action_types/external_module.rs @@ -0,0 +1,75 @@ +use multiversx_sc_modules::transfer_role_proxy::PaymentsVec; + +multiversx_sc::imports!(); + +pub type ModuleId = AddressId; + +mod external_module_proxy { + use multiversx_sc_modules::transfer_role_proxy::PaymentsVec; + + multiversx_sc::imports!(); + + #[multiversx_sc::proxy] + pub trait ExternalModuleProxy { + #[view(canExecute)] + fn can_execute( + &self, + proposer: ManagedAddress, + sc_address: ManagedAddress, + egld_value: BigUint, + esdt_payments: PaymentsVec, + ) -> bool; + } +} + +#[multiversx_sc::module] +pub trait ExternalModuleModule: + crate::common_functions::CommonFunctionsModule + crate::state::StateModule +{ + fn can_execute_action( + &self, + proposer: &ManagedAddress, + sc_address: &ManagedAddress, + egld_value: &BigUint, + esdt_payments: &PaymentsVec, + ) -> bool { + let module_id_mapper = self.module_id(); + for module_id in self.active_modules_ids().iter() { + let opt_module_address = module_id_mapper.get_address(module_id); + require!(opt_module_address.is_some(), "Invalid setup"); + + let module_address = unsafe { opt_module_address.unwrap_unchecked() }; + let can_execute: bool = self + .external_sc_proxy(module_address) + .can_execute( + proposer.clone(), + sc_address.clone(), + egld_value.clone(), + esdt_payments.clone(), + ) + .execute_on_dest_context(); + + if can_execute { + return true; + } + } + + false + } + + #[proxy] + fn external_sc_proxy( + &self, + sc_address: ManagedAddress, + ) -> external_module_proxy::Proxy; + + #[storage_mapper("moduleId")] + fn module_id(&self) -> AddressToIdMapper; + + #[view(getNrDeployedModules)] + #[storage_mapper("nrDeployModules")] + fn nr_deployed_modules(&self) -> SingleValueMapper; + + #[storage_mapper("activeModulesIds")] + fn active_modules_ids(&self) -> UnorderedSetMapper; +} diff --git a/contracts/multisig-improved/src/action_types/mod.rs b/contracts/multisig-improved/src/action_types/mod.rs new file mode 100644 index 00000000..aac51d01 --- /dev/null +++ b/contracts/multisig-improved/src/action_types/mod.rs @@ -0,0 +1,6 @@ +pub mod discard; +pub mod execute_action; +pub mod external_module; +pub mod perform; +pub mod propose; +pub mod sign; diff --git a/contracts/multisig-improved/src/action_types/perform.rs b/contracts/multisig-improved/src/action_types/perform.rs new file mode 100644 index 00000000..10987ee1 --- /dev/null +++ b/contracts/multisig-improved/src/action_types/perform.rs @@ -0,0 +1,114 @@ +use multiversx_sc_modules::transfer_role_proxy::PaymentsVec; + +use crate::common_types::action::{ + ActionFullInfo, ActionId, ActionStatus, CallActionData, EsdtTransferExecuteData, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait PerformModule: + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + super::external_module::ExternalModuleModule + + crate::external::events::EventsModule + + super::execute_action::ExecuteActionModule + + crate::ms_endpoints::callbacks::CallbacksModule +{ + fn perform_action_by_id(&self, action_id: ActionId) -> OptionalValue { + let action = self.action_mapper().get(action_id); + + let group_id = self.group_for_action(action_id).get(); + if group_id != 0 { + let group_status = self.action_group_status(group_id).get(); + require!( + group_status == ActionStatus::Available, + "cannot perform actions of an aborted batch" + ); + } + + self.start_perform_action_event(&ActionFullInfo { + action_id, + action_data: action.clone(), + signers: self.get_action_signers(action_id), + group_id, + }); + + // clean up storage + // happens before actual execution, because the match provides the return on each branch + // syntax aside, the async_call_raw kills contract execution so cleanup cannot happen afterwards + self.clear_action(action_id); + + let opt_address = self.try_execute_deploy(action_id, &action); + if opt_address.is_some() { + return opt_address; + } + + self.execute_action_by_type(action_id, action); + + OptionalValue::None + } + + fn try_perform_egld_action_directly( + &self, + proposer: &ManagedAddress, + action_id: ActionId, + call_data: &CallActionData, + ) -> bool { + let can_execute = self.can_execute_action( + proposer, + &call_data.to, + &call_data.egld_amount, + &PaymentsVec::new(), + ); + if !can_execute { + return false; + } + + let _ = self.perform_action_by_id(action_id); + + true + } + + fn try_perform_esdt_action_directly( + &self, + proposer: &ManagedAddress, + action_id: ActionId, + call_data: &EsdtTransferExecuteData, + ) -> bool { + let can_execute = + self.can_execute_action(proposer, &call_data.to, &BigUint::zero(), &call_data.tokens); + if !can_execute { + return false; + } + + let _ = self.perform_action_by_id(action_id); + + true + } + + fn try_perform_action(&self, action_id: ActionId) -> OptionalValue { + let (_, caller_role) = self.get_caller_id_and_role(); + caller_role.require_can_perform_action::(); + + if !self.quorum_reached(action_id) { + return OptionalValue::None; + } + + let group_id = self.group_for_action(action_id).get(); + require!(group_id == 0, "May not execute this action by itself"); + + self.perform_action_by_id(action_id) + } + + fn require_same_shard(&self, sc_address: &ManagedAddress) { + if cfg!(debug_assertions) { + return; + } + + let own_address = self.blockchain().get_sc_address(); + let own_shard = self.blockchain().get_shard_of_address(&own_address); + let sc_shard = self.blockchain().get_shard_of_address(sc_address); + require!(own_shard == sc_shard, "Must be same shard"); + } +} diff --git a/contracts/multisig-improved/src/action_types/propose.rs b/contracts/multisig-improved/src/action_types/propose.rs new file mode 100644 index 00000000..d21a3782 --- /dev/null +++ b/contracts/multisig-improved/src/action_types/propose.rs @@ -0,0 +1,98 @@ +use crate::common_types::{ + action::{Action, ActionId}, + signature::SignatureArg, +}; + +multiversx_sc::imports!(); + +static ALL_TRANSFER_EXEC_SAME_SHARD_ERR_MSG: &[u8] = b"All transfer exec must be to the same shard"; + +#[multiversx_sc::module] +pub trait ProposeModule: + crate::check_signature::CheckSignatureModule + + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule +{ + fn propose_action( + &self, + action: Action, + opt_signature: OptionalValue>, + ) -> ActionId { + let proposer = self.get_proposer(&action, opt_signature); + let (proposer_id, proposer_role) = self.get_id_and_role(&proposer); + proposer_role.require_can_propose::(); + + let action_id = self.action_mapper().push(&action); + let quorum = self.quorum().get(); + self.quorum_for_action(action_id).set(quorum); + + if proposer_role.can_sign() { + // also sign + // since the action is newly created, the proposer can be the only signer + let _ = self.action_signer_ids(action_id).insert(proposer_id); + } + + action_id + } + + fn get_proposer( + &self, + action: &Action, + opt_signature: OptionalValue>, + ) -> ManagedAddress { + match opt_signature { + OptionalValue::Some(sig_arg) => { + let proposer = sig_arg.user_address.clone(); + self.check_proposal_signature(action, sig_arg); + + proposer + } + OptionalValue::None => self.blockchain().get_caller(), + } + } + + fn get_proposer_no_sig_check( + &self, + opt_signature: &OptionalValue>, + ) -> ManagedAddress { + match opt_signature { + OptionalValue::Some(sig_arg) => sig_arg.user_address.clone(), + OptionalValue::None => self.blockchain().get_caller(), + } + } + + fn require_valid_action_type(&self, action: &Action) { + require!( + !action.is_nothing() && !action.is_async_call() && !action.is_sc_upgrade(), + "Invalid action" + ); + } + + fn ensure_valid_transfer_action(&self, action: &Action) { + let own_sc_address = self.blockchain().get_sc_address(); + let own_shard = self.blockchain().get_shard_of_address(&own_sc_address); + match action { + Action::SendTransferExecuteEgld(call_data) => { + let other_sc_shard = self.blockchain().get_shard_of_address(&call_data.to); + require!( + call_data.egld_amount > 0 || !call_data.endpoint_name.is_empty(), + "proposed action has no effect" + ); + require!( + own_shard == other_sc_shard, + ALL_TRANSFER_EXEC_SAME_SHARD_ERR_MSG + ); + } + Action::SendTransferExecuteEsdt(call_data) => { + require!(!call_data.tokens.is_empty(), "No tokens to transfer"); + + let other_sc_shard = self.blockchain().get_shard_of_address(&call_data.to); + require!( + own_shard == other_sc_shard, + ALL_TRANSFER_EXEC_SAME_SHARD_ERR_MSG + ); + } + _ => {} + } + } +} diff --git a/contracts/multisig-improved/src/action_types/sign.rs b/contracts/multisig-improved/src/action_types/sign.rs new file mode 100644 index 00000000..e59b566b --- /dev/null +++ b/contracts/multisig-improved/src/action_types/sign.rs @@ -0,0 +1,29 @@ +use crate::common_types::action::ActionId; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait SignModule: + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + super::external_module::ExternalModuleModule + + super::propose::ProposeModule + + super::perform::PerformModule + + super::execute_action::ExecuteActionModule + + crate::ms_endpoints::callbacks::CallbacksModule + + crate::external::events::EventsModule + + crate::check_signature::CheckSignatureModule +{ + fn unsign_action(&self, action_id: ActionId, caller_id: AddressId) { + self.require_action_exists(action_id); + + let _ = self.action_signer_ids(action_id).swap_remove(&caller_id); + } + + fn add_signatures(&self, action_id: ActionId, board_members: &ManagedVec) { + let mut mapper = self.action_signer_ids(action_id); + for board_member in board_members { + let _ = mapper.insert(board_member); + } + } +} diff --git a/contracts/multisig-improved/src/check_signature.rs b/contracts/multisig-improved/src/check_signature.rs new file mode 100644 index 00000000..0abee19e --- /dev/null +++ b/contracts/multisig-improved/src/check_signature.rs @@ -0,0 +1,181 @@ +use multiversx_sc::api::SHA256_RESULT_LEN; + +use crate::common_types::{ + action::{Action, ActionId, GroupId, Nonce}, + signature::{ActionType, ItemToSign, Signature, SignatureArg}, +}; + +multiversx_sc::imports!(); + +static ENCODING_NONCE_ERR_MSG: &[u8] = b"Error encoding user nonce to buffer"; +static ENCODING_ACTION_TYPE_ERR_MSG: &[u8] = b"Error encoding action type to buffer"; + +#[multiversx_sc::module] +pub trait CheckSignatureModule: + crate::common_functions::CommonFunctionsModule + crate::state::StateModule +{ + fn check_proposal_signature( + &self, + action: &Action, + signature: SignatureArg, + ) { + let mut multi_arg = MultiValueEncoded::new(); + multi_arg.push(signature); + + let _ = self.check_sig_common(ActionType::Propose, ItemToSign::Propose(action), multi_arg); + } + + fn check_single_action_signatures( + &self, + action_id: ActionId, + signatures: MultiValueEncoded>, + ) -> ManagedVec { + let action = self.action_mapper().get_unchecked(action_id); + + self.check_sig_common( + ActionType::SimpleAction, + ItemToSign::Action(&action), + signatures, + ) + } + + fn check_group_signatures( + &self, + group_id: GroupId, + signatures: MultiValueEncoded>, + ) -> ManagedVec { + self.check_sig_common(ActionType::Group, ItemToSign::Group(group_id), signatures) + } + + fn check_sig_common( + &self, + action_type: ActionType, + item_to_sign: ItemToSign, + signatures: MultiValueEncoded>, + ) -> ManagedVec { + let mut board_members = ManagedVec::new(); + + let id_mapper = self.user_ids(); + for sig_arg in signatures { + let user_id = id_mapper.get_id_non_zero(&sig_arg.user_address); + + self.check_base_signature_validity(&sig_arg, action_type); + self.check_signature_by_item_to_sign(sig_arg, item_to_sign.clone()); + + board_members.push(user_id); + } + + board_members + } + + fn check_base_signature_validity( + &self, + sig_arg: &SignatureArg, + requested_action_type: ActionType, + ) { + let (_, user_role) = self.get_id_and_role(&sig_arg.user_address); + user_role.require_can_sign::(); + + let next_user_nonce = self.get_and_increment_user_nonce(&sig_arg.user_address); + require!(sig_arg.nonce == next_user_nonce, "Invalid nonce"); + + sig_arg + .action_type + .require_is_type::(requested_action_type); + } + + fn check_signature_by_item_to_sign( + &self, + sig_arg: SignatureArg, + item_to_sign: ItemToSign, + ) { + let bytes_to_sign = match item_to_sign { + ItemToSign::Propose(action) => { + self.serialize_and_hash_proposal(action, &sig_arg.user_address, sig_arg.nonce) + } + ItemToSign::Action(action) => { + self.serialize_and_hash_action(action, &sig_arg.user_address, sig_arg.nonce) + } + ItemToSign::Group(group_id) => { + self.serialize_and_hash_group(group_id, &sig_arg.user_address, sig_arg.nonce) + } + }; + let signature_struct = Signature { + signature_type: sig_arg.signature_type, + raw_sig_bytes: sig_arg.raw_sig_bytes, + }; + signature_struct + .check_signature_by_type(&sig_arg.user_address, bytes_to_sign.as_managed_buffer()); + } + + fn serialize_and_hash_proposal( + &self, + action: &Action, + signer: &ManagedAddress, + user_nonce: Nonce, + ) -> ManagedByteArray { + self.serialize_and_hash_action_common(action, signer, user_nonce, ActionType::Propose) + } + + fn serialize_and_hash_action( + &self, + action: &Action, + signer: &ManagedAddress, + user_nonce: Nonce, + ) -> ManagedByteArray { + self.serialize_and_hash_action_common(action, signer, user_nonce, ActionType::SimpleAction) + } + + fn serialize_and_hash_action_common( + &self, + action: &Action, + signer: &ManagedAddress, + user_nonce: Nonce, + action_type: ActionType, + ) -> ManagedByteArray { + let mut all_data = signer.as_managed_buffer().clone(); + + let nonce_encode_result = user_nonce.dep_encode(&mut all_data); + require!(nonce_encode_result.is_ok(), ENCODING_NONCE_ERR_MSG); + + let action_encode_result = action.dep_encode(&mut all_data); + require!( + action_encode_result.is_ok(), + "Error encoding action to buffer" + ); + + let action_type_encode_result = action_type.dep_encode(&mut all_data); + require!( + action_type_encode_result.is_ok(), + ENCODING_ACTION_TYPE_ERR_MSG + ); + + self.crypto().sha256(all_data) + } + + fn serialize_and_hash_group( + &self, + group_id: GroupId, + signer: &ManagedAddress, + user_nonce: Nonce, + ) -> ManagedByteArray { + let mut all_data = signer.as_managed_buffer().clone(); + + let nonce_encode_result = user_nonce.dep_encode(&mut all_data); + require!(nonce_encode_result.is_ok(), ENCODING_NONCE_ERR_MSG); + + let group_encode_result = group_id.dep_encode(&mut all_data); + require!( + group_encode_result.is_ok(), + "Error encoding Group ID to buffer" + ); + + let action_type_encode_result = ActionType::Group.dep_encode(&mut all_data); + require!( + action_type_encode_result.is_ok(), + ENCODING_ACTION_TYPE_ERR_MSG + ); + + self.crypto().sha256(all_data) + } +} diff --git a/contracts/multisig-improved/src/common_functions.rs b/contracts/multisig-improved/src/common_functions.rs new file mode 100644 index 00000000..3176a925 --- /dev/null +++ b/contracts/multisig-improved/src/common_functions.rs @@ -0,0 +1,71 @@ +use crate::common_types::{ + action::{ActionId, Nonce}, + user_role::UserRole, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait CommonFunctionsModule: crate::state::StateModule { + /// Returns `true` (`1`) if `getActionValidSignerCount >= getQuorum`. + #[view(quorumReached)] + fn quorum_reached(&self, action_id: ActionId) -> bool { + let quorum = self.quorum_for_action(action_id).get(); + let valid_signers_count = self.get_action_valid_signer_count(action_id); + valid_signers_count >= quorum + } + + fn get_action_valid_signer_count(&self, action_id: ActionId) -> usize { + let signer_ids = self.action_signer_ids(action_id); + signer_ids + .iter() + .filter(|signer_id| { + let signer_role = self.user_id_to_role(*signer_id).get(); + signer_role.can_sign() + }) + .count() + } + + fn get_action_signers(&self, action_id: ActionId) -> ManagedVec { + let signer_ids = self.action_signer_ids(action_id); + let mut signers = ManagedVec::new(); + for signer_id in signer_ids.iter() { + let opt_user_address = self.user_ids().get_address(signer_id); + let address = unsafe { opt_user_address.unwrap_unchecked() }; + signers.push(address); + } + + signers + } + + fn get_caller_id_and_role(&self) -> (AddressId, UserRole) { + let caller_address = self.blockchain().get_caller(); + self.get_id_and_role(&caller_address) + } + + fn get_id_and_role(&self, user_address: &ManagedAddress) -> (AddressId, UserRole) { + let user_id = self.user_ids().get_id(user_address); + let user_role = self.user_id_to_role(user_id).get(); + + (user_id, user_role) + } + + fn get_and_increment_user_nonce(&self, user_address: &ManagedAddress) -> Nonce { + let user_id = self.user_ids().get_id_non_zero(user_address); + + let mut output_nonce = 0; + self.user_nonce(user_id).update(|user_nonce| { + output_nonce = *user_nonce; + *user_nonce += 1; + }); + + output_nonce + } + + fn require_action_exists(&self, action_id: ActionId) { + require!( + !self.action_mapper().item_is_empty_unchecked(action_id), + "action does not exist" + ); + } +} diff --git a/contracts/multisig-improved/src/common_types/action.rs b/contracts/multisig-improved/src/common_types/action.rs new file mode 100644 index 00000000..8a025abd --- /dev/null +++ b/contracts/multisig-improved/src/common_types/action.rs @@ -0,0 +1,112 @@ +use multiversx_sc_modules::transfer_role_proxy::PaymentsVec; + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +pub type GasLimit = u64; +pub type Nonce = u64; + +pub type ActionId = usize; +pub type GroupId = usize; + +#[derive( + TopEncode, TopDecode, NestedEncode, NestedDecode, TypeAbi, PartialEq, Eq, Clone, Copy, Debug, +)] +pub enum ActionStatus { + Available, + Aborted, +} + +#[derive(NestedEncode, NestedDecode, TypeAbi, Clone)] +pub struct CallActionData { + pub to: ManagedAddress, + pub egld_amount: BigUint, + pub opt_gas_limit: Option, + pub endpoint_name: ManagedBuffer, + pub arguments: ManagedVec>, +} + +#[derive(NestedEncode, NestedDecode, TypeAbi, Clone)] +pub struct EsdtTransferExecuteData { + pub to: ManagedAddress, + pub tokens: PaymentsVec, + pub opt_gas_limit: Option, + pub endpoint_name: ManagedBuffer, + pub arguments: ManagedVec>, +} + +#[derive(NestedEncode, NestedDecode, TypeAbi, Clone)] +pub struct DeployArgs { + pub amount: BigUint, + pub source: ManagedAddress, + pub code_metadata: CodeMetadata, + pub arguments: ManagedVec>, +} + +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, TypeAbi, Clone)] +pub enum Action { + Nothing, + AddBoardMember(ManagedAddress), + AddProposer(ManagedAddress), + RemoveUser(ManagedAddress), + ChangeQuorum(usize), + SendTransferExecuteEgld(CallActionData), + SendTransferExecuteEsdt(EsdtTransferExecuteData), + SendAsyncCall(CallActionData), + SCDeployFromSource(DeployArgs), + SCUpgradeFromSource { + sc_address: ManagedAddress, + args: DeployArgs, + }, + AddModule(ManagedAddress), + RemoveModule(ManagedAddress), +} + +impl Action { + /// Only pending actions are kept in storage, + /// both executed and discarded actions are removed (converted to `Nothing`). + /// So this is equivalent to `action != Action::Nothing`. + pub fn is_pending(&self) -> bool { + !matches!(*self, Action::Nothing) + } + + pub fn is_nothing(&self) -> bool { + matches!(*self, Action::Nothing) + } + + pub fn is_async_call(&self) -> bool { + matches!(*self, Action::SendAsyncCall(_)) + } + + pub fn is_sc_upgrade(&self) -> bool { + matches!( + self, + Action::SCUpgradeFromSource { + sc_address: _, + args: _ + } + ) + } +} + +/// Not used internally, just to retrieve results via endpoint. +#[derive(TopEncode, TypeAbi)] +pub struct ActionFullInfo { + pub action_id: ActionId, + pub group_id: GroupId, + pub action_data: Action, + pub signers: ManagedVec>, +} + +#[cfg(test)] +mod test { + use multiversx_sc_scenario::api::StaticApi; + + use super::Action; + + #[test] + fn test_is_pending() { + assert!(!Action::::Nothing.is_pending()); + assert!(Action::::ChangeQuorum(5).is_pending()); + } +} diff --git a/contracts/multisig-improved/src/common_types/mod.rs b/contracts/multisig-improved/src/common_types/mod.rs new file mode 100644 index 00000000..9a08e948 --- /dev/null +++ b/contracts/multisig-improved/src/common_types/mod.rs @@ -0,0 +1,3 @@ +pub mod action; +pub mod signature; +pub mod user_role; diff --git a/contracts/multisig-improved/src/common_types/signature.rs b/contracts/multisig-improved/src/common_types/signature.rs new file mode 100644 index 00000000..404d6cef --- /dev/null +++ b/contracts/multisig-improved/src/common_types/signature.rs @@ -0,0 +1,82 @@ +use multiversx_sc::api::{CryptoApi, CryptoApiImpl}; + +use super::action::{Action, GroupId, Nonce}; + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[derive(TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode)] +pub struct SignatureArg { + pub user_address: ManagedAddress, + pub nonce: Nonce, + pub action_type: ActionType, + pub signature_type: SignatureType, + pub raw_sig_bytes: ManagedBuffer, +} + +/// Note: Always add new signature types at the end, and NEVER delete any types. +#[derive(TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode)] +pub enum SignatureType { + Ed25519, + Secp256r1, + Secp256k1, +} + +#[derive(TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode, PartialEq, Eq, Clone, Copy)] +pub enum ActionType { + Propose, + SimpleAction, + Group, +} + +#[derive(Clone)] +pub enum ItemToSign<'a, M: ManagedTypeApi> { + Propose(&'a Action), + Action(&'a Action), + Group(GroupId), +} + +impl ActionType { + pub fn require_is_type(&self, action_type: Self) { + if self != &action_type { + M::error_api_impl().signal_error(b"Wrong action type signed"); + } + } +} + +#[derive(TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode)] +pub struct Signature { + pub signature_type: SignatureType, + pub raw_sig_bytes: ManagedBuffer, +} + +impl Signature { + pub fn check_signature_by_type( + &self, + user_address: &ManagedAddress, + bytes_to_sign: &ManagedBuffer, + ) { + if cfg!(debug_assertions) { + return; + } + + match self.signature_type { + SignatureType::Ed25519 => M::crypto_api_impl().verify_ed25519_managed( + user_address.as_managed_buffer().get_handle(), + bytes_to_sign.get_handle(), + self.raw_sig_bytes.get_handle(), + ), + SignatureType::Secp256r1 => todo!(), // not implemented yet + SignatureType::Secp256k1 => { + let verify_result = M::crypto_api_impl().verify_secp256k1_managed( + user_address.as_managed_buffer().get_handle(), + bytes_to_sign.get_handle(), + self.raw_sig_bytes.get_handle(), + ); + if !verify_result { + M::error_api_impl().signal_error(b"Failed checking Secp256k1 signature"); + } + } + } + } +} diff --git a/contracts/multisig-improved/src/common_types/user_role.rs b/contracts/multisig-improved/src/common_types/user_role.rs new file mode 100644 index 00000000..7c907014 --- /dev/null +++ b/contracts/multisig-improved/src/common_types/user_role.rs @@ -0,0 +1,127 @@ +use crate::common_types::action::ActionId; + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[derive(TopEncode, TopDecode, TypeAbi, Clone, Copy, PartialEq, Eq, Debug)] +pub enum UserRole { + None, + Proposer, + BoardMember, +} + +impl UserRole { + pub fn can_propose(&self) -> bool { + matches!(*self, UserRole::BoardMember | UserRole::Proposer) + } + + pub fn can_perform_action(&self) -> bool { + self.can_propose() + } + + pub fn can_discard_action(&self) -> bool { + self.can_propose() + } + + pub fn can_sign(&self) -> bool { + matches!(*self, UserRole::BoardMember) + } + + pub fn has_no_role(&self) -> bool { + matches!(*self, UserRole::None) + } + + pub fn require_can_propose(&self) { + if !self.can_propose() { + M::error_api_impl().signal_error(b"only board members and proposers can propose"); + } + } + + pub fn require_can_sign(&self) { + if !self.can_sign() { + M::error_api_impl().signal_error(b"only board members can sign"); + } + } + + pub fn require_can_unsign(&self) { + if !self.can_sign() { + M::error_api_impl().signal_error(b"only board members can un-sign"); + } + } + + pub fn require_can_perform_action(&self) { + if !self.can_perform_action() { + M::error_api_impl() + .signal_error(b"only board members and proposers can perform actions"); + } + } + + pub fn require_can_discard_action(&self) { + if !self.can_discard_action() { + M::error_api_impl() + .signal_error(b"only board members and proposers can discard actions"); + } + } +} + +fn usize_add_isize(value: &mut usize, delta: isize) { + *value = (*value as isize + delta) as usize; +} + +/// Can be used to: +/// - create new user (board member / proposer) +/// - remove user (board member / proposer) +/// - reactivate removed user +/// - convert between board member and proposer +/// Will keep the board size and proposer count in sync. +pub fn change_user_role( + sc_ref: &Sc, + action_id: ActionId, + user_address: ManagedAddress, + new_role: UserRole, +) { + let user_id = if new_role == UserRole::None { + // avoid creating a new user just to delete it + let user_id = sc_ref.user_ids().get_id(&user_address); + if user_id == 0 { + return; + } + + user_id + } else { + sc_ref.user_ids().get_id_or_insert(&user_address) + }; + + let user_id_to_role_mapper = sc_ref.user_id_to_role(user_id); + let old_role = user_id_to_role_mapper.get(); + user_id_to_role_mapper.set(new_role); + + sc_ref.perform_change_user_event(action_id, &user_address, old_role, new_role); + + // update board size + let mut board_members_delta = 0isize; + if old_role == UserRole::BoardMember { + board_members_delta -= 1; + } + if new_role == UserRole::BoardMember { + board_members_delta += 1; + } + if board_members_delta != 0 { + sc_ref + .num_board_members() + .update(|value| usize_add_isize(value, board_members_delta)); + } + + let mut proposers_delta = 0isize; + if old_role == UserRole::Proposer { + proposers_delta -= 1; + } + if new_role == UserRole::Proposer { + proposers_delta += 1; + } + if proposers_delta != 0 { + sc_ref + .num_proposers() + .update(|value| usize_add_isize(value, proposers_delta)); + } +} diff --git a/contracts/multisig-improved/src/external/events.rs b/contracts/multisig-improved/src/external/events.rs new file mode 100644 index 00000000..0fdf1fc9 --- /dev/null +++ b/contracts/multisig-improved/src/external/events.rs @@ -0,0 +1,101 @@ +use multiversx_sc_modules::transfer_role_proxy::PaymentsVec; + +use crate::{ + common_types::action::{ActionFullInfo, ActionId, GasLimit}, + common_types::user_role::UserRole, +}; + +multiversx_sc::imports!(); + +/// Contains all events that can be emitted by the contract. +#[multiversx_sc::module] +pub trait EventsModule { + #[event("startPerformAction")] + fn start_perform_action_event(&self, data: &ActionFullInfo); + + #[event("performChangeUser")] + fn perform_change_user_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] changed_user: &ManagedAddress, + #[indexed] old_role: UserRole, + #[indexed] new_role: UserRole, + ); + + #[event("performChangeQuorum")] + fn perform_change_quorum_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] new_quorum: usize, + ); + + #[event("performAddModuleEvent")] + fn perform_add_module_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] sc_address: &ManagedAddress, + ); + + #[event("performRemoveModuleEvent")] + fn perform_remove_module_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] sc_address: &ManagedAddress, + ); + + #[event("performAsyncCall")] + fn perform_async_call_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] to: &ManagedAddress, + #[indexed] egld_value: &BigUint, + #[indexed] gas: GasLimit, + #[indexed] endpoint: &ManagedBuffer, + #[indexed] arguments: &MultiValueManagedVec, + ); + + #[event("performTransferExecuteEgld")] + fn perform_transfer_execute_egld_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] to: &ManagedAddress, + #[indexed] egld_value: &BigUint, + #[indexed] gas: GasLimit, + #[indexed] endpoint: &ManagedBuffer, + #[indexed] arguments: &MultiValueManagedVec, + ); + + #[event("performTransferExecuteEsdt")] + fn perform_transfer_execute_esdt_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] to: &ManagedAddress, + #[indexed] tokens: &PaymentsVec, + #[indexed] gas: GasLimit, + #[indexed] endpoint: &ManagedBuffer, + #[indexed] arguments: &MultiValueManagedVec, + ); + + #[event("performDeployFromSource")] + fn perform_deploy_from_source_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] egld_value: &BigUint, + #[indexed] source_address: &ManagedAddress, + #[indexed] code_metadata: CodeMetadata, + #[indexed] gas: GasLimit, + #[indexed] arguments: &MultiValueManagedVec, + ); + + #[event("performUpgradeFromSource")] + fn perform_upgrade_from_source_event( + &self, + #[indexed] action_id: ActionId, + #[indexed] target_address: &ManagedAddress, + #[indexed] egld_value: &BigUint, + #[indexed] source_address: &ManagedAddress, + #[indexed] code_metadata: CodeMetadata, + #[indexed] gas: GasLimit, + #[indexed] arguments: &MultiValueManagedVec, + ); +} diff --git a/contracts/multisig-improved/src/external/mod.rs b/contracts/multisig-improved/src/external/mod.rs new file mode 100644 index 00000000..b3383e10 --- /dev/null +++ b/contracts/multisig-improved/src/external/mod.rs @@ -0,0 +1,2 @@ +pub mod events; +pub mod views; diff --git a/contracts/multisig-improved/src/external/views.rs b/contracts/multisig-improved/src/external/views.rs new file mode 100644 index 00000000..9cc05b81 --- /dev/null +++ b/contracts/multisig-improved/src/external/views.rs @@ -0,0 +1,167 @@ +use crate::common_types::{ + action::{Action, ActionFullInfo, ActionId, Nonce}, + user_role::UserRole, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait ViewsModule: + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + crate::action_types::external_module::ExternalModuleModule + + crate::action_types::propose::ProposeModule + + crate::action_types::sign::SignModule + + crate::action_types::perform::PerformModule + + crate::action_types::execute_action::ExecuteActionModule + + crate::ms_endpoints::callbacks::CallbacksModule + + crate::check_signature::CheckSignatureModule + + super::events::EventsModule +{ + /// Iterates through all actions and retrieves those that are still pending. + /// Serialized full action data: + /// - the action id + /// - the serialized action data + /// - (number of signers followed by) list of signer addresses. + #[label("multisig-external-view")] + #[allow_multiple_var_args] + #[view(getPendingActionFullInfo)] + fn get_pending_action_full_info( + &self, + opt_range: OptionalValue<(usize, usize)>, + ) -> MultiValueEncoded> { + let mut result = MultiValueEncoded::new(); + let action_last_index = self.get_action_last_index(); + let action_mapper = self.action_mapper(); + let mut index_of_first_action = 1; + let mut index_of_last_action = action_last_index; + if let OptionalValue::Some((count, first_action_id)) = opt_range { + require!( + first_action_id <= action_last_index, + "first_action_id needs to be within the range of the available action ids" + ); + index_of_first_action = first_action_id; + + require!( + index_of_first_action + count <= action_last_index, + "cannot exceed the total number of actions" + ); + index_of_last_action = index_of_first_action + count; + } + for action_id in index_of_first_action..=index_of_last_action { + let action_data = action_mapper.get(action_id); + if action_data.is_pending() { + result.push(ActionFullInfo { + action_id, + action_data, + signers: self.get_action_signers(action_id), + group_id: self.group_for_action(action_id).get(), + }); + } + } + result + } + + /// Gets addresses of all users who signed an action and are still board members. + /// All these signatures are currently valid. + #[label("multisig-external-view")] + #[view(getActionSignerCount)] + fn get_action_signer_count(&self, action_id: ActionId) -> usize { + self.action_signer_ids(action_id).len() + } + + /// It is possible for board members to lose their role. + /// They are not automatically removed from all actions when doing so, + /// therefore the contract needs to re-check every time when actions are performed. + /// This function is used to validate the signers before performing an action. + /// It also makes it easy to check before performing an action. + #[label("multisig-external-view")] + #[view(getActionValidSignerCount)] + fn get_action_valid_signer_count_view(&self, action_id: ActionId) -> usize { + self.get_action_valid_signer_count(action_id) + } + + /// Gets addresses of all users who signed an action. + /// Does not check if those users are still board members or not, + /// so the result may contain invalid signers. + #[label("multisig-external-view")] + #[view(getActionSigners)] + fn get_action_signers_view(&self, action_id: ActionId) -> ManagedVec { + self.get_action_signers(action_id) + } + + /// Indicates user rights. + /// `0` = no rights, + /// `1` = can propose, but not sign, + /// `2` = can propose and sign. + #[label("multisig-external-view")] + #[view(userRole)] + fn user_role(&self, user: ManagedAddress) -> UserRole { + let user_id = self.user_ids().get_id(&user); + if user_id == 0 { + return UserRole::None; + } + + self.user_id_to_role(user_id).get() + } + + /// Lists all users that can sign actions. + #[label("multisig-external-view")] + #[view(getAllBoardMembers)] + fn get_all_board_members(&self) -> MultiValueEncoded { + self.get_all_users_with_role(UserRole::BoardMember) + } + + /// Lists all proposers that are not board members. + #[label("multisig-external-view")] + #[view(getAllProposers)] + fn get_all_proposers(&self) -> MultiValueEncoded { + self.get_all_users_with_role(UserRole::Proposer) + } + + /// Serialized action data of an action with index. + #[label("multisig-external-view")] + #[view(getActionData)] + fn get_action_data(&self, action_id: ActionId) -> Action { + self.action_mapper().get(action_id) + } + + /// Returns `true` (`1`) if the user has signed the action. + /// Does not check whether or not the user is still a board member and the signature valid. + #[view] + fn signed(&self, user: ManagedAddress, action_id: ActionId) -> bool { + let user_id = self.user_ids().get_id(&user); + if user_id != 0 { + self.action_signer_ids(action_id).contains(&user_id) + } else { + false + } + } + + /// The index of the last proposed action. + /// 0 means that no action was ever proposed yet. + #[view(getActionLastIndex)] + fn get_action_last_index(&self) -> ActionId { + self.action_mapper().len() + } + + #[view(getUserNonce)] + fn get_user_nonce(&self, user_address: ManagedAddress) -> Nonce { + let user_id = self.user_ids().get_id_non_zero(&user_address); + self.user_nonce(user_id).get() + } + + fn get_all_users_with_role(&self, role: UserRole) -> MultiValueEncoded { + let mut result = MultiValueEncoded::new(); + let num_users = self.user_ids().get_last_id(); + for user_id in 1..=num_users { + if self.user_id_to_role(user_id).get() == role { + if let Some(address) = self.user_ids().get_address(user_id) { + result.push(address); + } + } + } + + result + } +} diff --git a/contracts/multisig-improved/src/ms_endpoints/callbacks.rs b/contracts/multisig-improved/src/ms_endpoints/callbacks.rs new file mode 100644 index 00000000..128a4b8e --- /dev/null +++ b/contracts/multisig-improved/src/ms_endpoints/callbacks.rs @@ -0,0 +1,26 @@ +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait CallbacksModule { + /// Callback only performs logging. + #[callback] + fn perform_async_call_callback( + &self, + #[call_result] call_result: ManagedAsyncCallResult>, + ) { + match call_result { + ManagedAsyncCallResult::Ok(results) => { + self.async_call_success(results); + } + ManagedAsyncCallResult::Err(err) => { + self.async_call_error(err.err_code, err.err_msg); + } + } + } + + #[event("asyncCallSuccess")] + fn async_call_success(&self, #[indexed] results: MultiValueEncoded); + + #[event("asyncCallError")] + fn async_call_error(&self, #[indexed] err_code: u32, #[indexed] err_message: ManagedBuffer); +} diff --git a/contracts/multisig-improved/src/ms_endpoints/discard.rs b/contracts/multisig-improved/src/ms_endpoints/discard.rs new file mode 100644 index 00000000..ddf59e3f --- /dev/null +++ b/contracts/multisig-improved/src/ms_endpoints/discard.rs @@ -0,0 +1,40 @@ +use crate::common_types::action::ActionId; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait DiscardEndpointsModule: + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + crate::action_types::external_module::ExternalModuleModule + + crate::action_types::propose::ProposeModule + + crate::action_types::sign::SignModule + + crate::action_types::perform::PerformModule + + crate::action_types::execute_action::ExecuteActionModule + + crate::action_types::discard::DiscardActionModule + + super::callbacks::CallbacksModule + + crate::check_signature::CheckSignatureModule + + crate::external::events::EventsModule +{ + /// Clears storage pertaining to an action that is no longer supposed to be executed. + /// Any signatures that the action received must first be removed, via `unsign`. + /// Otherwise this endpoint would be prone to abuse. + #[endpoint(discardAction)] + fn discard_action_endpoint(&self, action_id: ActionId) { + let (_, caller_role) = self.get_caller_id_and_role(); + caller_role.require_can_discard_action::(); + + self.discard_action(action_id); + } + + /// Discard all the actions with the given IDs + #[endpoint(discardBatch)] + fn discard_batch(&self, action_ids: MultiValueEncoded) { + let (_, caller_role) = self.get_caller_id_and_role(); + caller_role.require_can_discard_action::(); + + for action_id in action_ids { + self.discard_action(action_id); + } + } +} diff --git a/contracts/multisig-improved/src/ms_endpoints/mod.rs b/contracts/multisig-improved/src/ms_endpoints/mod.rs new file mode 100644 index 00000000..9bb85ec9 --- /dev/null +++ b/contracts/multisig-improved/src/ms_endpoints/mod.rs @@ -0,0 +1,5 @@ +pub mod callbacks; +pub mod discard; +pub mod perform; +pub mod propose; +pub mod sign; diff --git a/contracts/multisig-improved/src/ms_endpoints/perform.rs b/contracts/multisig-improved/src/ms_endpoints/perform.rs new file mode 100644 index 00000000..6752940b --- /dev/null +++ b/contracts/multisig-improved/src/ms_endpoints/perform.rs @@ -0,0 +1,61 @@ +use crate::common_types::action::{ActionId, ActionStatus, GroupId}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait PerformEndpointsModule: + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + crate::action_types::external_module::ExternalModuleModule + + crate::external::events::EventsModule + + crate::action_types::perform::PerformModule + + crate::action_types::execute_action::ExecuteActionModule + + super::callbacks::CallbacksModule +{ + /// Proposers and board members use this to launch signed actions. + #[endpoint(performAction)] + fn perform_action_endpoint(&self, action_id: ActionId) -> OptionalValue { + let (_, caller_role) = self.get_caller_id_and_role(); + caller_role.require_can_perform_action::(); + + require!( + self.quorum_reached(action_id), + "quorum has not been reached" + ); + + let group_id = self.group_for_action(action_id).get(); + require!(group_id == 0, "May not execute this action by itself"); + + self.perform_action_by_id(action_id) + } + + /// Perform all the actions in the given batch + #[endpoint(performBatch)] + fn perform_batch(&self, group_id: GroupId) { + let (_, caller_role) = self.get_caller_id_and_role(); + caller_role.require_can_perform_action::(); + + let group_status = self.action_group_status(group_id).get(); + require!( + group_status == ActionStatus::Available, + "cannot perform actions of an aborted batch" + ); + + let mapper = self.action_groups(group_id); + require!(!mapper.is_empty(), "Invalid group ID"); + + let mut action_ids = ManagedVec::::new(); + for action_id in mapper.iter() { + action_ids.push(action_id); + } + + for action_id in &action_ids { + require!( + self.quorum_reached(action_id), + "quorum has not been reached" + ); + + let _ = self.perform_action_by_id(action_id); + } + } +} diff --git a/contracts/multisig-improved/src/ms_endpoints/propose.rs b/contracts/multisig-improved/src/ms_endpoints/propose.rs new file mode 100644 index 00000000..fa3961ad --- /dev/null +++ b/contracts/multisig-improved/src/ms_endpoints/propose.rs @@ -0,0 +1,278 @@ +use multiversx_sc_modules::transfer_role_proxy::PaymentsVec; + +use crate::common_types::{ + action::{ + Action, ActionId, ActionStatus, CallActionData, DeployArgs, EsdtTransferExecuteData, + GasLimit, GroupId, + }, + signature::SignatureArg, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait ProposeEndpointsModule: + crate::check_signature::CheckSignatureModule + + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + crate::action_types::external_module::ExternalModuleModule + + crate::action_types::propose::ProposeModule + + crate::action_types::execute_action::ExecuteActionModule + + crate::action_types::perform::PerformModule + + crate::ms_endpoints::callbacks::CallbacksModule + + crate::external::events::EventsModule +{ + /// Initiates board member addition process. + /// Can also be used to promote a proposer to board member. + #[endpoint(proposeAddBoardMember)] + fn propose_add_board_member( + &self, + board_member_address: ManagedAddress, + opt_signature: OptionalValue>, + ) -> ActionId { + self.propose_action(Action::AddBoardMember(board_member_address), opt_signature) + } + + /// Initiates proposer addition process.. + /// Can also be used to demote a board member to proposer. + #[endpoint(proposeAddProposer)] + fn propose_add_proposer( + &self, + proposer_address: ManagedAddress, + opt_signature: OptionalValue>, + ) -> ActionId { + self.propose_action(Action::AddProposer(proposer_address), opt_signature) + } + + /// Removes user regardless of whether it is a board member or proposer. + #[endpoint(proposeRemoveUser)] + fn propose_remove_user( + &self, + user_address: ManagedAddress, + opt_signature: OptionalValue>, + ) -> ActionId { + self.propose_action(Action::RemoveUser(user_address), opt_signature) + } + + #[endpoint(proposeChangeQuorum)] + fn propose_change_quorum( + &self, + new_quorum: usize, + opt_signature: OptionalValue>, + ) -> ActionId { + self.propose_action(Action::ChangeQuorum(new_quorum), opt_signature) + } + + /// Propose a transaction in which the contract will perform a transfer-execute call. + /// Can send EGLD without calling anything. + /// Can call smart contract endpoints directly. + /// Doesn't really work with builtin functions. + #[allow_multiple_var_args] + #[endpoint(proposeTransferExecute)] + fn propose_transfer_execute( + &self, + to: ManagedAddress, + egld_amount: BigUint, + opt_gas_limit: Option, + function_call: FunctionCall, + opt_signature: OptionalValue>, + ) -> OptionalValue { + require!( + egld_amount > 0 || !function_call.is_empty(), + "proposed action has no effect" + ); + + let proposer = self.get_proposer_no_sig_check(&opt_signature); + let call_data = CallActionData { + to, + egld_amount, + opt_gas_limit, + endpoint_name: function_call.function_name, + arguments: function_call.arg_buffer.into_vec_of_buffers(), + }; + let action_id = self.propose_action( + Action::SendTransferExecuteEgld(call_data.clone()), + opt_signature, + ); + + if self.try_perform_egld_action_directly(&proposer, action_id, &call_data) { + return OptionalValue::None; + } + + OptionalValue::Some(action_id) + } + + #[allow_multiple_var_args] + #[endpoint(proposeTransferExecuteEsdt)] + fn propose_transfer_execute_esdt( + &self, + to: ManagedAddress, + tokens: PaymentsVec, + opt_gas_limit: Option, + function_call: FunctionCall, + opt_signature: OptionalValue>, + ) -> OptionalValue { + require!(!tokens.is_empty(), "No tokens to transfer"); + + let proposer = self.get_proposer_no_sig_check(&opt_signature); + let call_data = EsdtTransferExecuteData { + to, + tokens, + opt_gas_limit, + endpoint_name: function_call.function_name, + arguments: function_call.arg_buffer.into_vec_of_buffers(), + }; + let action_id = self.propose_action( + Action::SendTransferExecuteEsdt(call_data.clone()), + opt_signature, + ); + + if self.try_perform_esdt_action_directly(&proposer, action_id, &call_data) { + return OptionalValue::None; + } + + OptionalValue::Some(action_id) + } + + /// Propose a transaction in which the contract will perform an async call call. + /// Can call smart contract endpoints directly. + /// Can use ESDTTransfer/ESDTNFTTransfer/MultiESDTTransfer to send tokens, while also optionally calling endpoints. + /// Works well with builtin functions. + /// Cannot simply send EGLD directly without calling anything. + #[allow_multiple_var_args] + #[endpoint(proposeAsyncCall)] + fn propose_async_call( + &self, + to: ManagedAddress, + egld_amount: BigUint, + opt_gas_limit: Option, + function_call: FunctionCall, + opt_signature: OptionalValue>, + ) -> ActionId { + require!( + egld_amount > 0 || !function_call.is_empty(), + "proposed action has no effect" + ); + + let call_data = CallActionData { + to, + egld_amount, + opt_gas_limit, + endpoint_name: function_call.function_name, + arguments: function_call.arg_buffer.into_vec_of_buffers(), + }; + + self.propose_action(Action::SendAsyncCall(call_data), opt_signature) + } + + #[allow_multiple_var_args] + #[endpoint(proposeSCDeployFromSource)] + fn propose_sc_deploy_from_source( + &self, + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + opt_signature: Option>, + arguments: MultiValueEncoded, + ) -> ActionId { + self.propose_action( + Action::SCDeployFromSource(DeployArgs { + amount, + source, + code_metadata, + arguments: arguments.into_vec_of_buffers(), + }), + opt_signature.into(), + ) + } + + #[allow_multiple_var_args] + #[endpoint(proposeSCUpgradeFromSource)] + fn propose_sc_upgrade_from_source( + &self, + sc_address: ManagedAddress, + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + opt_signature: Option>, + arguments: MultiValueEncoded, + ) -> ActionId { + self.propose_action( + Action::SCUpgradeFromSource { + sc_address, + args: DeployArgs { + amount, + source, + code_metadata, + arguments: arguments.into_vec_of_buffers(), + }, + }, + opt_signature.into(), + ) + } + + #[endpoint(proposeAddModule)] + fn propose_add_module( + &self, + sc_address: ManagedAddress, + opt_signature: OptionalValue>, + ) -> ActionId { + require!( + self.blockchain().is_smart_contract(&sc_address), + "Invalid SC address" + ); + self.require_same_shard(&sc_address); + + let existing_id = self.module_id().get_id(&sc_address); + require!(existing_id == NULL_ID, "Module already known"); + + self.propose_action(Action::AddModule(sc_address), opt_signature) + } + + #[endpoint(proposeRemoveModule)] + fn propose_remove_module( + &self, + sc_address: ManagedAddress, + opt_signature: OptionalValue>, + ) -> ActionId { + let _ = self.module_id().get_id_non_zero(&sc_address); + + self.propose_action(Action::RemoveModule(sc_address), opt_signature) + } + + #[endpoint(proposeBatch)] + fn propose_batch(&self, actions: MultiValueEncoded>) -> GroupId { + let group_id = self.last_action_group_id().get() + 1; + require!(!actions.is_empty(), "No actions"); + + let (caller_id, caller_role) = self.get_caller_id_and_role(); + caller_role.require_can_propose::(); + + let mut action_mapper = self.action_mapper(); + let mut action_groups_mapper = self.action_groups(group_id); + self.action_group_status(group_id) + .set(ActionStatus::Available); + + require!( + action_groups_mapper.is_empty(), + "cannot add actions to an already existing batch" + ); + + for action in actions { + self.require_valid_action_type(&action); + self.ensure_valid_transfer_action(&action); + + let action_id = action_mapper.push(&action); + if caller_role.can_sign() { + let _ = self.action_signer_ids(action_id).insert(caller_id); + } + + let _ = action_groups_mapper.insert(action_id); + self.group_for_action(action_id).set(group_id); + } + + self.last_action_group_id().set(group_id); + + group_id + } +} diff --git a/contracts/multisig-improved/src/ms_endpoints/sign.rs b/contracts/multisig-improved/src/ms_endpoints/sign.rs new file mode 100644 index 00000000..da5409a3 --- /dev/null +++ b/contracts/multisig-improved/src/ms_endpoints/sign.rs @@ -0,0 +1,154 @@ +use crate::common_types::{ + action::{ActionId, ActionStatus, GroupId}, + signature::SignatureArg, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait SignEndpointsModule: + crate::common_functions::CommonFunctionsModule + + crate::state::StateModule + + crate::action_types::external_module::ExternalModuleModule + + crate::action_types::propose::ProposeModule + + crate::action_types::perform::PerformModule + + crate::action_types::execute_action::ExecuteActionModule + + crate::action_types::sign::SignModule + + super::callbacks::CallbacksModule + + crate::external::events::EventsModule + + crate::check_signature::CheckSignatureModule +{ + /// Used by board members to sign actions. + #[endpoint] + fn sign(&self, action_id: ActionId, signatures: MultiValueEncoded>) { + self.require_action_exists(action_id); + + let group_id = self.group_for_action(action_id).get(); + if group_id != 0 { + let group_status = self.action_group_status(group_id).get(); + require!( + group_status == ActionStatus::Available, + "cannot sign actions of an aborted batch" + ); + } + + let user_ids = self.check_single_action_signatures(action_id, signatures); + self.add_signatures(action_id, &user_ids); + } + + /// Sign all the actions in the given batch + /// Signatures must be given in order of the action IDs inside batch, even if it was already signed + #[endpoint(signBatch)] + fn sign_batch( + &self, + group_id: GroupId, + signatures: MultiValueEncoded>, + ) { + let group_status = self.action_group_status(group_id).get(); + require!( + group_status == ActionStatus::Available, + "cannot sign actions of an aborted batch" + ); + + let mapper = self.action_groups(group_id); + require!(!mapper.is_empty(), "Invalid group ID"); + + let user_ids = self.check_group_signatures(group_id, signatures); + for action_id in mapper.iter() { + self.require_action_exists(action_id); + + self.add_signatures(action_id, &user_ids); + } + } + + #[endpoint(signAndPerform)] + fn sign_and_perform( + &self, + action_id: ActionId, + signatures: MultiValueEncoded>, + ) -> OptionalValue { + self.sign(action_id, signatures); + self.try_perform_action(action_id) + } + + #[endpoint(signBatchAndPerform)] + fn sign_batch_and_perform( + &self, + group_id: GroupId, + signatures: MultiValueEncoded>, + ) { + self.sign_batch(group_id, signatures); + + let (_, caller_role) = self.get_caller_id_and_role(); + require!( + caller_role.can_perform_action(), + "only board members and proposers can perform actions" + ); + + // Copy action_ids before executing them since perform_action does a swap_remove + // clearing the last item + let mut action_ids = ManagedVec::::new(); + for action_id in self.action_groups(group_id).iter() { + require!( + self.quorum_reached(action_id), + "Quorum not reached for action" + ); + + action_ids.push(action_id); + } + + for action_id in &action_ids { + let _ = self.perform_action_by_id(action_id); + } + } + + /// Board members can withdraw their signatures if they no longer desire for the action to be executed. + /// Actions that are left with no valid signatures can be then deleted to free up storage. + #[endpoint] + fn unsign(&self, action_id: ActionId) { + let (caller_id, caller_role) = self.get_caller_id_and_role(); + caller_role.require_can_unsign::(); + + self.unsign_action(action_id, caller_id); + } + + /// Unsign all actions with the given IDs + #[endpoint(unsignBatch)] + fn unsign_batch(&self, group_id: GroupId) { + let (caller_id, caller_role) = self.get_caller_id_and_role(); + caller_role.require_can_unsign::(); + + let mapper = self.action_groups(group_id); + require!(!mapper.is_empty(), "Invalid group ID"); + + for action_id in mapper.iter() { + self.unsign_action(action_id, caller_id); + } + } + + #[endpoint(unsignForOutdatedBoardMembers)] + fn unsign_for_outdated_board_members( + &self, + action_id: ActionId, + outdated_board_members: MultiValueEncoded, + ) { + let mut board_members_to_remove = ManagedVec::::new(); + if outdated_board_members.is_empty() { + for signer_id in self.action_signer_ids(action_id).iter() { + if !self.user_id_to_role(signer_id).get().can_sign() { + board_members_to_remove.push(signer_id); + } + } + } else { + for signer_id in outdated_board_members.into_iter() { + if !self.user_id_to_role(signer_id).get().can_sign() { + board_members_to_remove.push(signer_id); + } + } + } + + for member in board_members_to_remove.iter() { + self.action_signer_ids(action_id).swap_remove(&member); + } + } +} diff --git a/contracts/multisig-improved/src/multisig_improved.rs b/contracts/multisig-improved/src/multisig_improved.rs new file mode 100644 index 00000000..c98d28c3 --- /dev/null +++ b/contracts/multisig-improved/src/multisig_improved.rs @@ -0,0 +1,80 @@ +#![no_std] + +use action_types::execute_action::{BOARD_SIZE_TOO_BIG_ERR_MSG, MAX_BOARD_MEMBERS}; +use common_types::user_role::UserRole; + +pub mod action_types; +pub mod check_signature; +pub mod common_functions; +pub mod common_types; +pub mod external; +pub mod ms_endpoints; +pub mod state; + +multiversx_sc::imports!(); + +/// Multi-signature smart contract implementation. +/// Acts like a wallet that needs multiple signers for any action performed. +/// See the readme file for more detailed documentation. +#[multiversx_sc::contract] +pub trait Multisig: + state::StateModule + + common_functions::CommonFunctionsModule + + check_signature::CheckSignatureModule + + ms_endpoints::propose::ProposeEndpointsModule + + ms_endpoints::perform::PerformEndpointsModule + + ms_endpoints::discard::DiscardEndpointsModule + + ms_endpoints::sign::SignEndpointsModule + + ms_endpoints::callbacks::CallbacksModule + + action_types::external_module::ExternalModuleModule + + action_types::execute_action::ExecuteActionModule + + action_types::propose::ProposeModule + + action_types::sign::SignModule + + action_types::perform::PerformModule + + action_types::discard::DiscardActionModule + + external::events::EventsModule + + external::views::ViewsModule + + multiversx_sc_modules::dns::DnsModule +{ + #[init] + fn init(&self, quorum: usize, board: MultiValueEncoded) { + let board_vec = board.to_vec(); + let new_num_board_members = self.add_initial_board_members(board_vec); + require!( + new_num_board_members > 0, + "board cannot be empty on init, no-one would be able to propose" + ); + + require!( + quorum <= new_num_board_members, + "quorum cannot exceed board size" + ); + self.quorum().set(quorum); + } + + #[upgrade] + fn upgrade(&self) {} + + /// Allows the contract to receive funds even if it is marked as unpayable in the protocol. + #[payable("*")] + #[endpoint] + fn deposit(&self) {} + + fn add_initial_board_members(&self, new_board_members: ManagedVec) -> usize { + let new_board_members_len = new_board_members.len(); + require!( + new_board_members_len <= MAX_BOARD_MEMBERS, + BOARD_SIZE_TOO_BIG_ERR_MSG + ); + + let mapper = self.user_ids(); + for new_member in &new_board_members { + let user_id = mapper.insert_new(&new_member); + self.user_id_to_role(user_id).set(UserRole::BoardMember); + } + + self.num_board_members().set(new_board_members_len); + + new_board_members_len + } +} diff --git a/contracts/multisig-improved/src/state.rs b/contracts/multisig-improved/src/state.rs new file mode 100644 index 00000000..6ebab763 --- /dev/null +++ b/contracts/multisig-improved/src/state.rs @@ -0,0 +1,62 @@ +use crate::common_types::action::{ActionId, ActionStatus, GroupId, Nonce}; +use crate::common_types::{action::Action, user_role::UserRole}; + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[multiversx_sc::module] +pub trait StateModule { + /// Minimum number of signatures needed to perform any action. + #[view(getQuorum)] + #[storage_mapper("quorum_ids")] + fn quorum(&self) -> SingleValueMapper; + + #[storage_mapper("user_ids")] + fn user_ids(&self) -> AddressToIdMapper; + + #[storage_mapper("userNonce")] + fn user_nonce(&self, user_id: AddressId) -> SingleValueMapper; + + #[storage_mapper("quorum_for_action")] + fn quorum_for_action(&self, action_id: ActionId) -> SingleValueMapper; + + #[storage_mapper("user_role")] + fn user_id_to_role(&self, user_id: AddressId) -> SingleValueMapper; + + /// Denormalized board member count. + /// It is kept in sync with the user list by the contract. + #[view(getNumBoardMembers)] + #[storage_mapper("num_board_members")] + fn num_board_members(&self) -> SingleValueMapper; + + #[view(getNumGroups)] + #[storage_mapper("num_groups")] + fn num_groups(&self) -> SingleValueMapper; + + /// Denormalized proposer count. + /// It is kept in sync with the user list by the contract. + #[view(getNumProposers)] + #[storage_mapper("num_proposers")] + fn num_proposers(&self) -> SingleValueMapper; + + #[storage_mapper("action_data")] + fn action_mapper(&self) -> VecMapper>; + + #[view(getActionGroup)] + #[storage_mapper("action_groups")] + fn action_groups(&self, group_id: GroupId) -> UnorderedSetMapper; + + #[view(getLastGroupActionId)] + #[storage_mapper("last_action_group_id")] + fn last_action_group_id(&self) -> SingleValueMapper; + + #[view(getActionGroup)] + #[storage_mapper("action_group_status")] + fn action_group_status(&self, group_id: GroupId) -> SingleValueMapper; + + #[storage_mapper("group_for_action")] + fn group_for_action(&self, action_id: ActionId) -> SingleValueMapper; + + #[storage_mapper("action_signer_ids")] + fn action_signer_ids(&self, action_id: ActionId) -> UnorderedSetMapper; +} diff --git a/contracts/multisig-improved/tests/ms_improved_setup/can_execute_mock.rs b/contracts/multisig-improved/tests/ms_improved_setup/can_execute_mock.rs new file mode 100644 index 00000000..af514d34 --- /dev/null +++ b/contracts/multisig-improved/tests/ms_improved_setup/can_execute_mock.rs @@ -0,0 +1,29 @@ +use multiversx_sc::contract_base::{CallableContract, ContractBase}; +use multiversx_sc_scenario::DebugApi; + +static CAN_EXECUTE_FN_NAME: &str = "canExecute"; + +#[derive(Clone, Default)] +pub struct CanExecuteMock {} + +impl ContractBase for CanExecuteMock { + type Api = DebugApi; +} + +impl CallableContract for CanExecuteMock { + fn call(&self, fn_name: &str) -> bool { + if fn_name == CAN_EXECUTE_FN_NAME { + multiversx_sc::io::finish_multi::(&true); + + return true; + } + + false + } +} + +impl CanExecuteMock { + pub fn new() -> Self { + CanExecuteMock {} + } +} diff --git a/contracts/multisig-improved/tests/ms_improved_setup/mod.rs b/contracts/multisig-improved/tests/ms_improved_setup/mod.rs new file mode 100644 index 00000000..45548771 --- /dev/null +++ b/contracts/multisig-improved/tests/ms_improved_setup/mod.rs @@ -0,0 +1,71 @@ +use adder::Adder; +use multisig_improved::Multisig; +use multiversx_sc::types::{Address, MultiValueEncoded}; +use multiversx_sc_scenario::{ + imports::{BlockchainStateWrapper, ContractObjWrapper}, + managed_address, managed_biguint, rust_biguint, DebugApi, +}; + +pub mod can_execute_mock; + +pub struct MsImprovedSetup +where + MsImprovedBuilder: 'static + Copy + Fn() -> multisig_improved::ContractObj, + AdderBuilder: 'static + Copy + Fn() -> adder::ContractObj, +{ + pub b_mock: BlockchainStateWrapper, + pub first_board_member: Address, + pub second_board_member: Address, + pub ms_owner: Address, + pub ms_wrapper: ContractObjWrapper, MsImprovedBuilder>, + pub adder_wrapper: ContractObjWrapper, AdderBuilder>, +} + +impl MsImprovedSetup +where + MsImprovedBuilder: 'static + Copy + Fn() -> multisig_improved::ContractObj, + AdderBuilder: 'static + Copy + Fn() -> adder::ContractObj, +{ + pub fn new(ms_builder: MsImprovedBuilder, adder_builder: AdderBuilder) -> Self { + let rust_zero = rust_biguint!(0u64); + let mut b_mock = BlockchainStateWrapper::new(); + let first_board_member = b_mock.create_user_account(&rust_zero); + let second_board_member = b_mock.create_user_account(&rust_zero); + let ms_owner = b_mock.create_user_account(&rust_zero); + let adder_wrapper = b_mock.create_sc_account( + &rust_zero, + Some(&first_board_member), + adder_builder, + "adder", + ); + let ms_wrapper = + b_mock.create_sc_account(&rust_zero, Some(&ms_owner), ms_builder, "multisig"); + + // init adder + b_mock + .execute_tx(&first_board_member, &adder_wrapper, &rust_zero, |sc| { + sc.init(managed_biguint!(0)); + }) + .assert_ok(); + + // init multisig + b_mock + .execute_tx(&ms_owner, &ms_wrapper, &rust_zero, |sc| { + let mut board = MultiValueEncoded::new(); + board.push(managed_address!(&first_board_member)); + board.push(managed_address!(&second_board_member)); + + sc.init(2, board); + }) + .assert_ok(); + + Self { + b_mock, + first_board_member, + second_board_member, + ms_owner, + ms_wrapper, + adder_wrapper, + } + } +} diff --git a/contracts/multisig-improved/tests/ms_improved_tests.rs b/contracts/multisig-improved/tests/ms_improved_tests.rs new file mode 100644 index 00000000..542a5377 --- /dev/null +++ b/contracts/multisig-improved/tests/ms_improved_tests.rs @@ -0,0 +1,213 @@ +pub mod ms_improved_setup; + +use adder::Adder; +use can_execute_mock::CanExecuteMock; +use ms_improved_setup::*; +use multisig_improved::{ + common_types::signature::{ActionType, SignatureArg, SignatureType}, + ms_endpoints::{ + perform::PerformEndpointsModule, propose::ProposeEndpointsModule, sign::SignEndpointsModule, + }, +}; +use multiversx_sc::{ + imports::OptionalValue, + types::{FunctionCall, ManagedArgBuffer, MultiValueEncoded}, +}; +use multiversx_sc_scenario::{ + managed_address, managed_biguint, managed_buffer, rust_biguint, DebugApi, +}; + +#[test] +fn init_test() { + let _ = MsImprovedSetup::new(multisig_improved::contract_obj, adder::contract_obj); +} + +#[test] +fn add_can_execute_module_test() { + let mut ms_setup = MsImprovedSetup::new(multisig_improved::contract_obj, adder::contract_obj); + let can_execute_mock = ms_setup.b_mock.create_sc_account( + &rust_biguint!(0), + Some(&ms_setup.ms_owner), + CanExecuteMock::new, + "canExecute mock", + ); + + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + sc.propose_add_module( + managed_address!(can_execute_mock.address_ref()), + OptionalValue::None, + ); + }, + ) + .assert_ok(); + + // try execute action without enough signatures + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + let _ = sc.perform_action_endpoint(1); + }, + ) + .assert_user_error("quorum has not been reached"); + + // other user sign + let other_board_member = ms_setup.second_board_member.clone(); + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + let mut signatures = MultiValueEncoded::new(); + signatures.push(SignatureArg { + user_address: managed_address!(&other_board_member), + nonce: 0, + action_type: ActionType::SimpleAction, + signature_type: SignatureType::Ed25519, // unused + raw_sig_bytes: managed_buffer!(b"signature"), + }); + + sc.sign(1, signatures) + }, + ) + .assert_ok(); + + // execute action ok + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + let _ = sc.perform_action_endpoint(1); + }, + ) + .assert_ok(); + + // execute action via canExecute -> no signatures required + let adder_addr = ms_setup.adder_wrapper.address_ref(); + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + let mut add_function_args = ManagedArgBuffer::new(); + add_function_args + .push_arg::>(managed_biguint!(5)); + + let func_result = sc.propose_transfer_execute( + managed_address!(adder_addr), + managed_biguint!(0), + Option::None, + FunctionCall { + function_name: managed_buffer!(b"add"), + arg_buffer: add_function_args, + }, + OptionalValue::None, + ); + assert!(func_result.is_none()); + }, + ) + .assert_ok(); + + // check action was actually executed inside adder + ms_setup + .b_mock + .execute_query(&ms_setup.adder_wrapper, |sc| { + assert_eq!(sc.sum().get(), managed_biguint!(5)); + }) + .assert_ok(); + + // remove module + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + sc.propose_remove_module( + managed_address!(can_execute_mock.address_ref()), + OptionalValue::None, + ); + }, + ) + .assert_ok(); + + // ID is 3, as even though previous proposal didn't actually register, an action ID is still used + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + let mut signatures = MultiValueEncoded::new(); + signatures.push(SignatureArg { + user_address: managed_address!(&other_board_member), + nonce: 1, + action_type: ActionType::SimpleAction, + signature_type: SignatureType::Ed25519, // unused + raw_sig_bytes: managed_buffer!(b"signature"), + }); + + sc.sign(3, signatures) + }, + ) + .assert_ok(); + + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + let _ = sc.perform_action_endpoint(3); + }, + ) + .assert_ok(); + + // user try execute action directly again + ms_setup + .b_mock + .execute_tx( + &ms_setup.first_board_member, + &ms_setup.ms_wrapper, + &rust_biguint!(0), + |sc| { + let mut add_function_args = ManagedArgBuffer::new(); + add_function_args + .push_arg::>(managed_biguint!(5)); + + let func_result = sc.propose_transfer_execute( + managed_address!(adder_addr), + managed_biguint!(0), + Option::None, + FunctionCall { + function_name: managed_buffer!(b"add"), + arg_buffer: add_function_args, + }, + OptionalValue::None, + ); + + // action didn't execute, it returned action_id + assert!(func_result.is_some()); + }, + ) + .assert_ok(); +} diff --git a/contracts/multisig-improved/wasm-multisig-full/Cargo.lock b/contracts/multisig-improved/wasm-multisig-full/Cargo.lock new file mode 100644 index 00000000..89337879 --- /dev/null +++ b/contracts/multisig-improved/wasm-multisig-full/Cargo.lock @@ -0,0 +1,198 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "multisig-full-wasm" +version = "0.0.0" +dependencies = [ + "multisig-improved", + "multiversx-sc-wasm-adapter", +] + +[[package]] +name = "multisig-improved" +version = "1.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-modules", +] + +[[package]] +name = "multiversx-sc" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236f7890b2208796df8b5ac73b8572ffaf5e2b1531c7ad549d669328b715b657" +dependencies = [ + "bitflags", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcecd449ea708b72f92edaa17158fe4859c1780aed9b52b14de45f26124ccb8b" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f7fa25402e5e8054d719951289306fd79e481f7c21b2565b5549b6bc359772" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb683bc78d0e2eb43c16cac790144f53cc2ab27912aeb1484433895742ce698d" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-modules" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16af268784dff8a34cb696605413c325253da793d85f81b00dcb0e66f82963c9" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f0d6be22f911ce45427491a9bec94612a1678eab2769dd08c9c9731d13da53" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unwrap-infallible" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151ac09978d3c2862c4e39b557f4eceee2cc72150bc4cb4f16abf061b6e381fb" diff --git a/contracts/multisig-improved/wasm-multisig-full/Cargo.toml b/contracts/multisig-improved/wasm-multisig-full/Cargo.toml new file mode 100644 index 00000000..85546efd --- /dev/null +++ b/contracts/multisig-improved/wasm-multisig-full/Cargo.toml @@ -0,0 +1,34 @@ +# Code generated by the multiversx-sc build system. DO NOT EDIT. + +# ########################################## +# ############## AUTO-GENERATED ############# +# ########################################## + +[package] +name = "multisig-full-wasm" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = false + +[profile.dev] +panic = "abort" + +[dependencies.multisig-improved] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.51.1" + +[workspace] +members = ["."] diff --git a/contracts/multisig-improved/wasm-multisig-full/src/lib.rs b/contracts/multisig-improved/wasm-multisig-full/src/lib.rs new file mode 100644 index 00000000..39343eee --- /dev/null +++ b/contracts/multisig-improved/wasm-multisig-full/src/lib.rs @@ -0,0 +1,70 @@ +// Code generated by the multiversx-sc build system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Upgrade: 1 +// Endpoints: 44 +// Async Callback: 1 +// Total number of exported functions: 47 + +#![no_std] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + multisig_improved + ( + init => init + upgrade => upgrade + deposit => deposit + getQuorum => quorum + getNumBoardMembers => num_board_members + getNumGroups => num_groups + getNumProposers => num_proposers + getActionGroup => action_groups + getLastGroupActionId => last_action_group_id + quorumReached => quorum_reached + proposeAddBoardMember => propose_add_board_member + proposeAddProposer => propose_add_proposer + proposeRemoveUser => propose_remove_user + proposeChangeQuorum => propose_change_quorum + proposeTransferExecute => propose_transfer_execute + proposeTransferExecuteEsdt => propose_transfer_execute_esdt + proposeAsyncCall => propose_async_call + proposeSCDeployFromSource => propose_sc_deploy_from_source + proposeSCUpgradeFromSource => propose_sc_upgrade_from_source + proposeAddModule => propose_add_module + proposeRemoveModule => propose_remove_module + proposeBatch => propose_batch + performAction => perform_action_endpoint + performBatch => perform_batch + discardAction => discard_action_endpoint + discardBatch => discard_batch + sign => sign + signBatch => sign_batch + signAndPerform => sign_and_perform + signBatchAndPerform => sign_batch_and_perform + unsign => unsign + unsignBatch => unsign_batch + unsignForOutdatedBoardMembers => unsign_for_outdated_board_members + getNrDeployedModules => nr_deployed_modules + signed => signed + getActionLastIndex => get_action_last_index + getUserNonce => get_user_nonce + dnsRegister => dns_register + getPendingActionFullInfo => get_pending_action_full_info + getActionSignerCount => get_action_signer_count + getActionValidSignerCount => get_action_valid_signer_count_view + getActionSigners => get_action_signers_view + userRole => user_role + getAllBoardMembers => get_all_board_members + getAllProposers => get_all_proposers + getActionData => get_action_data + ) +} + +multiversx_sc_wasm_adapter::async_callback! { multisig_improved } diff --git a/contracts/multisig-improved/wasm-multisig-view/Cargo.lock b/contracts/multisig-improved/wasm-multisig-view/Cargo.lock new file mode 100644 index 00000000..c70f1b28 --- /dev/null +++ b/contracts/multisig-improved/wasm-multisig-view/Cargo.lock @@ -0,0 +1,198 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "multisig-improved" +version = "1.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-modules", +] + +[[package]] +name = "multisig-view-wasm" +version = "0.0.0" +dependencies = [ + "multisig-improved", + "multiversx-sc-wasm-adapter", +] + +[[package]] +name = "multiversx-sc" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236f7890b2208796df8b5ac73b8572ffaf5e2b1531c7ad549d669328b715b657" +dependencies = [ + "bitflags", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcecd449ea708b72f92edaa17158fe4859c1780aed9b52b14de45f26124ccb8b" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f7fa25402e5e8054d719951289306fd79e481f7c21b2565b5549b6bc359772" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb683bc78d0e2eb43c16cac790144f53cc2ab27912aeb1484433895742ce698d" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-modules" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16af268784dff8a34cb696605413c325253da793d85f81b00dcb0e66f82963c9" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f0d6be22f911ce45427491a9bec94612a1678eab2769dd08c9c9731d13da53" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unwrap-infallible" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151ac09978d3c2862c4e39b557f4eceee2cc72150bc4cb4f16abf061b6e381fb" diff --git a/contracts/multisig-improved/wasm-multisig-view/Cargo.toml b/contracts/multisig-improved/wasm-multisig-view/Cargo.toml new file mode 100644 index 00000000..acfbeb13 --- /dev/null +++ b/contracts/multisig-improved/wasm-multisig-view/Cargo.toml @@ -0,0 +1,34 @@ +# Code generated by the multiversx-sc build system. DO NOT EDIT. + +# ########################################## +# ############## AUTO-GENERATED ############# +# ########################################## + +[package] +name = "multisig-view-wasm" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = false + +[profile.dev] +panic = "abort" + +[dependencies.multisig-improved] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.51.1" + +[workspace] +members = ["."] diff --git a/contracts/multisig-improved/wasm-multisig-view/src/lib.rs b/contracts/multisig-improved/wasm-multisig-view/src/lib.rs new file mode 100644 index 00000000..d4831b6c --- /dev/null +++ b/contracts/multisig-improved/wasm-multisig-view/src/lib.rs @@ -0,0 +1,33 @@ +// Code generated by the multiversx-sc build system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Endpoints: 8 +// Async Callback (empty): 1 +// Total number of exported functions: 10 + +#![no_std] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::external_view_init! {} + +multiversx_sc_wasm_adapter::external_view_endpoints! { + multisig_improved + ( + getPendingActionFullInfo => get_pending_action_full_info + getActionSignerCount => get_action_signer_count + getActionValidSignerCount => get_action_valid_signer_count_view + getActionSigners => get_action_signers_view + userRole => user_role + getAllBoardMembers => get_all_board_members + getAllProposers => get_all_proposers + getActionData => get_action_data + ) +} + +multiversx_sc_wasm_adapter::async_callback_empty! {} diff --git a/contracts/multisig-improved/wasm/Cargo.lock b/contracts/multisig-improved/wasm/Cargo.lock new file mode 100644 index 00000000..9c4f2c95 --- /dev/null +++ b/contracts/multisig-improved/wasm/Cargo.lock @@ -0,0 +1,198 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "multisig-improved" +version = "1.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-modules", +] + +[[package]] +name = "multisig-wasm" +version = "0.0.0" +dependencies = [ + "multisig-improved", + "multiversx-sc-wasm-adapter", +] + +[[package]] +name = "multiversx-sc" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236f7890b2208796df8b5ac73b8572ffaf5e2b1531c7ad549d669328b715b657" +dependencies = [ + "bitflags", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcecd449ea708b72f92edaa17158fe4859c1780aed9b52b14de45f26124ccb8b" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f7fa25402e5e8054d719951289306fd79e481f7c21b2565b5549b6bc359772" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb683bc78d0e2eb43c16cac790144f53cc2ab27912aeb1484433895742ce698d" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-modules" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16af268784dff8a34cb696605413c325253da793d85f81b00dcb0e66f82963c9" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f0d6be22f911ce45427491a9bec94612a1678eab2769dd08c9c9731d13da53" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unwrap-infallible" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151ac09978d3c2862c4e39b557f4eceee2cc72150bc4cb4f16abf061b6e381fb" diff --git a/contracts/multisig-improved/wasm/Cargo.toml b/contracts/multisig-improved/wasm/Cargo.toml new file mode 100644 index 00000000..c745aed8 --- /dev/null +++ b/contracts/multisig-improved/wasm/Cargo.toml @@ -0,0 +1,34 @@ +# Code generated by the multiversx-sc build system. DO NOT EDIT. + +# ########################################## +# ############## AUTO-GENERATED ############# +# ########################################## + +[package] +name = "multisig-wasm" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = false + +[profile.dev] +panic = "abort" + +[dependencies.multisig-improved] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.51.1" + +[workspace] +members = ["."] diff --git a/contracts/multisig-improved/wasm/src/lib.rs b/contracts/multisig-improved/wasm/src/lib.rs new file mode 100644 index 00000000..813e9668 --- /dev/null +++ b/contracts/multisig-improved/wasm/src/lib.rs @@ -0,0 +1,62 @@ +// Code generated by the multiversx-sc build system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Upgrade: 1 +// Endpoints: 36 +// Async Callback: 1 +// Total number of exported functions: 39 + +#![no_std] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + multisig_improved + ( + init => init + upgrade => upgrade + deposit => deposit + getQuorum => quorum + getNumBoardMembers => num_board_members + getNumGroups => num_groups + getNumProposers => num_proposers + getActionGroup => action_groups + getLastGroupActionId => last_action_group_id + quorumReached => quorum_reached + proposeAddBoardMember => propose_add_board_member + proposeAddProposer => propose_add_proposer + proposeRemoveUser => propose_remove_user + proposeChangeQuorum => propose_change_quorum + proposeTransferExecute => propose_transfer_execute + proposeTransferExecuteEsdt => propose_transfer_execute_esdt + proposeAsyncCall => propose_async_call + proposeSCDeployFromSource => propose_sc_deploy_from_source + proposeSCUpgradeFromSource => propose_sc_upgrade_from_source + proposeAddModule => propose_add_module + proposeRemoveModule => propose_remove_module + proposeBatch => propose_batch + performAction => perform_action_endpoint + performBatch => perform_batch + discardAction => discard_action_endpoint + discardBatch => discard_batch + sign => sign + signBatch => sign_batch + signAndPerform => sign_and_perform + signBatchAndPerform => sign_batch_and_perform + unsign => unsign + unsignBatch => unsign_batch + unsignForOutdatedBoardMembers => unsign_for_outdated_board_members + getNrDeployedModules => nr_deployed_modules + signed => signed + getActionLastIndex => get_action_last_index + getUserNonce => get_user_nonce + dnsRegister => dns_register + ) +} + +multiversx_sc_wasm_adapter::async_callback! { multisig_improved }