diff --git a/contracts/feature-tests/composability/forwarder/tests/forwarder_whitebox_test.rs b/contracts/feature-tests/composability/forwarder/tests/forwarder_whitebox_test.rs index efd969a716..56f383eb75 100644 --- a/contracts/feature-tests/composability/forwarder/tests/forwarder_whitebox_test.rs +++ b/contracts/feature-tests/composability/forwarder/tests/forwarder_whitebox_test.rs @@ -1,7 +1,7 @@ use forwarder::nft::{Color, ForwarderNftModule}; use multiversx_sc::{contract_base::ContractBase, types::Address}; use multiversx_sc_scenario::{ - managed_address, managed_biguint, managed_token_id, rust_biguint, + managed_address, managed_biguint, managed_token_id, scenario_model::{ Account, AddressValue, CheckAccount, CheckStateStep, ScCallStep, SetStateStep, }, @@ -88,7 +88,7 @@ fn test_nft_update_attributes_and_send() { &forwarder_whitebox, ScCallStep::new() .from(USER_ADDRESS_EXPR) - .esdt_transfer(NFT_TOKEN_ID, 1, rust_biguint!(1)), + .esdt_transfer(NFT_TOKEN_ID, 1, "1"), |sc| { sc.nft_update_attributes(managed_token_id!(NFT_TOKEN_ID), 1, new_attributes); diff --git a/contracts/feature-tests/composability/transfer-role-features/tests/transfer_role_whitebox_test.rs b/contracts/feature-tests/composability/transfer-role-features/tests/transfer_role_whitebox_test.rs index 7294fa76b4..c5af4e7712 100644 --- a/contracts/feature-tests/composability/transfer-role-features/tests/transfer_role_whitebox_test.rs +++ b/contracts/feature-tests/composability/transfer-role-features/tests/transfer_role_whitebox_test.rs @@ -3,7 +3,7 @@ use multiversx_sc::types::{ }; use multiversx_sc_modules::transfer_role_proxy::TransferRoleProxyModule; use multiversx_sc_scenario::{ - managed_address, managed_biguint, managed_buffer, managed_token_id, rust_biguint, + managed_address, managed_biguint, managed_buffer, managed_token_id, scenario_model::{ Account, AddressValue, CheckAccount, CheckStateStep, ScCallStep, ScDeployStep, SetStateStep, }, @@ -95,7 +95,7 @@ fn test_transfer_role() { ScCallStep::new().from(USER_ADDRESS_EXPR).esdt_transfer( TRANSFER_TOKEN_ID, 0, - rust_biguint!(100), + "100", ), |sc| { let payments = ManagedVec::from_single_item(EsdtTokenPayment::new( @@ -114,11 +114,11 @@ fn test_transfer_role() { world.check_state_step(CheckStateStep::new().put_account( USER_ADDRESS_EXPR, - CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, &rust_biguint!(900)), + CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, "900"), )); world.check_state_step(CheckStateStep::new().put_account( OWNER_ADDRESS_EXPR, - CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, &rust_biguint!(100)), + CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, "100"), )); // transfer to user - err, not whitelisted @@ -126,7 +126,7 @@ fn test_transfer_role() { &transfer_role_features_whitebox, ScCallStep::new() .from(USER_ADDRESS_EXPR) - .esdt_transfer(TRANSFER_TOKEN_ID, 0, rust_biguint!(100)) + .esdt_transfer(TRANSFER_TOKEN_ID, 0, "100") .no_expect(), |sc| { let payments = ManagedVec::from_single_item(EsdtTokenPayment::new( @@ -152,7 +152,7 @@ fn test_transfer_role() { ScCallStep::new().from(USER_ADDRESS_EXPR).esdt_transfer( TRANSFER_TOKEN_ID, 0, - rust_biguint!(100), + "100", ), |sc| { let payments = ManagedVec::from_single_item(EsdtTokenPayment::new( @@ -173,11 +173,11 @@ fn test_transfer_role() { world.check_state_step(CheckStateStep::new().put_account( USER_ADDRESS_EXPR, - CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, &rust_biguint!(800)), + CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, "800"), )); world.check_state_step(CheckStateStep::new().put_account( VAULT_ADDRESS_EXPR, - CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, &rust_biguint!(100)), + CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, "100"), )); // transfer to sc - reject @@ -186,7 +186,7 @@ fn test_transfer_role() { ScCallStep::new().from(USER_ADDRESS_EXPR).esdt_transfer( TRANSFER_TOKEN_ID, 0, - rust_biguint!(100), + "100", ), |sc| { let payments = ManagedVec::from_single_item(EsdtTokenPayment::new( @@ -207,11 +207,11 @@ fn test_transfer_role() { world.check_state_step(CheckStateStep::new().put_account( USER_ADDRESS_EXPR, - CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, &rust_biguint!(800)), + CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, "800"), )); world.check_state_step(CheckStateStep::new().put_account( VAULT_ADDRESS_EXPR, - CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, &rust_biguint!(100)), + CheckAccount::new().esdt_balance(TRANSFER_TOKEN_ID_EXPR, "100"), )); } diff --git a/contracts/feature-tests/use-module/tests/gov_module_legacy_test.rs b/contracts/feature-tests/use-module/tests/gov_module_legacy_test.rs deleted file mode 100644 index 1a51124bb5..0000000000 --- a/contracts/feature-tests/use-module/tests/gov_module_legacy_test.rs +++ /dev/null @@ -1,507 +0,0 @@ -#![allow(deprecated)] // TODO: migrate tests - -use multiversx_sc::types::{Address, ManagedVec, MultiValueEncoded}; -use multiversx_sc_modules::governance::{ - governance_configurable::GovernanceConfigurablePropertiesModule, governance_proposal::VoteType, - GovernanceModule, -}; -use multiversx_sc_scenario::{ - managed_address, managed_biguint, managed_buffer, managed_token_id, rust_biguint, - testing_framework::{BlockchainStateWrapper, ContractObjWrapper, TxResult}, - DebugApi, -}; - -static GOV_TOKEN_ID: &[u8] = b"GOV-123456"; -const QUORUM: u64 = 1_500; -const MIN_BALANCE_PROPOSAL: u64 = 500; -const VOTING_DELAY_BLOCKS: u64 = 10; -const VOTING_PERIOD_BLOCKS: u64 = 20; -const LOCKING_PERIOD_BLOCKS: u64 = 30; - -const INITIAL_GOV_TOKEN_BALANCE: u64 = 1_000; -const GAS_LIMIT: u64 = 1_000_000; - -pub struct Payment { - pub token: Vec, - pub nonce: u64, - pub amount: u64, -} - -pub struct GovSetup -where - GovBuilder: 'static + Copy + Fn() -> use_module::ContractObj, -{ - pub b_mock: BlockchainStateWrapper, - pub owner: Address, - pub first_user: Address, - pub second_user: Address, - pub third_user: Address, - pub gov_wrapper: ContractObjWrapper, GovBuilder>, - pub current_block: u64, -} - -impl GovSetup -where - GovBuilder: 'static + Copy + Fn() -> use_module::ContractObj, -{ - pub fn new(gov_builder: GovBuilder) -> Self { - let rust_zero = rust_biguint!(0); - let initial_gov = rust_biguint!(INITIAL_GOV_TOKEN_BALANCE); - - let mut b_mock = BlockchainStateWrapper::new(); - - let owner = b_mock.create_user_account(&rust_zero); - b_mock.set_esdt_balance(&owner, GOV_TOKEN_ID, &initial_gov); - - let first_user = b_mock.create_user_account(&rust_zero); - b_mock.set_esdt_balance(&first_user, GOV_TOKEN_ID, &initial_gov); - - let second_user = b_mock.create_user_account(&rust_zero); - b_mock.set_esdt_balance(&second_user, GOV_TOKEN_ID, &initial_gov); - - let third_user = b_mock.create_user_account(&rust_zero); - b_mock.set_esdt_balance(&third_user, GOV_TOKEN_ID, &initial_gov); - - let gov_wrapper = - b_mock.create_sc_account(&rust_zero, Some(&owner), gov_builder, "gov path"); - - b_mock - .execute_tx(&owner, &gov_wrapper, &rust_zero, |sc| { - sc.init_governance_module( - managed_token_id!(GOV_TOKEN_ID), - managed_biguint!(QUORUM), - managed_biguint!(MIN_BALANCE_PROPOSAL), - VOTING_DELAY_BLOCKS, - VOTING_PERIOD_BLOCKS, - LOCKING_PERIOD_BLOCKS, - ); - }) - .assert_ok(); - - b_mock.set_block_nonce(10); - - Self { - b_mock, - owner, - first_user, - second_user, - third_user, - gov_wrapper, - current_block: 10, - } - } - - pub fn propose( - &mut self, - proposer: &Address, - gov_token_amount: u64, - dest_address: &Address, - endpoint_name: &[u8], - args: Vec>, - ) -> (TxResult, usize) { - let mut proposal_id = 0; - let result = self.b_mock.execute_esdt_transfer( - proposer, - &self.gov_wrapper, - GOV_TOKEN_ID, - 0, - &rust_biguint!(gov_token_amount), - |sc| { - let mut args_managed = ManagedVec::new(); - for arg in args { - args_managed.push(managed_buffer!(&arg)); - } - - let mut actions = MultiValueEncoded::new(); - actions.push( - ( - GAS_LIMIT, - managed_address!(dest_address), - managed_buffer!(endpoint_name), - args_managed, - ) - .into(), - ); - - proposal_id = sc.propose(managed_buffer!(b"change quorum"), actions); - }, - ); - - (result, proposal_id) - } - - pub fn vote(&mut self, voter: &Address, proposal_id: usize, gov_token_amount: u64) -> TxResult { - self.b_mock.execute_esdt_transfer( - voter, - &self.gov_wrapper, - GOV_TOKEN_ID, - 0, - &rust_biguint!(gov_token_amount), - |sc| { - sc.vote(proposal_id, VoteType::UpVote); - }, - ) - } - - pub fn down_vote( - &mut self, - voter: &Address, - proposal_id: usize, - gov_token_amount: u64, - ) -> TxResult { - self.b_mock.execute_esdt_transfer( - voter, - &self.gov_wrapper, - GOV_TOKEN_ID, - 0, - &rust_biguint!(gov_token_amount), - |sc| { - sc.vote(proposal_id, VoteType::DownVote); - }, - ) - } - - pub fn down_veto_vote( - &mut self, - voter: &Address, - proposal_id: usize, - gov_token_amount: u64, - ) -> TxResult { - self.b_mock.execute_esdt_transfer( - voter, - &self.gov_wrapper, - GOV_TOKEN_ID, - 0, - &rust_biguint!(gov_token_amount), - |sc| { - sc.vote(proposal_id, VoteType::DownVetoVote); - }, - ) - } - - pub fn abstain_vote( - &mut self, - voter: &Address, - proposal_id: usize, - gov_token_amount: u64, - ) -> TxResult { - self.b_mock.execute_esdt_transfer( - voter, - &self.gov_wrapper, - GOV_TOKEN_ID, - 0, - &rust_biguint!(gov_token_amount), - |sc| { - sc.vote(proposal_id, VoteType::AbstainVote); - }, - ) - } - - pub fn queue(&mut self, proposal_id: usize) -> TxResult { - self.b_mock.execute_tx( - &self.first_user, - &self.gov_wrapper, - &rust_biguint!(0), - |sc| { - sc.queue(proposal_id); - }, - ) - } - - pub fn execute(&mut self, proposal_id: usize) -> TxResult { - self.b_mock.execute_tx( - &self.first_user, - &self.gov_wrapper, - &rust_biguint!(0), - |sc| { - sc.execute(proposal_id); - }, - ) - } - - pub fn cancel(&mut self, caller: &Address, proposal_id: usize) -> TxResult { - self.b_mock - .execute_tx(caller, &self.gov_wrapper, &rust_biguint!(0), |sc| { - sc.cancel(proposal_id); - }) - } - - pub fn increment_block_nonce(&mut self, inc_amount: u64) { - self.current_block += inc_amount; - self.b_mock.set_block_nonce(self.current_block); - } - - pub fn set_block_nonce(&mut self, block_nonce: u64) { - self.current_block = block_nonce; - self.b_mock.set_block_nonce(self.current_block); - } -} - -#[test] -fn init_gov_test() { - let _ = GovSetup::new(use_module::contract_obj); -} - -#[test] -fn change_gov_config_test() { - let mut gov_setup = GovSetup::new(use_module::contract_obj); - - let owner_addr = gov_setup.owner.clone(); - let first_user_addr = gov_setup.first_user.clone(); - let second_user_addr = gov_setup.second_user.clone(); - let third_user_addr = gov_setup.third_user.clone(); - let sc_addr = gov_setup.gov_wrapper.address_ref().clone(); - - let (result, proposal_id) = gov_setup.propose( - &first_user_addr, - 500, - &sc_addr, - b"changeQuorum", - vec![1_000u64.to_be_bytes().to_vec()], - ); - result.assert_ok(); - assert_eq!(proposal_id, 1); - - // vote too early - gov_setup - .vote(&second_user_addr, proposal_id, 999) - .assert_user_error("Proposal is not active"); - - gov_setup.increment_block_nonce(VOTING_DELAY_BLOCKS); - - gov_setup - .vote(&second_user_addr, proposal_id, 999) - .assert_ok(); - - // try execute before queue - gov_setup - .execute(proposal_id) - .assert_user_error("Can only execute queued proposals"); - - // try queue before voting ends - gov_setup - .queue(proposal_id) - .assert_user_error("Can only queue succeeded proposals"); - - gov_setup.increment_block_nonce(VOTING_PERIOD_BLOCKS); - - // try queue not enough votes - gov_setup - .queue(proposal_id) - .assert_user_error("Can only queue succeeded proposals"); - - // user 1 vote again - gov_setup.set_block_nonce(20); - gov_setup - .vote(&first_user_addr, proposal_id, 200) - .assert_ok(); - - // owner downvote - gov_setup - .down_vote(&owner_addr, proposal_id, 200) - .assert_ok(); - - // try queue too many downvotes - gov_setup.set_block_nonce(45); - gov_setup - .queue(proposal_id) - .assert_user_error("Can only queue succeeded proposals"); - - // user 1 vote again - gov_setup.set_block_nonce(20); - gov_setup - .vote(&first_user_addr, proposal_id, 200) - .assert_user_error("Already voted for this proposal"); - - // user 3 vote again - gov_setup - .vote(&third_user_addr, proposal_id, 200) - .assert_ok(); - - // queue ok - gov_setup.set_block_nonce(45); - gov_setup.queue(proposal_id).assert_ok(); - - // try execute too early - gov_setup - .execute(proposal_id) - .assert_user_error("Proposal is in timelock status. Try again later"); - - // execute ok - gov_setup.increment_block_nonce(LOCKING_PERIOD_BLOCKS); - gov_setup.execute(proposal_id).assert_ok(); - - // after execution, quorum changed from 1_500 to the proposed 1_000 - gov_setup - .b_mock - .execute_query(&gov_setup.gov_wrapper, |sc| { - assert_eq!(sc.quorum().get(), managed_biguint!(1_000)); - assert!(sc.proposals().item_is_empty(1)); - }) - .assert_ok(); - - gov_setup - .b_mock - .check_esdt_balance(&first_user_addr, GOV_TOKEN_ID, &rust_biguint!(300)); - gov_setup - .b_mock - .check_esdt_balance(&second_user_addr, GOV_TOKEN_ID, &rust_biguint!(1)); - gov_setup - .b_mock - .check_esdt_balance(&third_user_addr, GOV_TOKEN_ID, &rust_biguint!(800)); - gov_setup - .b_mock - .check_esdt_balance(&owner_addr, GOV_TOKEN_ID, &rust_biguint!(800)); -} - -#[test] -fn down_veto_gov_config_test() { - let mut gov_setup = GovSetup::new(use_module::contract_obj); - - let first_user_addr = gov_setup.first_user.clone(); - let second_user_addr = gov_setup.second_user.clone(); - let third_user_addr = gov_setup.third_user.clone(); - let sc_addr = gov_setup.gov_wrapper.address_ref().clone(); - - let (result, proposal_id) = gov_setup.propose( - &first_user_addr, - 500, - &sc_addr, - b"changeQuorum", - vec![1_000u64.to_be_bytes().to_vec()], - ); - result.assert_ok(); - assert_eq!(proposal_id, 1); - - gov_setup.increment_block_nonce(VOTING_DELAY_BLOCKS); - - gov_setup - .vote(&first_user_addr, proposal_id, 300) - .assert_ok(); - - gov_setup.increment_block_nonce(VOTING_PERIOD_BLOCKS); - - // user 1 vote again - gov_setup.set_block_nonce(20); - gov_setup - .vote(&second_user_addr, proposal_id, 200) - .assert_ok(); - - // user 3 vote again - gov_setup - .down_veto_vote(&third_user_addr, proposal_id, 200) - .assert_ok(); - - // Vote didn't succeed; - gov_setup.set_block_nonce(45); - gov_setup - .queue(proposal_id) - .assert_user_error("Can only queue succeeded proposals"); - - gov_setup - .b_mock - .check_esdt_balance(&first_user_addr, GOV_TOKEN_ID, &rust_biguint!(200)); - gov_setup - .b_mock - .check_esdt_balance(&second_user_addr, GOV_TOKEN_ID, &rust_biguint!(800)); - gov_setup - .b_mock - .check_esdt_balance(&third_user_addr, GOV_TOKEN_ID, &rust_biguint!(800)); -} - -#[test] -fn abstain_vote_gov_config_test() { - let mut gov_setup = GovSetup::new(use_module::contract_obj); - - let first_user_addr = gov_setup.first_user.clone(); - let second_user_addr = gov_setup.second_user.clone(); - let third_user_addr = gov_setup.third_user.clone(); - let sc_addr = gov_setup.gov_wrapper.address_ref().clone(); - - let (result, proposal_id) = gov_setup.propose( - &first_user_addr, - 500, - &sc_addr, - b"changeQuorum", - vec![1_000u64.to_be_bytes().to_vec()], - ); - result.assert_ok(); - assert_eq!(proposal_id, 1); - - gov_setup.increment_block_nonce(VOTING_DELAY_BLOCKS); - - gov_setup - .vote(&first_user_addr, proposal_id, 500) - .assert_ok(); - - gov_setup.increment_block_nonce(VOTING_PERIOD_BLOCKS); - - // user 1 vote again - gov_setup.set_block_nonce(20); - gov_setup - .down_vote(&second_user_addr, proposal_id, 400) - .assert_ok(); - - // user 3 vote again - gov_setup - .abstain_vote(&third_user_addr, proposal_id, 600) - .assert_ok(); - - // Vote didn't succeed; - gov_setup.set_block_nonce(45); - gov_setup.queue(proposal_id).assert_ok(); - - // execute ok - gov_setup.increment_block_nonce(LOCKING_PERIOD_BLOCKS); - gov_setup.execute(proposal_id).assert_ok(); - - // after execution, quorum changed from 1_500 to the proposed 1_000 - gov_setup - .b_mock - .execute_query(&gov_setup.gov_wrapper, |sc| { - assert_eq!(sc.quorum().get(), managed_biguint!(1_000)); - assert!(sc.proposals().item_is_empty(1)); - }) - .assert_ok(); - - gov_setup - .b_mock - .check_esdt_balance(&first_user_addr, GOV_TOKEN_ID, &rust_biguint!(0)); - gov_setup - .b_mock - .check_esdt_balance(&second_user_addr, GOV_TOKEN_ID, &rust_biguint!(600)); - gov_setup - .b_mock - .check_esdt_balance(&third_user_addr, GOV_TOKEN_ID, &rust_biguint!(400)); -} - -#[test] -fn gov_cancel_defeated_proposal_test() { - let mut gov_setup = GovSetup::new(use_module::contract_obj); - - let first_user_addr = gov_setup.first_user.clone(); - let second_user_addr = gov_setup.second_user.clone(); - let sc_addr = gov_setup.gov_wrapper.address_ref().clone(); - let (result, proposal_id) = gov_setup.propose( - &first_user_addr, - 500, - &sc_addr, - b"changeQuorum", - vec![1_000u64.to_be_bytes().to_vec()], - ); - result.assert_ok(); - assert_eq!(proposal_id, 1); - - gov_setup.increment_block_nonce(VOTING_DELAY_BLOCKS); - gov_setup - .down_vote(&second_user_addr, proposal_id, 999) - .assert_ok(); - - // try cancel too early - gov_setup - .cancel(&second_user_addr, proposal_id) - .assert_user_error("Action may not be cancelled"); - - gov_setup.increment_block_nonce(VOTING_PERIOD_BLOCKS); - gov_setup.cancel(&second_user_addr, proposal_id).assert_ok(); -} diff --git a/contracts/feature-tests/use-module/tests/gov_module_whitebox_test.rs b/contracts/feature-tests/use-module/tests/gov_module_whitebox_test.rs new file mode 100644 index 0000000000..3329b748d9 --- /dev/null +++ b/contracts/feature-tests/use-module/tests/gov_module_whitebox_test.rs @@ -0,0 +1,603 @@ +use multiversx_sc::types::{Address, ManagedVec, MultiValueEncoded}; +use multiversx_sc_modules::governance::{ + governance_configurable::GovernanceConfigurablePropertiesModule, governance_proposal::VoteType, + GovernanceModule, +}; +use multiversx_sc_scenario::{ + managed_address, managed_biguint, managed_buffer, managed_token_id, + scenario_model::{ + Account, AddressValue, CheckAccount, CheckStateStep, ScCallStep, ScDeployStep, SetStateStep, + }, + ScenarioWorld, WhiteboxContract, +}; + +const GOV_TOKEN_ID_EXPR: &str = "str:GOV-123456"; +const GOV_TOKEN_ID: &[u8] = b"GOV-123456"; +const QUORUM: u64 = 1_500; +const MIN_BALANCE_PROPOSAL: u64 = 500; +const VOTING_DELAY_BLOCKS: u64 = 10; +const VOTING_PERIOD_BLOCKS: u64 = 20; +const LOCKING_PERIOD_BLOCKS: u64 = 30; + +const INITIAL_GOV_TOKEN_BALANCE: u64 = 1_000; +const GAS_LIMIT: u64 = 1_000_000; + +const USE_MODULE_ADDRESS_EXPR: &str = "sc:use-module"; +const USE_MODULE_PATH_EXPR: &str = "file:output/use-module.wasm"; + +const OWNER_ADDRESS_EXPR: &str = "address:owner"; +const FIRST_USER_ADDRESS_EXPR: &str = "address:first-user"; +const SECOND_USER_ADDRESS_EXPR: &str = "address:second-user"; +const THIRD_USER_ADDRESS_EXPR: &str = "address:third-user"; + +pub struct Payment { + pub token: Vec, + pub nonce: u64, + pub amount: u64, +} + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.set_current_dir_from_workspace("contracts/features-tests/use-module"); + + blockchain.register_contract(USE_MODULE_PATH_EXPR, use_module::ContractBuilder); + blockchain +} + +fn setup() -> ScenarioWorld { + let mut world = world(); + + world.set_state_step( + SetStateStep::new() + .put_account( + OWNER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(GOV_TOKEN_ID_EXPR, INITIAL_GOV_TOKEN_BALANCE), + ) + .new_address(OWNER_ADDRESS_EXPR, 1, USE_MODULE_ADDRESS_EXPR) + .put_account( + FIRST_USER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(GOV_TOKEN_ID_EXPR, INITIAL_GOV_TOKEN_BALANCE), + ) + .put_account( + SECOND_USER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(GOV_TOKEN_ID_EXPR, INITIAL_GOV_TOKEN_BALANCE), + ) + .put_account( + THIRD_USER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(GOV_TOKEN_ID_EXPR, INITIAL_GOV_TOKEN_BALANCE), + ), + ); + + // init + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + let use_module_code = world.code_expression(USE_MODULE_PATH_EXPR); + + world.whitebox_deploy( + &use_module_whitebox, + ScDeployStep::new() + .from(OWNER_ADDRESS_EXPR) + .code(use_module_code), + |sc| { + sc.init_governance_module( + managed_token_id!(GOV_TOKEN_ID), + managed_biguint!(QUORUM), + managed_biguint!(MIN_BALANCE_PROPOSAL), + VOTING_DELAY_BLOCKS, + VOTING_PERIOD_BLOCKS, + LOCKING_PERIOD_BLOCKS, + ); + }, + ); + + world.set_state_step(SetStateStep::new().block_nonce(10)); + + world +} + +pub fn propose( + world: &mut ScenarioWorld, + proposer: &Address, + gov_token_amount: u64, + dest_address: &Address, + endpoint_name: &[u8], + args: Vec>, +) -> usize { + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + + let mut proposal_id = 0; + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(proposer).esdt_transfer( + GOV_TOKEN_ID, + 0, + gov_token_amount, + ), + |sc| { + let mut args_managed = ManagedVec::new(); + for arg in args { + args_managed.push(managed_buffer!(&arg)); + } + + let mut actions = MultiValueEncoded::new(); + actions.push( + ( + GAS_LIMIT, + managed_address!(dest_address), + managed_buffer!(endpoint_name), + args_managed, + ) + .into(), + ); + + proposal_id = sc.propose(managed_buffer!(b"change quorum"), actions); + }, + ); + + proposal_id +} + +#[test] +fn test_init() { + setup(); +} + +#[test] +fn test_change_gov_config() { + let mut world = setup(); + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + + let mut current_block_nonce = 10; + + let proposal_id = propose( + &mut world, + &address_expr_to_address(FIRST_USER_ADDRESS_EXPR), + 500, + &address_expr_to_address(USE_MODULE_ADDRESS_EXPR), + b"changeQuorum", + vec![1_000u64.to_be_bytes().to_vec()], + ); + + assert_eq!(proposal_id, 1); + + // vote too early + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new() + .from(SECOND_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "999") + .no_expect(), + |sc| { + sc.vote(proposal_id, VoteType::UpVote); + }, + |r| { + r.assert_user_error("Proposal is not active"); + }, + ); + + current_block_nonce += VOTING_DELAY_BLOCKS; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(SECOND_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "999"), + |sc| { + sc.vote(proposal_id, VoteType::UpVote); + }, + ); + + // try execute before queue + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.execute(proposal_id); + }, + |r| { + r.assert_user_error("Can only execute queued proposals"); + }, + ); + + // try queue before voting ends + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.queue(proposal_id); + }, + |r| { + r.assert_user_error("Can only queue succeeded proposals"); + }, + ); + + current_block_nonce += VOTING_PERIOD_BLOCKS; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + + // try queue not enough votes + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.queue(proposal_id); + }, + |r| { + r.assert_user_error("Can only queue succeeded proposals"); + }, + ); + + // user 1 vote again + current_block_nonce = 20; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(FIRST_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "200"), + |sc| { + sc.vote(proposal_id, VoteType::UpVote); + }, + ); + + // owner downvote + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR).esdt_transfer( + GOV_TOKEN_ID, + 0, + "200", + ), + |sc| { + sc.vote(proposal_id, VoteType::DownVote); + }, + ); + + // try queue too many downvotes + current_block_nonce = 45; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.queue(proposal_id); + }, + |r| { + r.assert_user_error("Can only queue succeeded proposals"); + }, + ); + + // user 1 vote again + current_block_nonce = 20; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new() + .from(FIRST_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "200") + .no_expect(), + |sc| { + sc.vote(proposal_id, VoteType::UpVote); + }, + |r| { + r.assert_user_error("Already voted for this proposal"); + }, + ); + + // user 3 vote again + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(THIRD_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "200"), + |sc| { + sc.vote(proposal_id, VoteType::UpVote); + }, + ); + + // queue ok + current_block_nonce = 45; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.queue(proposal_id); + }, + ); + + // try execute too early + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.execute(proposal_id); + }, + |r| { + r.assert_user_error("Proposal is in timelock status. Try again later"); + }, + ); + + // execute ok + current_block_nonce += LOCKING_PERIOD_BLOCKS; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.execute(proposal_id); + }, + ); + + // after execution, quorum changed from 1_500 to the proposed 1_000 + world.whitebox_query(&use_module_whitebox, |sc| { + assert_eq!(sc.quorum().get(), managed_biguint!(1_000)); + assert!(sc.proposals().item_is_empty(1)); + }); + + world.check_state_step(CheckStateStep::new().put_account( + FIRST_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "300"), + )); + world.check_state_step(CheckStateStep::new().put_account( + SECOND_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "1"), + )); + world.check_state_step(CheckStateStep::new().put_account( + THIRD_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "800"), + )); + world.check_state_step(CheckStateStep::new().put_account( + OWNER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "800"), + )); +} + +#[test] +fn test_down_veto_gov_config() { + let mut world = setup(); + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + + let mut current_block_nonce = 10; + + let proposal_id = propose( + &mut world, + &address_expr_to_address(FIRST_USER_ADDRESS_EXPR), + 500, + &address_expr_to_address(USE_MODULE_ADDRESS_EXPR), + b"changeQuorum", + vec![1_000u64.to_be_bytes().to_vec()], + ); + + assert_eq!(proposal_id, 1); + + current_block_nonce += VOTING_DELAY_BLOCKS; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(FIRST_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "300"), + |sc| { + sc.vote(proposal_id, VoteType::UpVote); + }, + ); + + current_block_nonce = 20; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(SECOND_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "200"), + |sc| { + sc.vote(proposal_id, VoteType::UpVote); + }, + ); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(THIRD_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "200"), + |sc| { + sc.vote(proposal_id, VoteType::DownVetoVote); + }, + ); + + // Vote didn't succeed; + current_block_nonce = 45; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.queue(proposal_id); + }, + |r| { + r.assert_user_error("Can only queue succeeded proposals"); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + FIRST_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "200"), + )); + world.check_state_step(CheckStateStep::new().put_account( + SECOND_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "800"), + )); + world.check_state_step(CheckStateStep::new().put_account( + THIRD_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "800"), + )); +} + +#[test] +fn test_abstain_vote_gov_config() { + let mut world = setup(); + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + + let mut current_block_nonce = 10; + + let proposal_id = propose( + &mut world, + &address_expr_to_address(FIRST_USER_ADDRESS_EXPR), + 500, + &address_expr_to_address(USE_MODULE_ADDRESS_EXPR), + b"changeQuorum", + vec![1_000u64.to_be_bytes().to_vec()], + ); + + assert_eq!(proposal_id, 1); + + current_block_nonce += VOTING_DELAY_BLOCKS; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(FIRST_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "500"), + |sc| { + sc.vote(proposal_id, VoteType::UpVote); + }, + ); + + current_block_nonce = 20; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(SECOND_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "400"), + |sc| { + sc.vote(proposal_id, VoteType::DownVote); + }, + ); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(THIRD_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "600"), + |sc| { + sc.vote(proposal_id, VoteType::AbstainVote); + }, + ); + + // Vote didn't succeed; + current_block_nonce = 45; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.queue(proposal_id); + }, + ); + + // execute ok + current_block_nonce += LOCKING_PERIOD_BLOCKS; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(FIRST_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.execute(proposal_id); + }, + ); + + // after execution, quorum changed from 1_500 to the proposed 1_000 + world.whitebox_query(&use_module_whitebox, |sc| { + assert_eq!(sc.quorum().get(), managed_biguint!(1_000)); + assert!(sc.proposals().item_is_empty(1)); + }); + + world.check_state_step(CheckStateStep::new().put_account( + FIRST_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "0"), + )); + world.check_state_step(CheckStateStep::new().put_account( + SECOND_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "600"), + )); + world.check_state_step(CheckStateStep::new().put_account( + THIRD_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(GOV_TOKEN_ID_EXPR, "400"), + )); +} + +#[test] +fn test_gov_cancel_defeated_proposal() { + let mut world = setup(); + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + + let mut current_block_nonce = 10; + + let proposal_id = propose( + &mut world, + &address_expr_to_address(FIRST_USER_ADDRESS_EXPR), + 500, + &address_expr_to_address(USE_MODULE_ADDRESS_EXPR), + b"changeQuorum", + vec![1_000u64.to_be_bytes().to_vec()], + ); + + assert_eq!(proposal_id, 1); + + current_block_nonce += VOTING_DELAY_BLOCKS; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(SECOND_USER_ADDRESS_EXPR) + .esdt_transfer(GOV_TOKEN_ID, 0, "999"), + |sc| { + sc.vote(proposal_id, VoteType::DownVote); + }, + ); + + // try cancel too early + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(SECOND_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.cancel(proposal_id); + }, + |r| { + r.assert_user_error("Action may not be cancelled"); + }, + ); + + current_block_nonce += VOTING_PERIOD_BLOCKS; + world.set_state_step(SetStateStep::new().block_nonce(current_block_nonce)); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(SECOND_USER_ADDRESS_EXPR).no_expect(), + |sc| { + sc.cancel(proposal_id); + }, + ); +} + +fn address_expr_to_address(address_expr: &str) -> Address { + AddressValue::from(address_expr).to_address() +} diff --git a/contracts/feature-tests/use-module/tests/staking_module_legacy_test.rs b/contracts/feature-tests/use-module/tests/staking_module_legacy_test.rs deleted file mode 100644 index 3d35ae4984..0000000000 --- a/contracts/feature-tests/use-module/tests/staking_module_legacy_test.rs +++ /dev/null @@ -1,266 +0,0 @@ -#![allow(deprecated)] // TODO: migrate tests - -use multiversx_sc::types::{EgldOrEsdtTokenIdentifier, ManagedVec}; -use multiversx_sc_modules::staking::StakingModule; -use multiversx_sc_scenario::{ - managed_address, managed_biguint, managed_token_id, rust_biguint, - testing_framework::BlockchainStateWrapper, -}; - -static STAKING_TOKEN_ID: &[u8] = b"STAKE-123456"; -const INITIAL_BALANCE: u64 = 2_000_000; -const REQUIRED_STAKE_AMOUNT: u64 = 1_000_000; -const SLASH_AMOUNT: u64 = 600_000; -const QUORUM: usize = 2; - -#[test] -fn staking_module_test() { - // setup accounts - let rust_zero = rust_biguint!(0); - let mut b_mock = BlockchainStateWrapper::new(); - let owner = b_mock.create_user_account(&rust_zero); - let alice = b_mock.create_user_account(&rust_zero); - let bob = b_mock.create_user_account(&rust_zero); - let carol = b_mock.create_user_account(&rust_zero); - let eve = b_mock.create_user_account(&rust_zero); - let staking_sc = b_mock.create_sc_account( - &rust_zero, - Some(&owner), - use_module::contract_obj, - "wasm path", - ); - - b_mock.set_esdt_balance(&alice, STAKING_TOKEN_ID, &rust_biguint!(INITIAL_BALANCE)); - b_mock.set_esdt_balance(&bob, STAKING_TOKEN_ID, &rust_biguint!(INITIAL_BALANCE)); - b_mock.set_esdt_balance(&carol, STAKING_TOKEN_ID, &rust_biguint!(INITIAL_BALANCE)); - b_mock.set_esdt_balance(&eve, STAKING_TOKEN_ID, &rust_biguint!(INITIAL_BALANCE)); - - // init module - b_mock - .execute_tx(&owner, &staking_sc, &rust_zero, |sc| { - let mut whitelist = ManagedVec::new(); - whitelist.push(managed_address!(&alice)); - whitelist.push(managed_address!(&bob)); - whitelist.push(managed_address!(&carol)); - - sc.init_staking_module( - &EgldOrEsdtTokenIdentifier::esdt(managed_token_id!(STAKING_TOKEN_ID)), - &managed_biguint!(REQUIRED_STAKE_AMOUNT), - &managed_biguint!(SLASH_AMOUNT), - QUORUM, - &whitelist, - ); - }) - .assert_ok(); - - // try stake - not a board member - b_mock - .execute_esdt_transfer( - &eve, - &staking_sc, - STAKING_TOKEN_ID, - 0, - &rust_biguint!(REQUIRED_STAKE_AMOUNT), - |sc| { - sc.stake(); - }, - ) - .assert_user_error("Only whitelisted members can stake"); - - // stake half and try unstake - b_mock - .execute_esdt_transfer( - &alice, - &staking_sc, - STAKING_TOKEN_ID, - 0, - &rust_biguint!(REQUIRED_STAKE_AMOUNT / 2), - |sc| { - sc.stake(); - }, - ) - .assert_ok(); - b_mock - .execute_tx(&alice, &staking_sc, &rust_zero, |sc| { - sc.unstake(managed_biguint!(REQUIRED_STAKE_AMOUNT / 4)); - }) - .assert_user_error("Not enough stake"); - - // bob and carol stake - b_mock - .execute_esdt_transfer( - &bob, - &staking_sc, - STAKING_TOKEN_ID, - 0, - &rust_biguint!(REQUIRED_STAKE_AMOUNT), - |sc| { - sc.stake(); - }, - ) - .assert_ok(); - b_mock - .execute_esdt_transfer( - &carol, - &staking_sc, - STAKING_TOKEN_ID, - 0, - &rust_biguint!(REQUIRED_STAKE_AMOUNT), - |sc| { - sc.stake(); - }, - ) - .assert_ok(); - - // try vote slash, not enough stake - b_mock - .execute_tx(&alice, &staking_sc, &rust_zero, |sc| { - sc.vote_slash_member(managed_address!(&bob)); - }) - .assert_user_error("Not enough stake"); - - // try vote slash, slashed address not a board member - b_mock - .execute_tx(&bob, &staking_sc, &rust_zero, |sc| { - sc.vote_slash_member(managed_address!(&eve)); - }) - .assert_user_error("Voted user is not a staked board member"); - - // alice stake over max amount and withdraw surplus - b_mock - .execute_esdt_transfer( - &alice, - &staking_sc, - STAKING_TOKEN_ID, - 0, - &rust_biguint!(REQUIRED_STAKE_AMOUNT), - |sc| { - sc.stake(); - - let alice_staked_amount = sc.staked_amount(&managed_address!(&alice)).get(); - assert_eq!(alice_staked_amount, managed_biguint!(1_500_000)); - }, - ) - .assert_ok(); - b_mock - .execute_tx(&alice, &staking_sc, &rust_zero, |sc| { - sc.unstake(managed_biguint!(500_000)); - - let alice_staked_amount = sc.staked_amount(&managed_address!(&alice)).get(); - assert_eq!(alice_staked_amount, managed_biguint!(1_000_000)); - }) - .assert_ok(); - b_mock.check_esdt_balance(&alice, STAKING_TOKEN_ID, &rust_biguint!(1_000_000)); - - // alice vote to slash bob - b_mock - .execute_tx(&alice, &staking_sc, &rust_zero, |sc| { - sc.vote_slash_member(managed_address!(&bob)); - - assert_eq!( - sc.slashing_proposal_voters(&managed_address!(&bob)).len(), - 1 - ); - assert!(sc - .slashing_proposal_voters(&managed_address!(&bob)) - .contains(&managed_address!(&alice))); - }) - .assert_ok(); - - // bob vote to slash alice - b_mock - .execute_tx(&bob, &staking_sc, &rust_zero, |sc| { - sc.vote_slash_member(managed_address!(&alice)); - }) - .assert_ok(); - - // try slash before quorum reached - b_mock - .execute_tx(&bob, &staking_sc, &rust_zero, |sc| { - sc.slash_member(managed_address!(&alice)); - }) - .assert_user_error("Quorum not reached"); - - // carol vote - b_mock - .execute_tx(&carol, &staking_sc, &rust_zero, |sc| { - sc.vote_slash_member(managed_address!(&alice)); - - assert_eq!( - sc.slashing_proposal_voters(&managed_address!(&alice)).len(), - 2 - ); - assert!(sc - .slashing_proposal_voters(&managed_address!(&alice)) - .contains(&managed_address!(&bob))); - assert!(sc - .slashing_proposal_voters(&managed_address!(&alice)) - .contains(&managed_address!(&carol))); - }) - .assert_ok(); - - // slash alice - b_mock - .execute_tx(&bob, &staking_sc, &rust_zero, |sc| { - sc.slash_member(managed_address!(&alice)); - - assert_eq!( - sc.staked_amount(&managed_address!(&alice)).get(), - managed_biguint!(REQUIRED_STAKE_AMOUNT - SLASH_AMOUNT) - ); - assert_eq!( - sc.total_slashed_amount().get(), - managed_biguint!(SLASH_AMOUNT) - ); - assert!(sc - .slashing_proposal_voters(&managed_address!(&alice)) - .is_empty()); - }) - .assert_ok(); - - // alice try vote after slash - b_mock - .execute_tx(&alice, &staking_sc, &rust_zero, |sc| { - sc.vote_slash_member(managed_address!(&bob)); - }) - .assert_user_error("Not enough stake"); - - // alice try unstake the remaining tokens - b_mock - .execute_tx(&alice, &staking_sc, &rust_zero, |sc| { - sc.unstake(managed_biguint!(400_000)); - }) - .assert_user_error("Not enough stake"); - - // alice remove from board members - b_mock - .execute_tx(&owner, &staking_sc, &rust_zero, |sc| { - // check alice's votes before slash - assert!(sc - .slashing_proposal_voters(&managed_address!(&bob)) - .contains(&managed_address!(&alice))); - - sc.remove_board_member(&managed_address!(&alice)); - - assert_eq!(sc.user_whitelist().len(), 2); - assert!(!sc.user_whitelist().contains(&managed_address!(&alice))); - - // alice's vote gets removed - assert!(sc - .slashing_proposal_voters(&managed_address!(&bob)) - .is_empty()); - }) - .assert_ok(); - - // alice unstake ok - b_mock - .execute_tx(&alice, &staking_sc, &rust_zero, |sc| { - sc.unstake(managed_biguint!(400_000)); - }) - .assert_ok(); - b_mock.check_esdt_balance( - &alice, - STAKING_TOKEN_ID, - &rust_biguint!(INITIAL_BALANCE - SLASH_AMOUNT), - ); -} diff --git a/contracts/feature-tests/use-module/tests/staking_module_whitebox_test.rs b/contracts/feature-tests/use-module/tests/staking_module_whitebox_test.rs new file mode 100644 index 0000000000..920701173e --- /dev/null +++ b/contracts/feature-tests/use-module/tests/staking_module_whitebox_test.rs @@ -0,0 +1,400 @@ +use multiversx_sc::types::{Address, EgldOrEsdtTokenIdentifier, ManagedVec}; +use multiversx_sc_modules::staking::StakingModule; +use multiversx_sc_scenario::{ + managed_address, managed_biguint, managed_token_id, + scenario_model::{ + Account, AddressValue, CheckAccount, CheckStateStep, ScCallStep, ScDeployStep, SetStateStep, + }, + ScenarioWorld, WhiteboxContract, +}; + +const STAKING_TOKEN_ID_EXPR: &str = "str:STAKE-123456"; +const STAKING_TOKEN_ID: &[u8] = b"STAKE-123456"; +const INITIAL_BALANCE: u64 = 2_000_000; +const REQUIRED_STAKE_AMOUNT: u64 = 1_000_000; +const SLASH_AMOUNT: u64 = 600_000; +const QUORUM: usize = 2; + +const OWNER_ADDRESS_EXPR: &str = "address:owner"; +const ALICE_ADDRESS_EXPR: &str = "address:alice"; +const BOB_ADDRESS_EXPR: &str = "address:bob"; +const CAROL_ADDRESS_EXPR: &str = "address:carol"; +const EVE_ADDRESS_EXPR: &str = "address:eve"; + +const USE_MODULE_ADDRESS_EXPR: &str = "sc:use-module"; +const USE_MODULE_PATH_EXPR: &str = "file:output/use-module.wasm"; + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.set_current_dir_from_workspace("contracts/features-tests/use-module"); + + blockchain.register_contract(USE_MODULE_PATH_EXPR, use_module::ContractBuilder); + blockchain +} + +#[test] +fn test_staking_module() { + let mut world = world(); + + world.set_state_step( + SetStateStep::new() + .put_account(OWNER_ADDRESS_EXPR, Account::new().nonce(1)) + .new_address(OWNER_ADDRESS_EXPR, 1, USE_MODULE_ADDRESS_EXPR) + .put_account( + ALICE_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(STAKING_TOKEN_ID_EXPR, INITIAL_BALANCE), + ) + .put_account( + BOB_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(STAKING_TOKEN_ID_EXPR, INITIAL_BALANCE), + ) + .put_account( + CAROL_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(STAKING_TOKEN_ID_EXPR, INITIAL_BALANCE), + ) + .put_account( + EVE_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(STAKING_TOKEN_ID_EXPR, INITIAL_BALANCE), + ), + ); + + // init + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + let use_module_code = world.code_expression(USE_MODULE_PATH_EXPR); + + world.whitebox_deploy( + &use_module_whitebox, + ScDeployStep::new() + .from(OWNER_ADDRESS_EXPR) + .code(use_module_code), + |sc| { + let mut whitelist = ManagedVec::new(); + whitelist.push(managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))); + whitelist.push(managed_address!(&address_expr_to_address(BOB_ADDRESS_EXPR))); + whitelist.push(managed_address!(&address_expr_to_address( + CAROL_ADDRESS_EXPR + ))); + + sc.init_staking_module( + &EgldOrEsdtTokenIdentifier::esdt(managed_token_id!(STAKING_TOKEN_ID)), + &managed_biguint!(REQUIRED_STAKE_AMOUNT), + &managed_biguint!(SLASH_AMOUNT), + QUORUM, + &whitelist, + ); + }, + ); + + // try stake - not a board member + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new() + .from(EVE_ADDRESS_EXPR) + .esdt_transfer(STAKING_TOKEN_ID, 0, REQUIRED_STAKE_AMOUNT) + .no_expect(), + |sc| sc.stake(), + |r| { + r.assert_user_error("Only whitelisted members can stake"); + }, + ); + + // stake half and try unstake + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR).esdt_transfer( + STAKING_TOKEN_ID, + 0, + REQUIRED_STAKE_AMOUNT / 2, + ), + |sc| sc.stake(), + ); + + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR).no_expect(), + |sc| sc.unstake(managed_biguint!(REQUIRED_STAKE_AMOUNT / 4)), + |r| { + r.assert_user_error("Not enough stake"); + }, + ); + + // bob and carol stake + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(BOB_ADDRESS_EXPR).esdt_transfer( + STAKING_TOKEN_ID, + 0, + REQUIRED_STAKE_AMOUNT, + ), + |sc| sc.stake(), + ); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(CAROL_ADDRESS_EXPR).esdt_transfer( + STAKING_TOKEN_ID, + 0, + REQUIRED_STAKE_AMOUNT, + ), + |sc| sc.stake(), + ); + + // try vote slash, not enough stake + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR).no_expect(), + |sc| sc.vote_slash_member(managed_address!(&address_expr_to_address(BOB_ADDRESS_EXPR))), + |r| { + r.assert_user_error("Not enough stake"); + }, + ); + + // try vote slash, slashed address not a board member + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR).no_expect(), + |sc| sc.vote_slash_member(managed_address!(&address_expr_to_address(EVE_ADDRESS_EXPR))), + |r| { + r.assert_user_error("Voted user is not a staked board member"); + }, + ); + + // alice stake over max amount and withdraw surplus + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR).esdt_transfer( + STAKING_TOKEN_ID, + 0, + REQUIRED_STAKE_AMOUNT, + ), + |sc| { + sc.stake(); + let alice_staked_amount = sc + .staked_amount(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))) + .get(); + assert_eq!(alice_staked_amount, managed_biguint!(1_500_000)); + }, + ); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR), + |sc| { + sc.unstake(managed_biguint!(500_000)); + + let alice_staked_amount = sc + .staked_amount(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))) + .get(); + assert_eq!(alice_staked_amount, managed_biguint!(1_000_000)); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + ALICE_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(STAKING_TOKEN_ID_EXPR, "1_000_000"), + )); + + // alice vote to slash bob + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR), + |sc| { + sc.vote_slash_member(managed_address!(&address_expr_to_address(BOB_ADDRESS_EXPR))); + + assert_eq!( + sc.slashing_proposal_voters(&managed_address!(&address_expr_to_address( + BOB_ADDRESS_EXPR + ))) + .len(), + 1 + ); + assert!(sc + .slashing_proposal_voters(&managed_address!(&address_expr_to_address( + BOB_ADDRESS_EXPR + ))) + .contains(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + )))); + }, + ); + + // bob vote to slash alice + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(BOB_ADDRESS_EXPR), + |sc| { + sc.vote_slash_member(managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))); + }, + ); + + // try slash before quorum reached + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(BOB_ADDRESS_EXPR).no_expect(), + |sc| { + sc.slash_member(managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))); + }, + |r| { + r.assert_user_error("Quorum not reached"); + }, + ); + + // carol vote + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(CAROL_ADDRESS_EXPR), + |sc| { + sc.vote_slash_member(managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))); + + assert_eq!( + sc.slashing_proposal_voters(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))) + .len(), + 2 + ); + assert!(sc + .slashing_proposal_voters(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))) + .contains(&managed_address!(&address_expr_to_address( + BOB_ADDRESS_EXPR + )))); + assert!(sc + .slashing_proposal_voters(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))) + .contains(&managed_address!(&address_expr_to_address( + CAROL_ADDRESS_EXPR + )))); + }, + ); + + // slash alice + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(BOB_ADDRESS_EXPR), + |sc| { + sc.slash_member(managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))); + + assert_eq!( + sc.staked_amount(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))) + .get(), + managed_biguint!(REQUIRED_STAKE_AMOUNT - SLASH_AMOUNT) + ); + assert_eq!( + sc.total_slashed_amount().get(), + managed_biguint!(SLASH_AMOUNT) + ); + assert!(sc + .slashing_proposal_voters(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))) + .is_empty()); + }, + ); + + // alice try vote after slash + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR).no_expect(), + |sc| { + sc.vote_slash_member(managed_address!(&address_expr_to_address(BOB_ADDRESS_EXPR))); + }, + |r| { + r.assert_user_error("Not enough stake"); + }, + ); + + // alice try unstake the remaining tokens + world.whitebox_call_check( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR).no_expect(), + |sc| { + sc.unstake(managed_biguint!(400_000)); + }, + |r| { + r.assert_user_error("Not enough stake"); + }, + ); + + // alice remove from board members + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| { + // check alice's votes before slash + assert!(sc + .slashing_proposal_voters(&managed_address!(&address_expr_to_address( + BOB_ADDRESS_EXPR + ))) + .contains(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + )))); + + sc.remove_board_member(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + ))); + + assert_eq!(sc.user_whitelist().len(), 2); + assert!(!sc + .user_whitelist() + .contains(&managed_address!(&address_expr_to_address( + ALICE_ADDRESS_EXPR + )))); + + // alice's vote gets removed + assert!(sc + .slashing_proposal_voters(&managed_address!(&address_expr_to_address( + BOB_ADDRESS_EXPR + ))) + .is_empty()); + }, + ); + + // alice unstake ok + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(ALICE_ADDRESS_EXPR), + |sc| { + sc.unstake(managed_biguint!(400_000)); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + ALICE_ADDRESS_EXPR, + CheckAccount::new().esdt_balance( + STAKING_TOKEN_ID_EXPR, + INITIAL_BALANCE - SLASH_AMOUNT, + ), + )); +} + +fn address_expr_to_address(address_expr: &str) -> Address { + AddressValue::from(address_expr).to_address() +} diff --git a/contracts/feature-tests/use-module/tests/token_merge_module_legacy_test.rs b/contracts/feature-tests/use-module/tests/token_merge_module_legacy_test.rs deleted file mode 100644 index a2520439ed..0000000000 --- a/contracts/feature-tests/use-module/tests/token_merge_module_legacy_test.rs +++ /dev/null @@ -1,726 +0,0 @@ -#![allow(deprecated)] // TODO: migrate tests - -use multiversx_sc::{ - arrayvec::ArrayVec, - codec::Empty, - contract_base::ContractBase, - storage::mappers::StorageTokenWrapper, - types::{EsdtLocalRole, EsdtTokenPayment, ManagedVec}, -}; -use multiversx_sc_modules::token_merge::{ - merged_token_instances::MergedTokenInstances, merged_token_setup::MergedTokenSetupModule, -}; -use multiversx_sc_scenario::{ - managed_address, managed_biguint, managed_token_id, rust_biguint, - testing_framework::{BlockchainStateWrapper, TxTokenTransfer}, -}; -use use_module::token_merge_mod_impl::{CustomAttributes, TokenMergeModImpl}; - -static MERGED_TOKEN_ID: &[u8] = b"MERGED-123456"; -static NFT_TOKEN_ID: &[u8] = b"NFT-123456"; -static FUNGIBLE_TOKEN_ID: &[u8] = b"FUN-123456"; - -const NFT_AMOUNT: u64 = 1; -const FUNGIBLE_AMOUNT: u64 = 100; - -const FIRST_NFT_NONCE: u64 = 5; -static FIRST_ATTRIBUTES: &[u8] = b"FirstAttributes"; -const FIRST_ROYALTIES: u64 = 1_000; -static FIRST_URIS: &[&[u8]] = &[b"FirstUri", b"SecondUri"]; - -const SECOND_NFT_NONCE: u64 = 7; -static SECOND_ATTRIBUTES: &[u8] = b"SecondAttributes"; -const SECOND_ROYALTIES: u64 = 5_000; -static SECOND_URIS: &[&[u8]] = &[b"cool.com/safe_file.exe"]; - -#[test] -fn test_token_merge() { - let rust_zero = rust_biguint!(0); - let mut b_mock = BlockchainStateWrapper::new(); - let owner = b_mock.create_user_account(&rust_zero); - let user = b_mock.create_user_account(&rust_zero); - let merging_sc = b_mock.create_sc_account( - &rust_zero, - Some(&owner), - use_module::contract_obj, - "wasm path", - ); - - b_mock - .execute_tx(&owner, &merging_sc, &rust_zero, |sc| { - sc.merged_token() - .set_token_id(managed_token_id!(MERGED_TOKEN_ID)); - let _ = sc - .mergeable_tokens_whitelist() - .insert(managed_token_id!(NFT_TOKEN_ID)); - let _ = sc - .mergeable_tokens_whitelist() - .insert(managed_token_id!(FUNGIBLE_TOKEN_ID)); - }) - .assert_ok(); - b_mock.set_esdt_local_roles( - merging_sc.address_ref(), - MERGED_TOKEN_ID, - &[EsdtLocalRole::NftCreate, EsdtLocalRole::NftBurn], - ); - - b_mock.set_esdt_balance(&user, FUNGIBLE_TOKEN_ID, &rust_biguint!(FUNGIBLE_AMOUNT)); - b_mock.set_nft_balance_all_properties( - &user, - NFT_TOKEN_ID, - FIRST_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - &FIRST_ATTRIBUTES.to_vec(), - FIRST_ROYALTIES, - None, - None, - None, - &uris_to_vec(FIRST_URIS), - ); - b_mock.set_nft_balance_all_properties( - &user, - NFT_TOKEN_ID, - SECOND_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - &SECOND_ATTRIBUTES.to_vec(), - SECOND_ROYALTIES, - None, - None, - None, - &uris_to_vec(SECOND_URIS), - ); - - // merge two NFTs - let nft_transfers = vec![ - TxTokenTransfer { - token_identifier: NFT_TOKEN_ID.to_vec(), - nonce: FIRST_NFT_NONCE, - value: rust_biguint!(NFT_AMOUNT), - }, - TxTokenTransfer { - token_identifier: NFT_TOKEN_ID.to_vec(), - nonce: SECOND_NFT_NONCE, - value: rust_biguint!(NFT_AMOUNT), - }, - ]; - b_mock - .execute_esdt_multi_transfer(&user, &merging_sc, &nft_transfers, |sc| { - let merged_token = sc.merge_tokens_endpoint(); - assert_eq!( - merged_token.token_identifier, - managed_token_id!(MERGED_TOKEN_ID) - ); - assert_eq!(merged_token.token_nonce, 1); - assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); - - let merged_token_data = sc.blockchain().get_esdt_token_data( - &managed_address!(&user), - &managed_token_id!(MERGED_TOKEN_ID), - 1, - ); - let mut expected_uri = ArrayVec::new(); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - SECOND_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - - let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); - assert_eq!(expected_uri, actual_uri.into_instances()); - - assert_eq!( - merged_token_data.royalties, - managed_biguint!(SECOND_ROYALTIES) - ); - }) - .assert_ok(); - - b_mock.check_nft_balance( - &user, - MERGED_TOKEN_ID, - 1, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - b_mock.check_nft_balance( - merging_sc.address_ref(), - NFT_TOKEN_ID, - FIRST_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - b_mock.check_nft_balance( - merging_sc.address_ref(), - NFT_TOKEN_ID, - SECOND_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - - // split nfts - b_mock - .execute_esdt_transfer( - &user, - &merging_sc, - MERGED_TOKEN_ID, - 1, - &rust_biguint!(NFT_AMOUNT), - |sc| { - let output_tokens = sc.split_tokens_endpoint(); - let mut expected_output_tokens = ManagedVec::new(); - expected_output_tokens.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_output_tokens.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - SECOND_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - assert_eq!(output_tokens, expected_output_tokens); - }, - ) - .assert_ok(); - - b_mock.check_nft_balance( - &user, - NFT_TOKEN_ID, - FIRST_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - b_mock.check_nft_balance( - &user, - NFT_TOKEN_ID, - SECOND_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - - // merge the NFT with fungible - let esdt_transfers = vec![ - TxTokenTransfer { - token_identifier: NFT_TOKEN_ID.to_vec(), - nonce: FIRST_NFT_NONCE, - value: rust_biguint!(NFT_AMOUNT), - }, - TxTokenTransfer { - token_identifier: FUNGIBLE_TOKEN_ID.to_vec(), - nonce: 0, - value: rust_biguint!(FUNGIBLE_AMOUNT), - }, - ]; - b_mock - .execute_esdt_multi_transfer(&user, &merging_sc, &esdt_transfers, |sc| { - let merged_token = sc.merge_tokens_endpoint(); - assert_eq!( - merged_token.token_identifier, - managed_token_id!(MERGED_TOKEN_ID) - ); - assert_eq!(merged_token.token_nonce, 2); - assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); - - let merged_token_data = sc.blockchain().get_esdt_token_data( - &managed_address!(&user), - &managed_token_id!(MERGED_TOKEN_ID), - 2, - ); - let mut expected_uri = ArrayVec::new(); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(FUNGIBLE_TOKEN_ID), - 0, - managed_biguint!(FUNGIBLE_AMOUNT), - )); - - let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); - assert_eq!(expected_uri, actual_uri.into_instances()); - - assert_eq!( - merged_token_data.royalties, - managed_biguint!(FIRST_ROYALTIES) - ); - }) - .assert_ok(); - - b_mock.check_nft_balance( - &user, - MERGED_TOKEN_ID, - 2, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - - // merge NFT with an already merged token - let combined_transfers = vec![ - TxTokenTransfer { - token_identifier: NFT_TOKEN_ID.to_vec(), - nonce: SECOND_NFT_NONCE, - value: rust_biguint!(NFT_AMOUNT), - }, - TxTokenTransfer { - token_identifier: MERGED_TOKEN_ID.to_vec(), - nonce: 2, - value: rust_biguint!(NFT_AMOUNT), - }, - ]; - b_mock - .execute_esdt_multi_transfer(&user, &merging_sc, &combined_transfers, |sc| { - let merged_token = sc.merge_tokens_endpoint(); - assert_eq!( - merged_token.token_identifier, - managed_token_id!(MERGED_TOKEN_ID) - ); - assert_eq!(merged_token.token_nonce, 3); - assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); - - let merged_token_data = sc.blockchain().get_esdt_token_data( - &managed_address!(&user), - &managed_token_id!(MERGED_TOKEN_ID), - 3, - ); - let mut expected_uri = ArrayVec::new(); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(FUNGIBLE_TOKEN_ID), - 0, - managed_biguint!(FUNGIBLE_AMOUNT), - )); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - SECOND_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - - let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); - assert_eq!(expected_uri, actual_uri.into_instances()); - - assert_eq!( - merged_token_data.royalties, - managed_biguint!(SECOND_ROYALTIES) - ); - }) - .assert_ok(); - - b_mock.check_nft_balance( - &user, - MERGED_TOKEN_ID, - 3, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - - // split the 3 merged tokens - b_mock - .execute_esdt_transfer( - &user, - &merging_sc, - MERGED_TOKEN_ID, - 3, - &rust_biguint!(NFT_AMOUNT), - |sc| { - let output_tokens = sc.split_tokens_endpoint(); - let mut expected_output_tokens = ManagedVec::new(); - expected_output_tokens.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_output_tokens.push(EsdtTokenPayment::new( - managed_token_id!(FUNGIBLE_TOKEN_ID), - 0, - managed_biguint!(FUNGIBLE_AMOUNT), - )); - expected_output_tokens.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - SECOND_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - - assert_eq!(output_tokens, expected_output_tokens); - }, - ) - .assert_ok(); - - b_mock.check_nft_balance( - &user, - NFT_TOKEN_ID, - FIRST_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - b_mock.check_nft_balance( - &user, - NFT_TOKEN_ID, - SECOND_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - b_mock.check_esdt_balance(&user, FUNGIBLE_TOKEN_ID, &rust_biguint!(FUNGIBLE_AMOUNT)); -} - -#[test] -fn partial_split_test() { - let rust_zero = rust_biguint!(0); - let mut b_mock = BlockchainStateWrapper::new(); - let owner = b_mock.create_user_account(&rust_zero); - let user = b_mock.create_user_account(&rust_zero); - let merging_sc = b_mock.create_sc_account( - &rust_zero, - Some(&owner), - use_module::contract_obj, - "wasm path", - ); - - b_mock - .execute_tx(&owner, &merging_sc, &rust_zero, |sc| { - sc.merged_token() - .set_token_id(managed_token_id!(MERGED_TOKEN_ID)); - let _ = sc - .mergeable_tokens_whitelist() - .insert(managed_token_id!(NFT_TOKEN_ID)); - let _ = sc - .mergeable_tokens_whitelist() - .insert(managed_token_id!(FUNGIBLE_TOKEN_ID)); - }) - .assert_ok(); - b_mock.set_esdt_local_roles( - merging_sc.address_ref(), - MERGED_TOKEN_ID, - &[EsdtLocalRole::NftCreate, EsdtLocalRole::NftBurn], - ); - - b_mock.set_esdt_balance(&user, FUNGIBLE_TOKEN_ID, &rust_biguint!(FUNGIBLE_AMOUNT)); - b_mock.set_nft_balance_all_properties( - &user, - NFT_TOKEN_ID, - FIRST_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - &FIRST_ATTRIBUTES.to_vec(), - FIRST_ROYALTIES, - None, - None, - None, - &uris_to_vec(FIRST_URIS), - ); - b_mock.set_nft_balance_all_properties( - &user, - NFT_TOKEN_ID, - SECOND_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - &SECOND_ATTRIBUTES.to_vec(), - SECOND_ROYALTIES, - None, - None, - None, - &uris_to_vec(SECOND_URIS), - ); - - // merge 2 NFTs and a fungible token - let esdt_transfers = vec![ - TxTokenTransfer { - token_identifier: NFT_TOKEN_ID.to_vec(), - nonce: FIRST_NFT_NONCE, - value: rust_biguint!(NFT_AMOUNT), - }, - TxTokenTransfer { - token_identifier: NFT_TOKEN_ID.to_vec(), - nonce: SECOND_NFT_NONCE, - value: rust_biguint!(NFT_AMOUNT), - }, - TxTokenTransfer { - token_identifier: FUNGIBLE_TOKEN_ID.to_vec(), - nonce: 0, - value: rust_biguint!(FUNGIBLE_AMOUNT), - }, - ]; - b_mock - .execute_esdt_multi_transfer(&user, &merging_sc, &esdt_transfers, |sc| { - let merged_token = sc.merge_tokens_endpoint(); - assert_eq!( - merged_token.token_identifier, - managed_token_id!(MERGED_TOKEN_ID) - ); - assert_eq!(merged_token.token_nonce, 1); - assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); - - let merged_token_data = sc.blockchain().get_esdt_token_data( - &managed_address!(&user), - &managed_token_id!(MERGED_TOKEN_ID), - 1, - ); - let mut expected_uri = ArrayVec::new(); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - SECOND_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(FUNGIBLE_TOKEN_ID), - 0, - managed_biguint!(FUNGIBLE_AMOUNT), - )); - - let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); - assert_eq!(expected_uri, actual_uri.into_instances()); - }) - .assert_ok(); - - // split part of the fungible token - b_mock - .execute_esdt_transfer( - &user, - &merging_sc, - MERGED_TOKEN_ID, - 1, - &rust_biguint!(NFT_AMOUNT), - |sc| { - let mut tokens_to_remove = ManagedVec::new(); - tokens_to_remove.push(EsdtTokenPayment::new( - managed_token_id!(FUNGIBLE_TOKEN_ID), - 0, - managed_biguint!(40), - )); - let output_payments = sc.split_token_partial_endpoint(tokens_to_remove); - - let mut expected_output_payments = ManagedVec::new(); - expected_output_payments.push(EsdtTokenPayment::new( - managed_token_id!(FUNGIBLE_TOKEN_ID), - 0, - managed_biguint!(40), - )); - expected_output_payments.push(EsdtTokenPayment::new( - managed_token_id!(MERGED_TOKEN_ID), - 2, - managed_biguint!(NFT_AMOUNT), - )); - assert_eq!(output_payments, expected_output_payments); - }, - ) - .assert_ok(); - - // fully remove instance - b_mock - .execute_esdt_transfer( - &user, - &merging_sc, - MERGED_TOKEN_ID, - 2, - &rust_biguint!(NFT_AMOUNT), - |sc| { - let mut tokens_to_remove = ManagedVec::new(); - tokens_to_remove.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - let output_payments = sc.split_token_partial_endpoint(tokens_to_remove); - - let mut expected_output_payments = ManagedVec::new(); - expected_output_payments.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_output_payments.push(EsdtTokenPayment::new( - managed_token_id!(MERGED_TOKEN_ID), - 3, - managed_biguint!(NFT_AMOUNT), - )); - assert_eq!(output_payments, expected_output_payments); - - // check newest token attributes - let merged_token_data = sc.blockchain().get_esdt_token_data( - &managed_address!(&user), - &managed_token_id!(MERGED_TOKEN_ID), - 3, - ); - let mut expected_uri = ArrayVec::new(); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - SECOND_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(FUNGIBLE_TOKEN_ID), - 0, - managed_biguint!(FUNGIBLE_AMOUNT - 40), - )); - - let actual_uri = - MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); - assert_eq!(expected_uri, actual_uri.into_instances()); - - assert_eq!( - merged_token_data.royalties, - managed_biguint!(SECOND_ROYALTIES) - ); - }, - ) - .assert_ok(); -} - -#[test] -fn custom_attributes_test() { - let rust_zero = rust_biguint!(0); - let mut b_mock = BlockchainStateWrapper::new(); - let owner = b_mock.create_user_account(&rust_zero); - let user = b_mock.create_user_account(&rust_zero); - let merging_sc = b_mock.create_sc_account( - &rust_zero, - Some(&owner), - use_module::contract_obj, - "wasm path", - ); - - b_mock - .execute_tx(&owner, &merging_sc, &rust_zero, |sc| { - sc.merged_token() - .set_token_id(managed_token_id!(MERGED_TOKEN_ID)); - let _ = sc - .mergeable_tokens_whitelist() - .insert(managed_token_id!(NFT_TOKEN_ID)); - let _ = sc - .mergeable_tokens_whitelist() - .insert(managed_token_id!(FUNGIBLE_TOKEN_ID)); - }) - .assert_ok(); - b_mock.set_esdt_local_roles( - merging_sc.address_ref(), - MERGED_TOKEN_ID, - &[EsdtLocalRole::NftCreate, EsdtLocalRole::NftBurn], - ); - - b_mock.set_esdt_balance(&user, FUNGIBLE_TOKEN_ID, &rust_biguint!(FUNGIBLE_AMOUNT)); - b_mock.set_nft_balance_all_properties( - &user, - NFT_TOKEN_ID, - FIRST_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - &FIRST_ATTRIBUTES.to_vec(), - FIRST_ROYALTIES, - None, - None, - None, - &uris_to_vec(FIRST_URIS), - ); - b_mock.set_nft_balance_all_properties( - &user, - NFT_TOKEN_ID, - SECOND_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - &SECOND_ATTRIBUTES.to_vec(), - SECOND_ROYALTIES, - None, - None, - None, - &uris_to_vec(SECOND_URIS), - ); - - // merge two NFTs - let nft_transfers = vec![ - TxTokenTransfer { - token_identifier: NFT_TOKEN_ID.to_vec(), - nonce: FIRST_NFT_NONCE, - value: rust_biguint!(NFT_AMOUNT), - }, - TxTokenTransfer { - token_identifier: NFT_TOKEN_ID.to_vec(), - nonce: SECOND_NFT_NONCE, - value: rust_biguint!(NFT_AMOUNT), - }, - ]; - b_mock - .execute_esdt_multi_transfer(&user, &merging_sc, &nft_transfers, |sc| { - let merged_token = sc.merge_tokens_custom_attributes_endpoint(); - assert_eq!( - merged_token.token_identifier, - managed_token_id!(MERGED_TOKEN_ID) - ); - assert_eq!(merged_token.token_nonce, 1); - assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); - - let merged_token_data = sc.blockchain().get_esdt_token_data( - &managed_address!(&user), - &managed_token_id!(MERGED_TOKEN_ID), - 1, - ); - let mut expected_uri = ArrayVec::new(); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - FIRST_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - expected_uri.push(EsdtTokenPayment::new( - managed_token_id!(NFT_TOKEN_ID), - SECOND_NFT_NONCE, - managed_biguint!(NFT_AMOUNT), - )); - - let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); - assert_eq!(expected_uri, actual_uri.into_instances()); - - let expected_attributes = CustomAttributes { - first: 5u32, - second: 10u64, - }; - let actual_attributes: CustomAttributes = merged_token_data.decode_attributes(); - assert_eq!(expected_attributes, actual_attributes); - - assert_eq!( - merged_token_data.royalties, - managed_biguint!(SECOND_ROYALTIES) - ); - }) - .assert_ok(); - - b_mock.check_nft_balance( - &user, - MERGED_TOKEN_ID, - 1, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - b_mock.check_nft_balance( - merging_sc.address_ref(), - NFT_TOKEN_ID, - FIRST_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); - b_mock.check_nft_balance( - merging_sc.address_ref(), - NFT_TOKEN_ID, - SECOND_NFT_NONCE, - &rust_biguint!(NFT_AMOUNT), - Option::<&Empty>::None, - ); -} - -fn uris_to_vec(uris: &[&[u8]]) -> Vec> { - let mut out = Vec::new(); - for uri in uris { - out.push((*uri).to_vec()); - } - - out -} diff --git a/contracts/feature-tests/use-module/tests/token_merge_module_whitebox_test.rs b/contracts/feature-tests/use-module/tests/token_merge_module_whitebox_test.rs new file mode 100644 index 0000000000..a4a0666d08 --- /dev/null +++ b/contracts/feature-tests/use-module/tests/token_merge_module_whitebox_test.rs @@ -0,0 +1,823 @@ +use multiversx_sc::{ + arrayvec::ArrayVec, + codec::{test_util::top_encode_to_vec_u8_or_panic, Empty}, + contract_base::ContractBase, + storage::mappers::StorageTokenWrapper, + types::{Address, EsdtTokenPayment, ManagedVec}, +}; +use multiversx_sc_modules::token_merge::{ + merged_token_instances::MergedTokenInstances, merged_token_setup::MergedTokenSetupModule, +}; +use multiversx_sc_scenario::{ + managed_address, managed_biguint, managed_token_id, + scenario_model::{ + Account, AddressValue, CheckAccount, CheckStateStep, ScCallStep, SetStateStep, TxESDT, + }, + ScenarioWorld, WhiteboxContract, +}; +use use_module::token_merge_mod_impl::{CustomAttributes, TokenMergeModImpl}; + +const OWNER_ADDRESS_EXPR: &str = "address:owner"; +const USER_ADDRESS_EXPR: &str = "address:user"; + +const USE_MODULE_ADDRESS_EXPR: &str = "sc:use-module"; +const USE_MODULE_PATH_EXPR: &str = "file:output/use-module.wasm"; + +const MERGED_TOKEN_ID_EXPR: &str = "str:MERGED-123456"; +const MERGED_TOKEN_ID: &[u8] = b"MERGED-123456"; +const NFT_TOKEN_ID_EXPR: &str = "str:NFT-123456"; +const NFT_TOKEN_ID: &[u8] = b"NFT-123456"; +const FUNGIBLE_TOKEN_ID_EXPR: &str = "str:FUN-123456"; +const FUNGIBLE_TOKEN_ID: &[u8] = b"FUN-123456"; + +const NFT_AMOUNT: u64 = 1; +const FUNGIBLE_AMOUNT: u64 = 100; + +const FIRST_NFT_NONCE: u64 = 5; +const FIRST_ATTRIBUTES: &[u8] = b"FirstAttributes"; +const FIRST_ROYALTIES: u64 = 1_000; +const FIRST_URIS: &[&[u8]] = &[b"FirstUri", b"SecondUri"]; + +const SECOND_NFT_NONCE: u64 = 7; +const SECOND_ATTRIBUTES: &[u8] = b"SecondAttributes"; +const SECOND_ROYALTIES: u64 = 5_000; +const SECOND_URIS: &[&[u8]] = &[b"cool.com/safe_file.exe"]; + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.set_current_dir_from_workspace("contracts/features-tests/use-module"); + + blockchain.register_contract(USE_MODULE_PATH_EXPR, use_module::ContractBuilder); + blockchain +} + +#[test] +fn test_token_merge() { + let mut world = world(); + + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + let use_module_code = world.code_expression(USE_MODULE_PATH_EXPR); + + let roles = vec![ + "ESDTRoleNFTCreate".to_string(), + "ESDTRoleNFTBurn".to_string(), + ]; + + world.set_state_step( + SetStateStep::new() + .put_account(OWNER_ADDRESS_EXPR, Account::new().nonce(1)) + .put_account( + USER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(FUNGIBLE_TOKEN_ID_EXPR, FUNGIBLE_AMOUNT) + .esdt_nft_all_properties( + NFT_TOKEN_ID_EXPR, + FIRST_NFT_NONCE, + NFT_AMOUNT, + Some(FIRST_ATTRIBUTES), + FIRST_ROYALTIES, + None, + None, + Vec::from(FIRST_URIS), + ) + .esdt_nft_all_properties( + NFT_TOKEN_ID_EXPR, + SECOND_NFT_NONCE, + NFT_AMOUNT, + Some(SECOND_ATTRIBUTES), + SECOND_ROYALTIES, + None, + None, + Vec::from(SECOND_URIS), + ), + ) + .put_account( + USE_MODULE_ADDRESS_EXPR, + Account::new() + .nonce(1) + .code(use_module_code) + .owner(OWNER_ADDRESS_EXPR) + .esdt_roles(MERGED_TOKEN_ID_EXPR, roles), + ), + ); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| { + sc.merged_token() + .set_token_id(managed_token_id!(MERGED_TOKEN_ID)); + let _ = sc + .mergeable_tokens_whitelist() + .insert(managed_token_id!(NFT_TOKEN_ID)); + let _ = sc + .mergeable_tokens_whitelist() + .insert(managed_token_id!(FUNGIBLE_TOKEN_ID)); + }, + ); + + // merge two NFTs + let nft_transfers = vec![ + TxESDT { + esdt_token_identifier: NFT_TOKEN_ID.into(), + nonce: FIRST_NFT_NONCE.into(), + esdt_value: NFT_AMOUNT.into(), + }, + TxESDT { + esdt_token_identifier: NFT_TOKEN_ID.into(), + nonce: SECOND_NFT_NONCE.into(), + esdt_value: NFT_AMOUNT.into(), + }, + ]; + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(USER_ADDRESS_EXPR) + .multi_esdt_transfer(nft_transfers.clone()), + |sc| { + let merged_token = sc.merge_tokens_endpoint(); + assert_eq!( + merged_token.token_identifier, + managed_token_id!(MERGED_TOKEN_ID) + ); + assert_eq!(merged_token.token_nonce, 1); + assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); + + let merged_token_data = sc.blockchain().get_esdt_token_data( + &managed_address!(&address_expr_to_address(USER_ADDRESS_EXPR)), + &managed_token_id!(MERGED_TOKEN_ID), + 1, + ); + let mut expected_uri = ArrayVec::new(); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + SECOND_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + + let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); + assert_eq!(expected_uri, actual_uri.into_instances()); + + assert_eq!( + merged_token_data.royalties, + managed_biguint!(SECOND_ROYALTIES) + ); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + MERGED_TOKEN_ID_EXPR, + 1, + NFT_AMOUNT, + Option::<&Empty>::None, + ), + )); + + world.check_state_step(CheckStateStep::new().put_account( + USE_MODULE_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + FIRST_NFT_NONCE, + NFT_AMOUNT, + Some(FIRST_ATTRIBUTES), + ), + )); + + world.check_state_step(CheckStateStep::new().put_account( + USE_MODULE_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + SECOND_NFT_NONCE, + NFT_AMOUNT, + Some(SECOND_ATTRIBUTES), + ), + )); + + // split nfts + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(USER_ADDRESS_EXPR).esdt_transfer( + MERGED_TOKEN_ID_EXPR, + 1, + NFT_AMOUNT, + ), + |sc| { + let output_tokens = sc.split_tokens_endpoint(); + let mut expected_output_tokens = ManagedVec::new(); + expected_output_tokens.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_output_tokens.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + SECOND_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + assert_eq!(output_tokens, expected_output_tokens); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + FIRST_NFT_NONCE, + NFT_AMOUNT, + Some(FIRST_ATTRIBUTES), + ), + )); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + SECOND_NFT_NONCE, + NFT_AMOUNT, + Some(SECOND_ATTRIBUTES), + ), + )); + + // merge the NFT with fungible + let esdt_transfers = vec![ + TxESDT { + esdt_token_identifier: NFT_TOKEN_ID.into(), + nonce: FIRST_NFT_NONCE.into(), + esdt_value: NFT_AMOUNT.into(), + }, + TxESDT { + esdt_token_identifier: FUNGIBLE_TOKEN_ID.into(), + nonce: 0u64.into(), + esdt_value: FUNGIBLE_AMOUNT.into(), + }, + ]; + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(USER_ADDRESS_EXPR) + .multi_esdt_transfer(esdt_transfers.clone()), + |sc| { + let merged_token = sc.merge_tokens_endpoint(); + assert_eq!( + merged_token.token_identifier, + managed_token_id!(MERGED_TOKEN_ID) + ); + assert_eq!(merged_token.token_nonce, 2); + assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); + + let merged_token_data = sc.blockchain().get_esdt_token_data( + &managed_address!(&address_expr_to_address(USER_ADDRESS_EXPR)), + &managed_token_id!(MERGED_TOKEN_ID), + 2, + ); + let mut expected_uri = ArrayVec::new(); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(FUNGIBLE_TOKEN_ID), + 0, + managed_biguint!(FUNGIBLE_AMOUNT), + )); + + let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); + assert_eq!(expected_uri, actual_uri.into_instances()); + + assert_eq!( + merged_token_data.royalties, + managed_biguint!(FIRST_ROYALTIES) + ); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + MERGED_TOKEN_ID_EXPR, + 2, + NFT_AMOUNT, + Option::<&Empty>::None, + ), + )); + + // merge NFT with an already merged token + let combined_transfers = vec![ + TxESDT { + esdt_token_identifier: NFT_TOKEN_ID.into(), + nonce: SECOND_NFT_NONCE.into(), + esdt_value: NFT_AMOUNT.into(), + }, + TxESDT { + esdt_token_identifier: MERGED_TOKEN_ID.into(), + nonce: 2u64.into(), + esdt_value: NFT_AMOUNT.into(), + }, + ]; + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(USER_ADDRESS_EXPR) + .multi_esdt_transfer(combined_transfers.clone()), + |sc| { + let merged_token = sc.merge_tokens_endpoint(); + assert_eq!( + merged_token.token_identifier, + managed_token_id!(MERGED_TOKEN_ID) + ); + assert_eq!(merged_token.token_nonce, 3); + assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); + + let merged_token_data = sc.blockchain().get_esdt_token_data( + &managed_address!(&address_expr_to_address(USER_ADDRESS_EXPR)), + &managed_token_id!(MERGED_TOKEN_ID), + 3, + ); + let mut expected_uri = ArrayVec::new(); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(FUNGIBLE_TOKEN_ID), + 0, + managed_biguint!(FUNGIBLE_AMOUNT), + )); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + SECOND_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + + let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); + assert_eq!(expected_uri, actual_uri.into_instances()); + + assert_eq!( + merged_token_data.royalties, + managed_biguint!(SECOND_ROYALTIES) + ); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + MERGED_TOKEN_ID_EXPR, + 3, + NFT_AMOUNT, + Option::<&Empty>::None, + ), + )); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(USER_ADDRESS_EXPR).esdt_transfer( + MERGED_TOKEN_ID_EXPR, + 3, + NFT_AMOUNT, + ), + |sc| { + let output_tokens = sc.split_tokens_endpoint(); + let mut expected_output_tokens = ManagedVec::new(); + expected_output_tokens.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_output_tokens.push(EsdtTokenPayment::new( + managed_token_id!(FUNGIBLE_TOKEN_ID), + 0, + managed_biguint!(FUNGIBLE_AMOUNT), + )); + expected_output_tokens.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + SECOND_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + + assert_eq!(output_tokens, expected_output_tokens); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + FIRST_NFT_NONCE, + NFT_AMOUNT, + Some(FIRST_ATTRIBUTES), + ), + )); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + SECOND_NFT_NONCE, + NFT_AMOUNT, + Some(SECOND_ATTRIBUTES), + ), + )); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_balance(FUNGIBLE_TOKEN_ID_EXPR, FUNGIBLE_AMOUNT), + )); +} + +#[test] +fn test_partial_split() { + let mut world = world(); + + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + let use_module_code = world.code_expression(USE_MODULE_PATH_EXPR); + + let roles = vec![ + "ESDTRoleNFTCreate".to_string(), + "ESDTRoleNFTBurn".to_string(), + ]; + + world.set_state_step( + SetStateStep::new() + .put_account(OWNER_ADDRESS_EXPR, Account::new().nonce(1)) + .put_account( + USER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(FUNGIBLE_TOKEN_ID_EXPR, FUNGIBLE_AMOUNT) + .esdt_nft_all_properties( + NFT_TOKEN_ID_EXPR, + FIRST_NFT_NONCE, + NFT_AMOUNT, + Some(FIRST_ATTRIBUTES), + FIRST_ROYALTIES, + None, + None, + Vec::from(FIRST_URIS), + ) + .esdt_nft_all_properties( + NFT_TOKEN_ID_EXPR, + SECOND_NFT_NONCE, + NFT_AMOUNT, + Some(SECOND_ATTRIBUTES), + SECOND_ROYALTIES, + None, + None, + Vec::from(SECOND_URIS), + ), + ) + .put_account( + USE_MODULE_ADDRESS_EXPR, + Account::new() + .nonce(1) + .code(use_module_code) + .owner(OWNER_ADDRESS_EXPR) + .esdt_roles(MERGED_TOKEN_ID_EXPR, roles), + ), + ); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| { + sc.merged_token() + .set_token_id(managed_token_id!(MERGED_TOKEN_ID)); + let _ = sc + .mergeable_tokens_whitelist() + .insert(managed_token_id!(NFT_TOKEN_ID)); + let _ = sc + .mergeable_tokens_whitelist() + .insert(managed_token_id!(FUNGIBLE_TOKEN_ID)); + }, + ); + + // merge 2 NFTs and a fungible token + let esdt_transfers = vec![ + TxESDT { + esdt_token_identifier: NFT_TOKEN_ID.into(), + nonce: FIRST_NFT_NONCE.into(), + esdt_value: NFT_AMOUNT.into(), + }, + TxESDT { + esdt_token_identifier: NFT_TOKEN_ID.into(), + nonce: SECOND_NFT_NONCE.into(), + esdt_value: NFT_AMOUNT.into(), + }, + TxESDT { + esdt_token_identifier: FUNGIBLE_TOKEN_ID.into(), + nonce: 0u64.into(), + esdt_value: FUNGIBLE_AMOUNT.into(), + }, + ]; + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(USER_ADDRESS_EXPR) + .multi_esdt_transfer(esdt_transfers.clone()), + |sc| { + let merged_token = sc.merge_tokens_endpoint(); + assert_eq!( + merged_token.token_identifier, + managed_token_id!(MERGED_TOKEN_ID) + ); + assert_eq!(merged_token.token_nonce, 1); + assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); + + let merged_token_data = sc.blockchain().get_esdt_token_data( + &managed_address!(&address_expr_to_address(USER_ADDRESS_EXPR)), + &managed_token_id!(MERGED_TOKEN_ID), + 1, + ); + let mut expected_uri = ArrayVec::new(); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + SECOND_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(FUNGIBLE_TOKEN_ID), + 0, + managed_biguint!(FUNGIBLE_AMOUNT), + )); + + let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); + assert_eq!(expected_uri, actual_uri.into_instances()); + }, + ); + + // split part of the fungible token + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(USER_ADDRESS_EXPR).esdt_transfer( + MERGED_TOKEN_ID_EXPR, + 1, + NFT_AMOUNT, + ), + |sc| { + let mut tokens_to_remove = ManagedVec::new(); + tokens_to_remove.push(EsdtTokenPayment::new( + managed_token_id!(FUNGIBLE_TOKEN_ID), + 0, + managed_biguint!(40), + )); + let output_payments = sc.split_token_partial_endpoint(tokens_to_remove); + + let mut expected_output_payments = ManagedVec::new(); + expected_output_payments.push(EsdtTokenPayment::new( + managed_token_id!(FUNGIBLE_TOKEN_ID), + 0, + managed_biguint!(40), + )); + expected_output_payments.push(EsdtTokenPayment::new( + managed_token_id!(MERGED_TOKEN_ID), + 2, + managed_biguint!(NFT_AMOUNT), + )); + assert_eq!(output_payments, expected_output_payments); + }, + ); + + // fully remove instance + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(USER_ADDRESS_EXPR).esdt_transfer( + MERGED_TOKEN_ID_EXPR, + 2, + NFT_AMOUNT, + ), + |sc| { + let mut tokens_to_remove = ManagedVec::new(); + tokens_to_remove.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + let output_payments = sc.split_token_partial_endpoint(tokens_to_remove); + + let mut expected_output_payments = ManagedVec::new(); + expected_output_payments.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_output_payments.push(EsdtTokenPayment::new( + managed_token_id!(MERGED_TOKEN_ID), + 3, + managed_biguint!(NFT_AMOUNT), + )); + assert_eq!(output_payments, expected_output_payments); + + // check newest token attributes + let merged_token_data = sc.blockchain().get_esdt_token_data( + &managed_address!(&address_expr_to_address(USER_ADDRESS_EXPR)), + &managed_token_id!(MERGED_TOKEN_ID), + 3, + ); + let mut expected_uri = ArrayVec::new(); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + SECOND_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(FUNGIBLE_TOKEN_ID), + 0, + managed_biguint!(FUNGIBLE_AMOUNT - 40), + )); + + let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); + assert_eq!(expected_uri, actual_uri.into_instances()); + + assert_eq!( + merged_token_data.royalties, + managed_biguint!(SECOND_ROYALTIES) + ); + }, + ); +} + +#[test] +fn test_custom_attributes() { + let mut world = world(); + + let use_module_whitebox = + WhiteboxContract::new(USE_MODULE_ADDRESS_EXPR, use_module::contract_obj); + let use_module_code = world.code_expression(USE_MODULE_PATH_EXPR); + + let roles = vec![ + "ESDTRoleNFTCreate".to_string(), + "ESDTRoleNFTBurn".to_string(), + ]; + + world.set_state_step( + SetStateStep::new() + .put_account(OWNER_ADDRESS_EXPR, Account::new().nonce(1)) + .put_account( + USER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .esdt_balance(FUNGIBLE_TOKEN_ID_EXPR, FUNGIBLE_AMOUNT) + .esdt_nft_all_properties( + NFT_TOKEN_ID_EXPR, + FIRST_NFT_NONCE, + NFT_AMOUNT, + Some(FIRST_ATTRIBUTES), + FIRST_ROYALTIES, + None, + None, + Vec::from(FIRST_URIS), + ) + .esdt_nft_all_properties( + NFT_TOKEN_ID_EXPR, + SECOND_NFT_NONCE, + NFT_AMOUNT, + Some(SECOND_ATTRIBUTES), + SECOND_ROYALTIES, + None, + None, + Vec::from(SECOND_URIS), + ), + ) + .put_account( + USE_MODULE_ADDRESS_EXPR, + Account::new() + .nonce(1) + .code(use_module_code) + .owner(OWNER_ADDRESS_EXPR) + .esdt_roles(MERGED_TOKEN_ID_EXPR, roles), + ), + ); + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| { + sc.merged_token() + .set_token_id(managed_token_id!(MERGED_TOKEN_ID)); + let _ = sc + .mergeable_tokens_whitelist() + .insert(managed_token_id!(NFT_TOKEN_ID)); + let _ = sc + .mergeable_tokens_whitelist() + .insert(managed_token_id!(FUNGIBLE_TOKEN_ID)); + }, + ); + + // merge two NFTs + let nft_transfers = vec![ + TxESDT { + esdt_token_identifier: NFT_TOKEN_ID.into(), + nonce: FIRST_NFT_NONCE.into(), + esdt_value: NFT_AMOUNT.into(), + }, + TxESDT { + esdt_token_identifier: NFT_TOKEN_ID.into(), + nonce: SECOND_NFT_NONCE.into(), + esdt_value: NFT_AMOUNT.into(), + }, + ]; + + let expected_attributes = CustomAttributes { + first: 5u32, + second: 10u64, + }; + + world.whitebox_call( + &use_module_whitebox, + ScCallStep::new() + .from(USER_ADDRESS_EXPR) + .multi_esdt_transfer(nft_transfers.clone()), + |sc| { + let merged_token = sc.merge_tokens_custom_attributes_endpoint(); + assert_eq!( + merged_token.token_identifier, + managed_token_id!(MERGED_TOKEN_ID) + ); + assert_eq!(merged_token.token_nonce, 1); + assert_eq!(merged_token.amount, managed_biguint!(NFT_AMOUNT)); + + let merged_token_data = sc.blockchain().get_esdt_token_data( + &managed_address!(&address_expr_to_address(USER_ADDRESS_EXPR)), + &managed_token_id!(MERGED_TOKEN_ID), + 1, + ); + let mut expected_uri = ArrayVec::new(); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + FIRST_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + expected_uri.push(EsdtTokenPayment::new( + managed_token_id!(NFT_TOKEN_ID), + SECOND_NFT_NONCE, + managed_biguint!(NFT_AMOUNT), + )); + + let actual_uri = MergedTokenInstances::decode_from_first_uri(&merged_token_data.uris); + assert_eq!(expected_uri, actual_uri.into_instances()); + + let actual_attributes: CustomAttributes = merged_token_data.decode_attributes(); + assert_eq!(expected_attributes, actual_attributes); + + assert_eq!( + merged_token_data.royalties, + managed_biguint!(SECOND_ROYALTIES) + ); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + MERGED_TOKEN_ID_EXPR, + 1, + NFT_AMOUNT, + Some(top_encode_to_vec_u8_or_panic(&expected_attributes)), + ), + )); + + world.check_state_step(CheckStateStep::new().put_account( + USE_MODULE_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + FIRST_NFT_NONCE, + NFT_AMOUNT, + Some(FIRST_ATTRIBUTES), + ), + )); + + world.check_state_step(CheckStateStep::new().put_account( + USE_MODULE_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + SECOND_NFT_NONCE, + NFT_AMOUNT, + Some(SECOND_ATTRIBUTES), + ), + )); +} + +fn address_expr_to_address(address_expr: &str) -> Address { + AddressValue::from(address_expr).to_address() +} diff --git a/framework/scenario/src/scenario/model/account_data/account.rs b/framework/scenario/src/scenario/model/account_data/account.rs index 75c3fccf86..912d8d94f5 100644 --- a/framework/scenario/src/scenario/model/account_data/account.rs +++ b/framework/scenario/src/scenario/model/account_data/account.rs @@ -84,6 +84,43 @@ impl Account { self } + #[allow(clippy::too_many_arguments)] + pub fn esdt_nft_all_properties( + mut self, + token_id_expr: K, + nonce_expr: N, + balance_expr: V, + opt_attributes_expr: Option, + royalties_expr: N, + creator_expr: Option, + hash_expr: Option, + uris_expr: Vec, + ) -> Self + where + BytesKey: From, + U64Value: From, + BigUintValue: From, + BytesValue: From, + { + let token_id = BytesKey::from(token_id_expr); + + let esdt_obj_ref = self + .get_esdt_data_or_create(&token_id) + .get_mut_esdt_object(); + + esdt_obj_ref.set_token_all_properties( + nonce_expr, + balance_expr, + opt_attributes_expr, + royalties_expr, + creator_expr, + hash_expr, + uris_expr, + ); + + self + } + pub fn esdt_nft_last_nonce(mut self, token_id_expr: K, last_nonce_expr: N) -> Self where BytesKey: From, diff --git a/framework/scenario/src/scenario/model/esdt_data/esdt_object.rs b/framework/scenario/src/scenario/model/esdt_data/esdt_object.rs index 13ff58fe53..f8298e2b8c 100644 --- a/framework/scenario/src/scenario/model/esdt_data/esdt_object.rs +++ b/framework/scenario/src/scenario/model/esdt_data/esdt_object.rs @@ -52,6 +52,72 @@ impl EsdtObject { } } + #[allow(clippy::too_many_arguments)] + pub fn set_token_all_properties( + &mut self, + nonce_expr: N, + balance_expr: V, + opt_attributes_expr: Option, + royalties_expr: N, + creator_expr: Option, + hash_expr: Option, + uris_expr: Vec, + ) where + U64Value: From, + BigUintValue: From, + BytesValue: From, + { + let inst_for_nonce = self.get_or_insert_instance_for_nonce(nonce_expr); + + let balance = BigUintValue::from(balance_expr); + if balance.value > 0u64.into() { + inst_for_nonce.balance = Some(balance); + } else { + inst_for_nonce.balance = None; + } + + if let Some(attributes) = opt_attributes_expr { + let attributes = BytesValue::from(attributes); + if !attributes.value.is_empty() { + inst_for_nonce.attributes = Some(attributes); + } else { + inst_for_nonce.attributes = None; + } + } + + let royalties = U64Value::from(royalties_expr); + if royalties.value > 0 { + inst_for_nonce.royalties = Some(royalties); + } else { + inst_for_nonce.royalties = None; + } + + if let Some(creator_expr) = creator_expr { + let creator = BytesValue::from(creator_expr); + if !creator.value.is_empty() { + inst_for_nonce.creator = Some(creator); + } else { + inst_for_nonce.creator = None; + } + } + + if let Some(hash) = hash_expr { + let hash = BytesValue::from(hash); + if !hash.value.is_empty() { + inst_for_nonce.hash = Some(hash); + } else { + inst_for_nonce.hash = None; + } + } + + if !uris_expr.is_empty() { + inst_for_nonce.uri = uris_expr + .into_iter() + .map(|uri| BytesValue::from(uri)) + .collect(); + } + } + pub fn set_last_nonce(&mut self, last_nonce_expr: N) where U64Value: From,