diff --git a/src/errors.rs b/src/errors.rs index 53a6896..0bf3ea7 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -38,3 +38,4 @@ pub const ERR_ONLY_WITHDRAWAL_ADDRESS_CAN_WITHDRAW: &str = pub const ERR_WITHDRAWAL_ADDRESS_NOT_SET: &str = "Withdrawal address not set"; pub const ERR_WRONG_AMOUNT_OF_FUNDS: &str = "Wrong amount of funds"; pub const ERR_WRONG_BOND_PERIOD: &str = "Wrong bond period"; +pub const ERR_PERCENTAGE_TOO_HIGH: &str = "Percentage too high"; diff --git a/src/events.rs b/src/events.rs index 8fe303a..6e79975 100644 --- a/src/events.rs +++ b/src/events.rs @@ -11,6 +11,17 @@ pub trait EventsModule { #[event("setTreasuryAddress")] fn treasury_address_event(&self, #[indexed] treasury_address: &ManagedAddress); + // Emitted whenever donation treasury address is set + #[event("setDonationTreasuryAddress")] + fn donation_treasury_address_event( + &self, + #[indexed] donation_treasury_address: &ManagedAddress, + ); + + // Emitted whenever max donation percentage is set + #[event("setMaxDonationPercentage")] + fn max_donation_percentage_event(&self, #[indexed] max_donation_percentage: &u64); + // Emitted whenever whitelist enabling changes value #[event("whitelistEnableToggle")] fn whitelist_enable_toggle_event(&self, #[indexed] enable_value: &bool); @@ -129,7 +140,7 @@ pub trait EventsModule { #[indexed] token: &EgldOrEsdtTokenIdentifier, #[indexed] price: &BigUint, #[indexed] bond_amount: &BigUint, - #[indexed] extra_assets: &ManagedVec + #[indexed] extra_assets: &ManagedVec, ); #[event("setWithdrawalAddress")] diff --git a/src/lib.rs b/src/lib.rs index 7e2faac..3813528 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,8 @@ use crate::{ callbacks::CallbackProxy, errors::{ ERR_ALREADY_IN_WHITELIST, ERR_CONTRACT_ALREADY_INITIALIZED, ERR_DATA_STREAM_IS_EMPTY, - ERR_ISSUE_COST, ERR_NOT_IN_WHITELIST, ERR_WHITELIST_IS_EMPTY, ERR_WRONG_AMOUNT_OF_FUNDS, - ERR_WRONG_BOND_PERIOD, + ERR_ISSUE_COST, ERR_NOT_IN_WHITELIST, ERR_PERCENTAGE_TOO_HIGH, ERR_WHITELIST_IS_EMPTY, + ERR_WRONG_AMOUNT_OF_FUNDS, ERR_WRONG_BOND_PERIOD, }, storage::DataNftAttributes, }; @@ -148,6 +148,7 @@ pub trait DataNftMint: title: ManagedBuffer, description: ManagedBuffer, lock_period_sec: u64, + donation_percentage: u64, extra_assets: MultiValueEncoded, ) -> DataNftAttributes { self.require_ready_for_minting_and_burning(); @@ -161,6 +162,23 @@ pub trait DataNftMint: self.require_title_description_are_valid(&title, &description); self.require_sft_is_valid(&royalties, &supply); + sc_print!("donation percentage: {} ", donation_percentage.clone()); + + let donation_supply = if donation_percentage > 0 { + require!( + donation_percentage <= self.max_donation_percentage().get(), + ERR_PERCENTAGE_TOO_HIGH + ); + + let donation_supply = + &supply * &BigUint::from(donation_percentage) / BigUint::from(10_000u64); + donation_supply + } else { + BigUint::zero() + }; + + sc_print!("donation supply:{}", donation_supply); + let caller = self.blockchain().get_caller(); let current_time = self.blockchain().get_block_timestamp(); self.require_minting_is_allowed(&caller, current_time); @@ -206,14 +224,14 @@ pub trait DataNftMint: }; let token_identifier = self.token_id().get_token_id(); - let extra_assets_vec = extra_assets.into_vec_of_buffers(); + let extra_assets_vec = extra_assets.into_vec_of_buffers(); self.mint_event( &caller, &one_token, &payment.token_identifier, &price, &payment.amount, - &extra_assets_vec + &extra_assets_vec, ); let nonce = self.send().esdt_nft_create( @@ -234,8 +252,24 @@ pub trait DataNftMint: payment, ); - self.send() - .direct_esdt(&caller, &token_identifier, nonce, &supply); + if donation_supply > BigUint::zero() { + let donation_treasury_address = self.donation_treasury_address().get(); + self.send().direct_esdt( + &donation_treasury_address, + &token_identifier, + nonce, + &donation_supply, + ); + self.send().direct_esdt( + &caller, + &token_identifier, + nonce, + &(&supply - &donation_supply), + ); + } else { + self.send() + .direct_esdt(&caller, &token_identifier, nonce, &supply); + } attributes } @@ -268,6 +302,21 @@ pub trait DataNftMint: self.treasury_address().set(&address); } + #[endpoint(setDonationTreasuryAddress)] + fn set_donation_treasury_address(&self, address: ManagedAddress) { + self.require_is_privileged(&self.blockchain().get_caller()); + self.donation_treasury_address_event(&address); + self.donation_treasury_address().set(&address); + } + + #[endpoint(setMaxDonationPercentage)] + fn set_max_donation_percentage(&self, percentage: u64) { + self.require_is_privileged(&self.blockchain().get_caller()); + require!(percentage <= 10_000, ERR_PERCENTAGE_TOO_HIGH); + self.max_donation_percentage_event(&percentage); + self.max_donation_percentage().set(percentage); + } + // Endpoint that will be used by privileged address to change the contract pause value. #[endpoint(setIsPaused)] fn set_is_paused(&self, is_paused: bool) { diff --git a/src/requirements.rs b/src/requirements.rs index de5fdc3..6b55c94 100644 --- a/src/requirements.rs +++ b/src/requirements.rs @@ -26,6 +26,7 @@ pub trait RequirementsModule: crate::storage::StorageModule { if self.treasury_address().is_empty() { is_mint_ready = false; } + if self.bond_contract_address().is_empty() { is_mint_ready = false; } @@ -35,6 +36,9 @@ pub trait RequirementsModule: crate::storage::StorageModule { if self.bond_contract_address().is_empty() { is_mint_ready = false; } + if self.donation_treasury_address().is_empty() { + is_mint_ready = false; + } require!(is_mint_ready, ERR_MINTING_AND_BURNING_NOT_ALLOWED); } diff --git a/src/storage.rs b/src/storage.rs index d850da3..9746d11 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -25,6 +25,14 @@ pub trait StorageModule { #[storage_mapper("treasury_address")] fn treasury_address(&self) -> SingleValueMapper; + #[view(getDonationTreasuryAddress)] + #[storage_mapper("donation_treasury_address")] + fn donation_treasury_address(&self) -> SingleValueMapper; + + #[view(getMaxDonationPercentage)] + #[storage_mapper("max_donation_percentage")] + fn max_donation_percentage(&self) -> SingleValueMapper; + #[view(getWithdrawalAddress)] #[storage_mapper("withdrawal_address")] fn withdrawal_address(&self) -> SingleValueMapper; diff --git a/src/views.rs b/src/views.rs index 953d3d8..63b86f3 100644 --- a/src/views.rs +++ b/src/views.rs @@ -18,6 +18,7 @@ pub struct UserDataOut { pub total_minted: BigUint, pub frozen: bool, pub frozen_nonces: ManagedVec, + pub max_donation_percentage: u64, } //Module that handles read-only endpoints (views) for the smart contract @@ -47,6 +48,7 @@ pub trait ViewsModule: crate::storage::StorageModule { .frozen_sfts_per_address(&address) .iter() .collect::>(); + let max_donation_percentage = self.max_donation_percentage().get(); let user_data = UserDataOut { anti_spam_tax_value, @@ -62,6 +64,7 @@ pub trait ViewsModule: crate::storage::StorageModule { total_minted, frozen, frozen_nonces, + max_donation_percentage, }; user_data } diff --git a/tests/endpoints/burn.rs b/tests/endpoints/burn.rs index 61ca39b..74c462d 100644 --- a/tests/endpoints/burn.rs +++ b/tests/endpoints/burn.rs @@ -44,6 +44,7 @@ fn burn_token_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 100u64 + 100u64, + 0u64, None, ); diff --git a/tests/endpoints/mint.rs b/tests/endpoints/mint.rs index c7887e5..6dd660e 100644 --- a/tests/endpoints/mint.rs +++ b/tests/endpoints/mint.rs @@ -9,7 +9,7 @@ use crate::minter_state::minter_state::{ ContractsState, BONDING_CONTRACT_ADDRESS_EXPR, BONDING_OWNER_ADDRESS_EXPR, DATA_NFT_IDENTIFIER_EXPR, FIRST_USER_ADDRESS_EXPR, ITHEUM_TOKEN_IDENTIFIER, ITHEUM_TOKEN_IDENTIFIER_EXPR, MINTER_CONTRACT_ADDRESS_EXPR, MINTER_OWNER_ADDRESS_EXPR, - TREAASURY_ADDRESS_EXPR, + SECOND_USER_ADDRESS_EXPR, TREAASURY_ADDRESS_EXPR, }; #[test] @@ -34,6 +34,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:Minting and burning not allowed")), ); @@ -57,6 +58,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:Data Stream is empty")), ); @@ -76,6 +78,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:URL must start with https://")), ); @@ -95,6 +98,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:URL is empty")), ); @@ -114,6 +118,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:URL length is too small")), ); @@ -133,6 +138,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:URL length is too big")), ); @@ -152,6 +158,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:URL must start with https://")), ); @@ -171,6 +178,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:URL must start with https://")), ); @@ -190,6 +198,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:Field is empty")), ); @@ -209,6 +218,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:Field is empty")), ); @@ -228,6 +238,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:Too many characters")), ); @@ -247,6 +258,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:Too many characters")), ); @@ -266,6 +278,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error( "str:Royalties are bigger than max royalties", )), @@ -287,6 +300,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:Max supply exceeded")), ); @@ -306,6 +320,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error( "str:You need to wait more time before minting again", )), @@ -331,6 +346,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:You are not whitelisted")), ); @@ -354,6 +370,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 0u64, + 0u64, Some(TxExpect::user_error("str:Wrong bond period")), ); @@ -375,6 +392,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 10 + 100u64, + 0u64, Some(TxExpect::user_error("str:Wrong amount of funds")), ); @@ -394,6 +412,7 @@ fn mint_test_without_anti_spam_tax_test() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 100u64, + 0u64, None, ); @@ -449,6 +468,7 @@ fn mint_with_anti_spam_tax_test_and_whitelist() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 100u64, + 0u64, Some(TxExpect::user_error("str:Wrong amount of funds")), ); @@ -468,6 +488,7 @@ fn mint_with_anti_spam_tax_test_and_whitelist() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 99u64 + 100u64, + 0u64, Some(TxExpect::user_error("str:Wrong amount of funds")), ); @@ -487,6 +508,7 @@ fn mint_with_anti_spam_tax_test_and_whitelist() { ITHEUM_TOKEN_IDENTIFIER, 0u64, 100u64 + 100u64, + 0u64, None, ); @@ -531,3 +553,150 @@ fn mint_with_anti_spam_tax_test_and_whitelist() { ), ); } + +#[test] +fn mint_with_donation_to_treasury() { + let mut state = ContractsState::new(); + let treasury_address = state.treasury.clone(); + let first_user_address = state.first_user.clone(); + let second_user_address = state.second_user.clone(); + + state + .world + .set_state_step(SetStateStep::new().block_timestamp(0u64)); + + state + .mock_minter_initialized(ITHEUM_TOKEN_IDENTIFIER, 100u64, 10u64) + .unpause_minter_contract(MINTER_OWNER_ADDRESS_EXPR, None) + .bond_contract_default_deploy_and_set(10u64, 100u64) + .bond_unpause_contract(BONDING_OWNER_ADDRESS_EXPR, None) + .minter_set_donation_treasury_address( + MINTER_OWNER_ADDRESS_EXPR, + treasury_address.clone(), + None, + ) + .minter_disable_whitelist(MINTER_OWNER_ADDRESS_EXPR, None) + .minter_set_mint_time_limit(MINTER_OWNER_ADDRESS_EXPR, 0u64, None) + .minter_set_max_supply(MINTER_OWNER_ADDRESS_EXPR, 1_000u64, None) + .minter_set_donation_max_percentage(MINTER_OWNER_ADDRESS_EXPR, 1_000, None); + + state.minter_mint( + FIRST_USER_ADDRESS_EXPR, + "Test", + "https://test.com/test", + "https://test.com/test", + "https://test.com/test", + "random-url-encoded-here", + "https://test.com/test", + 1000u64, + 100u64, + &"Test title".repeat(1), + &"Test description".repeat(1), + 10u64, + ITHEUM_TOKEN_IDENTIFIER, + 0u64, + 100u64 + 100u64, + 5_000u64, // 1% + Some(TxExpect::user_error("str:Percentage too high")), + ); + + state.minter_mint( + FIRST_USER_ADDRESS_EXPR, + "Test", + "https://test.com/test", + "https://test.com/test", + "https://test.com/test", + "random-url-encoded-here", + "https://test.com/test", + 1000u64, + 100u64, + &"Test title".repeat(1), + &"Test description".repeat(1), + 10u64, + ITHEUM_TOKEN_IDENTIFIER, + 0u64, + 100u64 + 100u64, + 0, + None, + ); + + let data_nft_attributes: DataNftAttributes = DataNftAttributes { + data_stream_url: managed_buffer!(b"random-url-encoded-here"), + data_preview_url: managed_buffer!(b"https://test.com/test"), + data_marshal_url: managed_buffer!(b"https://test.com/test"), + creator: managed_address!(&first_user_address), + creation_time: 0u64, + title: managed_buffer!(b"Test title"), + description: managed_buffer!(b"Test description"), + }; + + state + .world + .check_state_step(CheckStateStep::new().put_account( + FIRST_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + DATA_NFT_IDENTIFIER_EXPR, + 1u64, + "100", + Some(data_nft_attributes.clone()), + ), + )); + + state + .world + .set_state_step(SetStateStep::new().block_timestamp(1u64)); + + state.minter_mint( + SECOND_USER_ADDRESS_EXPR, + "Test", + "https://test.com/test", + "https://test.com/test", + "https://test.com/test", + "random-url-encoded-here", + "https://test.com/test", + 1000u64, + 100u64, + &"Test title".repeat(1), + &"Test description".repeat(1), + 10u64, + ITHEUM_TOKEN_IDENTIFIER, + 0u64, + 100u64 + 100u64, + 100u64, + None, + ); + + let data_nft_attributes: DataNftAttributes = DataNftAttributes { + data_stream_url: managed_buffer!(b"random-url-encoded-here"), + data_preview_url: managed_buffer!(b"https://test.com/test"), + data_marshal_url: managed_buffer!(b"https://test.com/test"), + creator: managed_address!(&second_user_address), + creation_time: 1u64, + title: managed_buffer!(b"Test title"), + description: managed_buffer!(b"Test description"), + }; + + state + .world + .check_state_step(CheckStateStep::new().put_account( + SECOND_USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + DATA_NFT_IDENTIFIER_EXPR, + 2u64, + "99", + Some(data_nft_attributes.clone()), + ), + )); + + state + .world + .check_state_step(CheckStateStep::new().put_account( + TREAASURY_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + DATA_NFT_IDENTIFIER_EXPR, + 2u64, + "1", + Some(data_nft_attributes.clone()), + ), + )); +} diff --git a/tests/minter_state/minter_state.rs b/tests/minter_state/minter_state.rs index c54aae0..78af845 100644 --- a/tests/minter_state/minter_state.rs +++ b/tests/minter_state/minter_state.rs @@ -287,6 +287,41 @@ impl ContractsState { self } + pub fn minter_set_donation_treasury_address( + &mut self, + caller: &str, + address: Address, + expect: Option, + ) -> &mut Self { + let tx_expect = expect.unwrap_or(TxExpect::ok()); + self.world.sc_call( + ScCallStep::new() + .from(caller) + .call(self.minter_contract.set_donation_treasury_address(address)) + .expect(tx_expect), + ); + self + } + + pub fn minter_set_donation_max_percentage( + &mut self, + caller: &str, + max_donation_percentage: u64, + expect: Option, + ) -> &mut Self { + let tx_expect = expect.unwrap_or(TxExpect::ok()); + self.world.sc_call( + ScCallStep::new() + .from(caller) + .call( + self.minter_contract + .set_max_donation_percentage(max_donation_percentage), + ) + .expect(tx_expect), + ); + self + } + pub fn pause_minter_contract(&mut self, caller: &str, expect: Option) -> &mut Self { let tx_expect = expect.unwrap_or(TxExpect::ok()); self.world.sc_call( @@ -543,6 +578,7 @@ impl ContractsState { payment_token_identifier: &[u8], payment_token_nonce: u64, payment_amount: u64, + donation_percentage: u64, expect: Option, ) -> &mut Self { self.world.sc_call( @@ -565,7 +601,8 @@ impl ContractsState { title, description, lock_period, - MultiValueEncoded::new() + donation_percentage, + MultiValueEncoded::new(), )) .expect(expect.unwrap_or(TxExpect::ok())), ); @@ -654,7 +691,12 @@ impl ContractsState { self.minter_set_royalties_limits(MINTER_OWNER_ADDRESS_EXPR, 0u64, 8000u64, None); self.minter_set_administarator(MINTER_OWNER_ADDRESS_EXPR, admin, None); self.minter_set_bond_contract_address(MINTER_OWNER_ADDRESS_EXPR, None); - self.minter_set_treasury_address(MINTER_OWNER_ADDRESS_EXPR, treasury_address, None); + self.minter_set_treasury_address(MINTER_OWNER_ADDRESS_EXPR, treasury_address.clone(), None); + self.minter_set_donation_treasury_address( + MINTER_ADMIN_ADDRESS_EXPR, + treasury_address, + None, + ); self.minter_set_anti_spam_tax_token_and_amount( MINTER_OWNER_ADDRESS_EXPR, anti_spam_tax_token, diff --git a/tests/unit_test.rs b/tests/unit_test.rs index 5ba0653..4af854a 100644 --- a/tests/unit_test.rs +++ b/tests/unit_test.rs @@ -140,6 +140,12 @@ fn minter_contract_ready_test() { minter_contract.roles_are_set().set(true); + minter_contract + .donation_treasury_address() + .set(managed_address!( + &AddressValue::from("address:donation").to_address() + )); + minter_contract.require_ready_for_minting_and_burning(); }); diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 0fc1b9a..a8ad5ad 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -5,9 +5,9 @@ //////////////////////////////////////////////////// // Init: 1 -// Endpoints: 48 +// Endpoints: 52 // Async Callback: 1 -// Total number of exported functions: 50 +// Total number of exported functions: 54 #![no_std] #![allow(internal_features)] @@ -26,6 +26,8 @@ multiversx_sc_wasm_adapter::endpoints! { mint => mint_token burn => burn_token setTreasuryAddress => set_treasury_address + setDonationTreasuryAddress => set_donation_treasury_address + setMaxDonationPercentage => set_max_donation_percentage setIsPaused => set_is_paused setWhiteListEnabled => set_whitelist_enabled setAntiSpamTax => set_anti_spam_tax @@ -40,6 +42,8 @@ multiversx_sc_wasm_adapter::endpoints! { withdraw => withdraw getTokenId => token_id getTreasuryAddress => treasury_address + getDonationTreasuryAddress => donation_treasury_address + getMaxDonationPercentage => max_donation_percentage getWithdrawalAddress => withdrawal_address getMintedTokens => minted_tokens getAntiSpamTax => anti_spam_tax