From 999c24f384bfb296b382e6f5c59341d79076892d Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Thu, 25 Aug 2022 09:00:08 +0000 Subject: [PATCH] Merge pull request #469 from UniqueNetwork/feature/refactor_contract_sponsoring Feature/refactor contract sponsoring --- --- a/Cargo.lock +++ b/Cargo.lock @@ -2345,7 +2345,7 @@ [[package]] name = "fc-consensus" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "async-trait", "fc-db", @@ -2364,7 +2364,7 @@ [[package]] name = "fc-db" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "fp-storage", "kvdb-rocksdb", @@ -2380,7 +2380,7 @@ [[package]] name = "fc-mapping-sync" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "fc-db", "fp-consensus", @@ -2397,7 +2397,7 @@ [[package]] name = "fc-rpc" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "ethereum-types", @@ -2437,7 +2437,7 @@ [[package]] name = "fc-rpc-core" version = "1.1.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "ethereum-types", @@ -2578,7 +2578,7 @@ [[package]] name = "fp-consensus" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "parity-scale-codec 3.1.5", @@ -2590,7 +2590,7 @@ [[package]] name = "fp-evm" version = "3.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "evm", "frame-support", @@ -2604,7 +2604,7 @@ [[package]] name = "fp-evm-mapping" version = "0.1.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "frame-support", "sp-core", @@ -2613,7 +2613,7 @@ [[package]] name = "fp-rpc" version = "3.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "ethereum-types", @@ -2630,7 +2630,7 @@ [[package]] name = "fp-self-contained" version = "1.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "frame-support", @@ -2646,7 +2646,7 @@ [[package]] name = "fp-storage" version = "2.0.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "parity-scale-codec 3.1.5", ] @@ -5445,7 +5445,7 @@ [[package]] name = "pallet-base-fee" version = "1.0.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "fp-evm", "frame-support", @@ -5552,7 +5552,7 @@ [[package]] name = "pallet-common" -version = "0.1.6" +version = "0.1.7" dependencies = [ "ethereum", "evm-coder", @@ -5660,7 +5660,7 @@ [[package]] name = "pallet-ethereum" version = "4.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "ethereum-types", @@ -5689,7 +5689,7 @@ [[package]] name = "pallet-evm" version = "6.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "evm", "fp-evm", @@ -5733,7 +5733,7 @@ [[package]] name = "pallet-evm-contract-helpers" -version = "0.1.2" +version = "0.2.0" dependencies = [ "evm-coder", "fp-evm-mapping", @@ -12130,7 +12130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 0.1.10", "digest 0.10.3", "rand 0.8.5", "static_assertions", --- a/crates/evm-coder/CHANGELOG.md +++ b/crates/evm-coder/CHANGELOG.md @@ -1,4 +1,16 @@ - +# Change Log + +All notable changes to this project will be documented in this file. +## [v0.1.2] 2022-08-19 + +### Added + + - Implementation `AbiWrite` for tuples. + + ### Fixes + + - Tuple generation for solidity. + ## [v0.1.1] 2022-08-16 ### Other changes --- a/crates/evm-coder/src/abi.rs +++ b/crates/evm-coder/src/abi.rs @@ -342,6 +342,16 @@ )) } } + #[allow(non_snake_case)] + impl<$($ident),+> AbiWrite for &($($ident,)+) + where + $($ident: AbiWrite,)+ + { + fn abi_write(&self, writer: &mut AbiWriter) { + let ($($ident,)+) = self; + $($ident.abi_write(writer);)+ + } + } }; } --- a/crates/evm-coder/src/solidity.rs +++ b/crates/evm-coder/src/solidity.rs @@ -180,9 +180,16 @@ fn is_simple() -> bool { false } + #[allow(unused_assignments)] fn solidity_default(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result { write!(writer, "{}(", tc.collect_tuple::())?; + let mut first = true; $( + if !first { + write!(writer, ",")?; + } else { + first = false; + } <$ident>::solidity_default(writer, tc)?; )* write!(writer, ")") --- a/pallets/common/CHANGELOG.md +++ b/pallets/common/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [0.1.7] - 2022-08-19 + +### Added + + - Add convert funtion from `CrossAccountId` to eth `uint256`. + + ## [0.1.6] - 2022-08-16 ### Added --- a/pallets/common/Cargo.toml +++ b/pallets/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-common" -version = "0.1.6" +version = "0.1.7" license = "GPLv3" edition = "2021" --- a/pallets/common/src/eth.rs +++ b/pallets/common/src/eth.rs @@ -16,13 +16,11 @@ //! The module contains a number of functions for converting and checking ethereum identifiers. -use evm_coder::{types::*}; -pub use pallet_evm::account::CrossAccountId; +use evm_coder::types::uint256; +pub use pallet_evm::account::{Config, CrossAccountId}; use sp_core::H160; use up_data_structs::CollectionId; -use crate::Config; - // 0x17c4e6453Cc49AAAaEACA894e6D9683e00000001 - collection 1 // TODO: Unhardcode prefix const ETH_COLLECTION_PREFIX: [u8; 16] = [ @@ -52,6 +50,15 @@ address[0..16] == ETH_COLLECTION_PREFIX } +/// Convert `CrossAccountId` to `uint256`. +pub fn convert_cross_account_to_uint256(from: &T::CrossAccountId) -> uint256 +where + T::AccountId: AsRef<[u8; 32]>, +{ + let slice = from.as_sub().as_ref(); + uint256::from_big_endian(slice) +} + /// Converts Substrate address to CrossAccountId pub fn convert_substrate_address_to_cross_account_id( address: uint256, --- a/pallets/evm-contract-helpers/CHANGELOG.md +++ b/pallets/evm-contract-helpers/CHANGELOG.md @@ -1,4 +1,24 @@ - +# Change Log + +All notable changes to this project will be documented in this file. + +## [v0.2.0] - 2022-08-19 + +### Added + + - Set arbitrary evm address as contract sponsor. + - Ability to remove current sponsor. + +### Removed + - Remove methods + + sponsoring_enabled + + toggle_sponsoring + + ### Changed + + - Change `toggle_sponsoring` to `self_sponsored_enable`. + + ## [v0.1.2] 2022-08-16 ### Other changes --- a/pallets/evm-contract-helpers/Cargo.toml +++ b/pallets/evm-contract-helpers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-evm-contract-helpers" -version = "0.1.2" +version = "0.2.0" license = "GPLv3" edition = "2021" --- a/pallets/evm-contract-helpers/src/eth.rs +++ b/pallets/evm-contract-helpers/src/eth.rs @@ -16,14 +16,16 @@ use core::marker::PhantomData; use evm_coder::{abi::AbiWriter, execution::Result, generate_stubgen, solidity_interface, types::*}; -use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder}; +use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder, dispatch_to_evm}; use pallet_evm::{ ExitRevert, OnCreate, OnMethodCall, PrecompileResult, PrecompileFailure, PrecompileHandle, account::CrossAccountId, }; use sp_core::H160; +use up_data_structs::SponsorshipState; use crate::{ AllowlistEnabled, Config, Owner, Pallet, SponsorBasket, SponsoringRateLimit, SponsoringModeT, + Sponsoring, }; use frame_support::traits::Get; use up_sponsorship::SponsorshipHandler; @@ -41,36 +43,135 @@ } #[solidity_interface(name = "ContractHelpers")] -impl ContractHelpers { +impl ContractHelpers +where + T::AccountId: AsRef<[u8; 32]>, +{ + /// Get contract ovner + /// + /// @param Contract_address contract for which the owner is being determined. + /// @return Contract owner. fn contract_owner(&self, contract_address: address) -> Result
{ Ok(>::get(contract_address)) } - fn sponsoring_enabled(&self, contract_address: address) -> Result { - Ok(>::sponsoring_mode(contract_address) != SponsoringModeT::Disabled) - } - - /// Deprecated - fn toggle_sponsoring( + /// Set sponsor. + /// + /// @param contract_address Contract for which a sponsor is being established. + /// @param sponsor User address who set as pending sponsor. + fn set_sponsor( &mut self, caller: caller, contract_address: address, - enabled: bool, + sponsor: address, ) -> Result { - >::ensure_owner(contract_address, caller)?; - >::toggle_sponsoring(contract_address, enabled); + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + Pallet::::set_sponsor( + &T::CrossAccountId::from_eth(caller), + contract_address, + &T::CrossAccountId::from_eth(sponsor), + ) + .map_err(dispatch_to_evm::)?; + + Ok(()) + } + + /// Set contract as self sponsored. + /// + /// @param contract_address Contract for which a self sponsoring is being enabled. + fn self_sponsored_enable(&mut self, caller: caller, contract_address: address) -> Result { + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + Pallet::::self_sponsored_enable(&T::CrossAccountId::from_eth(caller), contract_address) + .map_err(dispatch_to_evm::)?; + + Ok(()) + } + + /// Remove sponsor. + /// + /// @param contract_address Contract for which a sponsorship is being removed. + fn remove_sponsor(&mut self, caller: caller, contract_address: address) -> Result { + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + Pallet::::remove_sponsor(&T::CrossAccountId::from_eth(caller), contract_address) + .map_err(dispatch_to_evm::)?; + + Ok(()) + } + + /// Confirm sponsorship. + /// + /// @dev Caller must be same that set via [`set_sponsor`]. + /// + /// @param contract_address Сontract for which need to confirm sponsorship. + fn confirm_sponsorship(&mut self, caller: caller, contract_address: address) -> Result { + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + Pallet::::confirm_sponsorship(&T::CrossAccountId::from_eth(caller), contract_address) + .map_err(dispatch_to_evm::)?; + Ok(()) } + /// Get current sponsor. + /// + /// @param contract_address The contract for which a sponsor is requested. + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + fn get_sponsor(&self, contract_address: address) -> Result<(address, uint256)> { + let sponsor = + Pallet::::get_sponsor(contract_address).ok_or("Contract has no sponsor")?; + let result: (address, uint256) = if sponsor.is_canonical_substrate() { + let sponsor = pallet_common::eth::convert_cross_account_to_uint256::(&sponsor); + (Default::default(), sponsor) + } else { + let sponsor = *sponsor.as_eth(); + (sponsor, Default::default()) + }; + Ok(result) + } + + /// Check tat contract has confirmed sponsor. + /// + /// @param contract_address The contract for which the presence of a confirmed sponsor is checked. + /// @return **true** if contract has confirmed sponsor. + fn has_sponsor(&self, contract_address: address) -> Result { + Ok(Pallet::::get_sponsor(contract_address).is_some()) + } + + /// Check tat contract has pending sponsor. + /// + /// @param contract_address The contract for which the presence of a pending sponsor is checked. + /// @return **true** if contract has pending sponsor. + fn has_pending_sponsor(&self, contract_address: address) -> Result { + Ok(match Sponsoring::::get(contract_address) { + SponsorshipState::Disabled | SponsorshipState::Confirmed(_) => false, + SponsorshipState::Unconfirmed(_) => true, + }) + } + + fn sponsoring_enabled(&self, contract_address: address) -> Result { + Ok(>::sponsoring_mode(contract_address) != SponsoringModeT::Disabled) + } + fn set_sponsoring_mode( &mut self, caller: caller, contract_address: address, mode: uint8, ) -> Result { - >::ensure_owner(contract_address, caller)?; + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + >::ensure_owner(contract_address, caller).map_err(dispatch_to_evm::)?; let mode = SponsoringModeT::from_eth(mode).ok_or("unknown mode")?; >::set_sponsoring_mode(contract_address, mode); + Ok(()) } @@ -84,8 +185,12 @@ contract_address: address, rate_limit: uint32, ) -> Result { - >::ensure_owner(contract_address, caller)?; + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + >::ensure_owner(contract_address, caller).map_err(dispatch_to_evm::)?; >::set_sponsoring_rate_limit(contract_address, rate_limit.into()); + Ok(()) } @@ -110,8 +215,12 @@ contract_address: address, enabled: bool, ) -> Result { - >::ensure_owner(contract_address, caller)?; + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + >::ensure_owner(contract_address, caller).map_err(dispatch_to_evm::)?; >::toggle_allowlist(contract_address, enabled); + Ok(()) } @@ -122,14 +231,21 @@ user: address, allowed: bool, ) -> Result { - >::ensure_owner(contract_address, caller)?; + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + >::ensure_owner(contract_address, caller).map_err(dispatch_to_evm::)?; >::toggle_allowed(contract_address, user, allowed); + Ok(()) } } pub struct HelpersOnMethodCall(PhantomData<*const T>); -impl OnMethodCall for HelpersOnMethodCall { +impl OnMethodCall for HelpersOnMethodCall +where + T::AccountId: AsRef<[u8; 32]>, +{ fn is_reserved(contract: &sp_core::H160) -> bool { contract == &T::ContractAddress::get() } @@ -179,18 +295,26 @@ for HelpersContractSponsoring { fn get_sponsor(who: &T::CrossAccountId, call: &(H160, Vec)) -> Option { - let mode = >::sponsoring_mode(call.0); + let (contract_address, _) = call; + let mode = >::sponsoring_mode(*contract_address); if mode == SponsoringModeT::Disabled { return None; } - if mode == SponsoringModeT::Allowlisted && !>::allowed(call.0, *who.as_eth()) { + let sponsor = match >::get_sponsor(*contract_address) { + Some(sponsor) => sponsor, + None => return None, + }; + + if mode == SponsoringModeT::Allowlisted + && !>::allowed(*contract_address, *who.as_eth()) + { return None; } let block_number = >::block_number() as T::BlockNumber; - if let Some(last_tx_block) = >::get(&call.0, who.as_eth()) { - let limit = >::get(&call.0); + if let Some(last_tx_block) = >::get(contract_address, who.as_eth()) { + let limit = >::get(contract_address); let timeout = last_tx_block + limit; if block_number < timeout { @@ -198,9 +322,8 @@ } } - >::insert(&call.0, who.as_eth(), block_number); + >::insert(contract_address, who.as_eth(), block_number); - let sponsor = T::CrossAccountId::from_eth(call.0); Some(sponsor) } } --- a/pallets/evm-contract-helpers/src/lib.rs +++ b/pallets/evm-contract-helpers/src/lib.rs @@ -25,9 +25,11 @@ #[frame_support::pallet] pub mod pallet { pub use super::*; - use evm_coder::execution::Result; use frame_support::pallet_prelude::*; + use pallet_evm_coder_substrate::DispatchResult; use sp_core::H160; + use pallet_evm::account::CrossAccountId; + use up_data_structs::SponsorshipState; #[pallet::config] pub trait Config: @@ -39,14 +41,21 @@ #[pallet::error] pub enum Error { - /// This method is only executable by owner + /// This method is only executable by owner. NoPermission, + + /// No pending sponsor for contract. + NoPendingSponsor, } #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); + /// Store owner for contract. + /// + /// * **Key** - contract address. + /// * **Value** - owner for contract. #[pallet::storage] pub(super) type Owner = StorageMap; @@ -56,10 +65,33 @@ pub(super) type SelfSponsoring = StorageMap; + /// Store for contract sponsorship state. + /// + /// * **Key** - contract address. + /// * **Value** - sponsorship state. + #[pallet::storage] + pub(super) type Sponsoring = StorageMap< + Hasher = Twox64Concat, + Key = H160, + Value = SponsorshipState, + QueryKind = ValueQuery, + >; + + /// Store for sponsoring mode. + /// + /// ### Usage + /// Prefer to delete collection from storage if mode chaged to [`Disabled`](SponsoringModeT::Disabled). + /// + /// * **Key** - contract address. + /// * **Value** - [`sponsoring mode`](SponsoringModeT). #[pallet::storage] pub(super) type SponsoringMode = StorageMap; + /// Storage for sponsoring rate limit in blocks. + /// + /// * **Key** - contract address. + /// * **Value** - amount of sponsored blocks. #[pallet::storage] pub(super) type SponsoringRateLimit = StorageMap< Hasher = Twox128, @@ -69,6 +101,11 @@ OnEmpty = T::DefaultSponsoringRateLimit, >; + /// Storage for last sponsored block. + /// + /// * **Key1** - contract address. + /// * **Key2** - sponsored user address. + /// * **Value** - last sponsored block number. #[pallet::storage] pub(super) type SponsorBasket = StorageDoubleMap< Hasher1 = Twox128, @@ -79,10 +116,25 @@ QueryKind = OptionQuery, >; + /// Storege for contracts with [`Allowlisted`](SponsoringModeT::Allowlisted) sponsoring mode. + /// + /// ### Usage + /// Prefer to delete collection from storage if mode chaged to non `Allowlisted`, than set **Value** to **false**. + /// + /// * **Key** - contract address. + /// * **Value** - is contract in [`Allowlisted`](SponsoringModeT::Allowlisted) mode. #[pallet::storage] pub(super) type AllowlistEnabled = StorageMap; + /// Storage for users that allowed for sponsorship. + /// + /// ### Usage + /// Prefer to delete record from storage if user no more allowed for sponsorship. + /// + /// * **Key1** - contract address. + /// * **Key2** - user that allowed for sponsorship. + /// * **Value** - allowance for sponsorship. #[pallet::storage] pub(super) type Allowlist = StorageDoubleMap< Hasher1 = Twox128, @@ -94,6 +146,77 @@ >; impl Pallet { + /// Get contract owner. + pub fn contract_owner(contract: H160) -> H160 { + >::get(contract) + } + + /// Set `sponsor` for `contract`. + /// + /// `sender` must be owner of contract. + pub fn set_sponsor( + sender: &T::CrossAccountId, + contract: H160, + sponsor: &T::CrossAccountId, + ) -> DispatchResult { + Pallet::::ensure_owner(contract, *sender.as_eth())?; + Sponsoring::::insert( + contract, + SponsorshipState::::Unconfirmed(sponsor.clone()), + ); + Ok(()) + } + + /// Set `contract` as self sponsored. + /// + /// `sender` must be owner of contract. + pub fn self_sponsored_enable(sender: &T::CrossAccountId, contract: H160) -> DispatchResult { + Pallet::::ensure_owner(contract, *sender.as_eth())?; + Sponsoring::::insert( + contract, + SponsorshipState::::Confirmed(T::CrossAccountId::from_eth( + contract, + )), + ); + Ok(()) + } + + /// Remove sponsor for `contract`. + /// + /// `sender` must be owner of contract. + pub fn remove_sponsor(sender: &T::CrossAccountId, contract: H160) -> DispatchResult { + Pallet::::ensure_owner(contract, *sender.as_eth())?; + Sponsoring::::remove(contract); + Ok(()) + } + + /// Confirm sponsorship. + /// + /// `sender` must be same that set via [`set_sponsor`]. + pub fn confirm_sponsorship(sender: &T::CrossAccountId, contract: H160) -> DispatchResult { + match Sponsoring::::get(contract) { + SponsorshipState::Unconfirmed(sponsor) => { + ensure!(sponsor == *sender, Error::::NoPermission); + Sponsoring::::insert( + contract, + SponsorshipState::::Confirmed(sponsor), + ); + Ok(()) + } + SponsorshipState::Disabled | SponsorshipState::Confirmed(_) => { + Err(Error::::NoPendingSponsor.into()) + } + } + } + + /// Get sponsor. + pub fn get_sponsor(contract: H160) -> Option { + match Sponsoring::::get(contract) { + SponsorshipState::Disabled | SponsorshipState::Unconfirmed(_) => None, + SponsorshipState::Confirmed(sponsor) => Some(sponsor), + } + } + pub fn sponsoring_mode(contract: H160) -> SponsoringModeT { >::get(contract) .or_else(|| { @@ -101,6 +224,7 @@ }) .unwrap_or_default() } + pub fn set_sponsoring_mode(contract: H160, mode: SponsoringModeT) { if mode == SponsoringModeT::Disabled { >::remove(contract); @@ -110,17 +234,6 @@ >::remove(contract) } - pub fn toggle_sponsoring(contract: H160, enabled: bool) { - Self::set_sponsoring_mode( - contract, - if enabled { - SponsoringModeT::Allowlisted - } else { - SponsoringModeT::Disabled - }, - ) - } - pub fn set_sponsoring_rate_limit(contract: H160, rate_limit: T::BlockNumber) { >::insert(contract, rate_limit); } @@ -137,8 +250,8 @@ >::insert(contract, user, allowed); } - pub fn ensure_owner(contract: H160, user: H160) -> Result<()> { - ensure!(>::get(&contract) == user, "no permission"); + pub fn ensure_owner(contract: H160, user: H160) -> DispatchResult { + ensure!(>::get(&contract) == user, Error::::NoPermission); Ok(()) } } --- a/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol +++ b/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol @@ -3,6 +3,12 @@ pragma solidity >=0.8.0 <0.9.0; +// Anonymous struct +struct Tuple0 { + address field_0; + uint256 field_1; +} + // Common stubs holder contract Dummy { uint8 dummy; @@ -21,8 +27,13 @@ } } -// Selector: 7b4866f9 +// Selector: 6073d917 contract ContractHelpers is Dummy, ERC165 { + // Get contract ovner + // + // @param Contract_address contract for which the owner is being determined. + // @return Contract owner. + // // Selector: contractOwner(address) 5152b14c function contractOwner(address contractAddress) public @@ -35,26 +46,111 @@ return 0x0000000000000000000000000000000000000000; } - // Selector: sponsoringEnabled(address) 6027dc61 - function sponsoringEnabled(address contractAddress) + // Set sponsor. + // + // @param contract_address Contract for which a sponsor is being established. + // @param sponsor User address who set as pending sponsor. + // + // Selector: setSponsor(address,address) f01fba93 + function setSponsor(address contractAddress, address sponsor) public { + require(false, stub_error); + contractAddress; + sponsor; + dummy = 0; + } + + // Set contract as self sponsored. + // + // @param contract_address Contract for which a self sponsoring is being enabled. + // + // Selector: selfSponsoredEnable(address) 89f7d9ae + function selfSponsoredEnable(address contractAddress) public { + require(false, stub_error); + contractAddress; + dummy = 0; + } + + // Remove sponsor. + // + // @param contract_address Contract for which a sponsorship is being removed. + // + // Selector: removeSponsor(address) ef784250 + function removeSponsor(address contractAddress) public { + require(false, stub_error); + contractAddress; + dummy = 0; + } + + // Confirm sponsorship. + // + // @dev Caller must be same that set via [`set_sponsor`]. + // + // @param contract_address Сontract for which need to confirm sponsorship. + // + // Selector: confirmSponsorship(address) abc00001 + function confirmSponsorship(address contractAddress) public { + require(false, stub_error); + contractAddress; + dummy = 0; + } + + // Get current sponsor. + // + // @param contract_address The contract for which a sponsor is requested. + // @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + // + // Selector: getSponsor(address) 743fc745 + function getSponsor(address contractAddress) public view - returns (bool) + returns (Tuple0 memory) { require(false, stub_error); contractAddress; dummy; + return Tuple0(0x0000000000000000000000000000000000000000, 0); + } + + // Check tat contract has confirmed sponsor. + // + // @param contract_address The contract for which the presence of a confirmed sponsor is checked. + // @return **true** if contract has confirmed sponsor. + // + // Selector: hasSponsor(address) 97418603 + function hasSponsor(address contractAddress) public view returns (bool) { + require(false, stub_error); + contractAddress; + dummy; return false; } - // Deprecated + // Check tat contract has pending sponsor. + // + // @param contract_address The contract for which the presence of a pending sponsor is checked. + // @return **true** if contract has pending sponsor. // - // Selector: toggleSponsoring(address,bool) fcac6d86 - function toggleSponsoring(address contractAddress, bool enabled) public { + // Selector: hasPendingSponsor(address) 39b9b242 + function hasPendingSponsor(address contractAddress) + public + view + returns (bool) + { + require(false, stub_error); + contractAddress; + dummy; + return false; + } + + // Selector: sponsoringEnabled(address) 6027dc61 + function sponsoringEnabled(address contractAddress) + public + view + returns (bool) + { require(false, stub_error); contractAddress; - enabled; - dummy = 0; + dummy; + return false; } // Selector: setSponsoringMode(address,uint8) fde8a560 --- a/runtime/tests/src/lib.rs +++ b/runtime/tests/src/lib.rs @@ -167,7 +167,7 @@ } #[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, TypeInfo, MaxEncodedLen)] -pub struct TestCrossAccountId(u64, sp_core::H160); +pub struct TestCrossAccountId(u64, sp_core::H160, bool); impl CrossAccountId for TestCrossAccountId { fn as_sub(&self) -> &u64 { &self.0 @@ -178,17 +178,20 @@ fn from_sub(sub: u64) -> Self { let mut eth = [0; 20]; eth[12..20].copy_from_slice(&sub.to_be_bytes()); - Self(sub, sp_core::H160(eth)) + Self(sub, sp_core::H160(eth), true) } fn from_eth(eth: sp_core::H160) -> Self { let mut sub_raw = [0; 8]; sub_raw.copy_from_slice(ð.0[0..8]); let sub = u64::from_be_bytes(sub_raw); - Self(sub, eth) + Self(sub, eth, false) } fn conv_eq(&self, other: &Self) -> bool { self.as_sub() == other.as_sub() } + fn is_canonical_substrate(&self) -> bool { + self.2 + } } impl Default for TestCrossAccountId { --- a/tests/src/eth/api/ContractHelpers.sol +++ b/tests/src/eth/api/ContractHelpers.sol @@ -3,6 +3,12 @@ pragma solidity >=0.8.0 <0.9.0; +// Anonymous struct +struct Tuple0 { + address field_0; + uint256 field_1; +} + // Common stubs holder interface Dummy { @@ -12,24 +18,85 @@ function supportsInterface(bytes4 interfaceID) external view returns (bool); } -// Selector: 7b4866f9 +// Selector: 6073d917 interface ContractHelpers is Dummy, ERC165 { + // Get contract ovner + // + // @param Contract_address contract for which the owner is being determined. + // @return Contract owner. + // // Selector: contractOwner(address) 5152b14c function contractOwner(address contractAddress) external view returns (address); + // Set sponsor. + // + // @param contract_address Contract for which a sponsor is being established. + // @param sponsor User address who set as pending sponsor. + // + // Selector: setSponsor(address,address) f01fba93 + function setSponsor(address contractAddress, address sponsor) external; + + // Set contract as self sponsored. + // + // @param contract_address Contract for which a self sponsoring is being enabled. + // + // Selector: selfSponsoredEnable(address) 89f7d9ae + function selfSponsoredEnable(address contractAddress) external; + + // Remove sponsor. + // + // @param contract_address Contract for which a sponsorship is being removed. + // + // Selector: removeSponsor(address) ef784250 + function removeSponsor(address contractAddress) external; + + // Confirm sponsorship. + // + // @dev Caller must be same that set via [`set_sponsor`]. + // + // @param contract_address Сontract for which need to confirm sponsorship. + // + // Selector: confirmSponsorship(address) abc00001 + function confirmSponsorship(address contractAddress) external; + + // Get current sponsor. + // + // @param contract_address The contract for which a sponsor is requested. + // @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + // + // Selector: getSponsor(address) 743fc745 + function getSponsor(address contractAddress) + external + view + returns (Tuple0 memory); + + // Check tat contract has confirmed sponsor. + // + // @param contract_address The contract for which the presence of a confirmed sponsor is checked. + // @return **true** if contract has confirmed sponsor. + // + // Selector: hasSponsor(address) 97418603 + function hasSponsor(address contractAddress) external view returns (bool); + + // Check tat contract has pending sponsor. + // + // @param contract_address The contract for which the presence of a pending sponsor is checked. + // @return **true** if contract has pending sponsor. + // + // Selector: hasPendingSponsor(address) 39b9b242 + function hasPendingSponsor(address contractAddress) + external + view + returns (bool); + // Selector: sponsoringEnabled(address) 6027dc61 function sponsoringEnabled(address contractAddress) external view returns (bool); - - // Deprecated - // - // Selector: toggleSponsoring(address,bool) fcac6d86 - function toggleSponsoring(address contractAddress, bool enabled) external; // Selector: setSponsoringMode(address,uint8) fde8a560 function setSponsoringMode(address contractAddress, uint8 mode) external; --- a/tests/src/eth/collectionSponsoring.test.ts +++ b/tests/src/eth/collectionSponsoring.test.ts @@ -1,7 +1,8 @@ -import {addToAllowListExpectSuccess, confirmSponsorshipExpectSuccess, createCollectionExpectSuccess, enablePublicMintingExpectSuccess, setCollectionSponsorExpectSuccess} from '../util/helpers'; -import {itWeb3, createEthAccount, collectionIdToAddress, GAS_ARGS, normalizeEvents} from './util/helpers'; +import {addToAllowListExpectSuccess, confirmSponsorshipExpectSuccess, createCollectionExpectSuccess, enablePublicMintingExpectSuccess, getDetailedCollectionInfo, setCollectionSponsorExpectSuccess} from '../util/helpers'; +import {itWeb3, createEthAccount, collectionIdToAddress, GAS_ARGS, normalizeEvents, createEthAccountWithBalance, evmCollectionHelpers, getCollectionAddressFromResult, evmCollection, ethBalanceViaSub} from './util/helpers'; import nonFungibleAbi from './nonFungibleAbi.json'; import {expect} from 'chai'; +import {evmToAddress} from '@polkadot/util-crypto'; describe('evm collection sponsoring', () => { itWeb3('sponsors mint transactions', async ({web3, privateKeyWrapper}) => { @@ -36,4 +37,130 @@ }, ]); }); + + itWeb3('Sponsoring collection from evm address via access list', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); + let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; + expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; + expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + + await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); + collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isConfirmed).to.be.true; + expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + + const user = createEthAccount(web3); + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + + const oldPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(oldPermissions.mintMode).to.be.false; + expect(oldPermissions.access).to.be.equal('Normal'); + + await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner}); + await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner}); + await collectionEvm.methods.setCollectionMintMode(true).send({from: owner}); + + const newPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(newPermissions.mintMode).to.be.true; + expect(newPermissions.access).to.be.equal('AllowList'); + + const ownerBalanceBefore = await ethBalanceViaSub(api, owner); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + + { + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + const result = await collectionEvm.methods.mintWithTokenURI( + user, + nextTokenId, + 'Test URI', + ).send({from: user}); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: nextTokenId, + }, + }, + ]); + + const ownerBalanceAfter = await ethBalanceViaSub(api, owner); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + + expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter); + expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true; + } + }); + + itWeb3('Check that transaction via EVM spend money from sponsor address', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + result = await collectionEvm.methods.setCollectionSponsor(sponsor).send(); + let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; + expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; + expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isConfirmed).to.be.true; + expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + + const user = createEthAccount(web3); + await collectionEvm.methods.addCollectionAdmin(user).send(); + + const ownerBalanceBefore = await ethBalanceViaSub(api, owner); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + + + const userCollectionEvm = evmCollection(web3, user, collectionIdAddress); + const nextTokenId = await userCollectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await userCollectionEvm.methods.mintWithTokenURI( + user, + nextTokenId, + 'Test URI', + ).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: nextTokenId, + }, + }, + ]); + expect(await userCollectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + + const ownerBalanceAfter = await ethBalanceViaSub(api, owner); + expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + }); }); --- a/tests/src/eth/contractSponsoring.test.ts +++ b/tests/src/eth/contractSponsoring.test.ts @@ -23,33 +23,35 @@ itWeb3, SponsoringMode, createEthAccount, - collectionIdToAddress, - GAS_ARGS, - normalizeEvents, - subToEth, - executeEthTxOnSub, - evmCollectionHelpers, - getCollectionAddressFromResult, - evmCollection, ethBalanceViaSub, } from './util/helpers'; -import { - addCollectionAdminExpectSuccess, - createCollectionExpectSuccess, - getDetailedCollectionInfo, - transferBalanceTo, -} from '../util/helpers'; -import nonFungibleAbi from './nonFungibleAbi.json'; -import getBalance from '../substrate/get-balance'; -import {evmToAddress} from '@polkadot/util-crypto'; describe('Sponsoring EVM contracts', () => { + itWeb3('Self sponsored can be set by the address that deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected; + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + }); + + itWeb3('Self sponsored can not be set by the address that did not deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.selfSponsoredEnable(flipper.options.address).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + itWeb3('Sponsoring can be set by the address that has deployed the contract', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const flipper = await deployFlipper(web3, owner); const helpers = contractHelpers(web3, owner); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; - await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); + await expect(helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner})).to.be.not.rejected; expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; }); @@ -59,11 +61,150 @@ const flipper = await deployFlipper(web3, owner); const helpers = contractHelpers(web3, owner); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; - await expect(helpers.methods.setSponsoringMode(notOwner, SponsoringMode.Allowlisted).send({from: notOwner})).to.rejected; + await expect(helpers.methods.setSponsoringMode(notOwner, SponsoringMode.Allowlisted).call({from: notOwner})).to.be.rejectedWith('NoPermission'); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; }); + + itWeb3('Sponsor can be set by the address that deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.setSponsor(flipper.options.address, sponsor).send()).to.be.not.rejected; + expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.true; + }); + + itWeb3('Sponsor can not be set by the address that did not deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.setSponsor(flipper.options.address, sponsor).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Sponsorship can be confirmed by the address that pending as sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.setSponsor(flipper.options.address, sponsor).send()).to.be.not.rejected; + await expect(helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor})).to.be.not.rejected; + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + }); + + itWeb3('Sponsorship can not be confirmed by the address that not pending as sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notSponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.setSponsor(flipper.options.address, sponsor).send()).to.be.not.rejected; + await expect(helpers.methods.confirmSponsorship(flipper.options.address).call({from: notSponsor})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Sponsorship can not be confirmed by the address that not set as sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notSponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.confirmSponsorship(flipper.options.address).call({from: notSponsor})).to.be.rejectedWith('NoPendingSponsor'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Get self sponsored sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + await helpers.methods.selfSponsoredEnable(flipper.options.address).send(); + + const result = await helpers.methods.getSponsor(flipper.options.address).call(); + + expect(result[0]).to.be.eq(flipper.options.address); + expect(result[1]).to.be.eq('0'); + }); + + itWeb3('Get confirmed sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + + const result = await helpers.methods.getSponsor(flipper.options.address).call(); + expect(result[0]).to.be.eq(sponsor); + expect(result[1]).to.be.eq('0'); + }); + + itWeb3('Sponsor can be removed by the address that deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + + await helpers.methods.removeSponsor(flipper.options.address).send(); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Sponsor can not be removed by the address that did not deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + + await expect(helpers.methods.removeSponsor(flipper.options.address).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + }); + itWeb3('In generous mode, non-allowlisted user transaction will be sponsored', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + + const flipper = await deployFlipper(web3, owner); + + const helpers = contractHelpers(web3, owner); + + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + + await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: owner}); + await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); + + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + const callerBalanceBefore = await ethBalanceViaSub(api, caller); + + await flipper.methods.flip().send({from: caller}); + expect(await flipper.methods.getValue().call()).to.be.true; + + // Balance should be taken from sponsor instead of caller + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + const callerBalanceAfter = await ethBalanceViaSub(api, caller); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + expect(callerBalanceAfter).to.be.eq(callerBalanceBefore); + }); + + itWeb3('In generous mode, non-allowlisted user transaction will be self sponsored', async ({api, web3, privateKeyWrapper}) => { const alice = privateKeyWrapper('//Alice'); const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); @@ -73,28 +214,29 @@ const helpers = contractHelpers(web3, owner); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; + await helpers.methods.selfSponsoredEnable(flipper.options.address).send(); + await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; await transferBalanceToEth(api, alice, flipper.options.address); - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); - expect(originalFlipperBalance).to.be.not.equal('0'); + const contractBalanceBefore = await ethBalanceViaSub(api, flipper.options.address); + const callerBalanceBefore = await ethBalanceViaSub(api, caller); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; - // Balance should be taken from flipper instead of caller - const balanceAfter = await web3.eth.getBalance(flipper.options.address); - expect(+balanceAfter).to.be.lessThan(+originalFlipperBalance); + // Balance should be taken from sponsor instead of caller + const contractBalanceAfter = await ethBalanceViaSub(api, flipper.options.address); + const callerBalanceAfter = await ethBalanceViaSub(api, caller); + expect(contractBalanceAfter < contractBalanceBefore).to.be.true; + expect(callerBalanceAfter).to.be.eq(callerBalanceBefore); }); itWeb3('Sponsoring is set, an address that has no UNQ can send a transaction and it works. Sponsor balance should decrease (allowlisted)', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = createEthAccount(web3); const flipper = await deployFlipper(web3, owner); @@ -103,22 +245,21 @@ await helpers.methods.toggleAllowlist(flipper.options.address, true).send({from: owner}); await helpers.methods.toggleAllowed(flipper.options.address, caller, true).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, flipper.options.address); + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); - expect(originalFlipperBalance).to.be.not.equal('0'); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + expect(sponsorBalanceBefore).to.be.not.equal('0'); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; // Balance should be taken from flipper instead of caller - const balanceAfter = await web3.eth.getBalance(flipper.options.address); - expect(+balanceAfter).to.be.lessThan(+originalFlipperBalance); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; }); itWeb3('Sponsoring is set, an address that has no UNQ can send a transaction and it works. Sponsor balance should not decrease (non-allowlisted)', async ({api, web3, privateKeyWrapper}) => { @@ -131,10 +272,8 @@ const helpers = contractHelpers(web3, owner); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; await transferBalanceToEth(api, alice, flipper.options.address); @@ -150,11 +289,9 @@ }); itWeb3('Sponsoring is set, an address that has UNQ can send a transaction and it works. User balance should not change', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const originalCallerBalance = await web3.eth.getBalance(caller); const flipper = await deployFlipper(web3, owner); @@ -162,26 +299,27 @@ await helpers.methods.toggleAllowlist(flipper.options.address, true).send({from: owner}); await helpers.methods.toggleAllowed(flipper.options.address, caller, true).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, flipper.options.address); + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); - expect(originalFlipperBalance).to.be.not.equal('0'); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + const callerBalanceBefore = await ethBalanceViaSub(api, caller); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; - expect(await web3.eth.getBalance(caller)).to.be.equals(originalCallerBalance); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + const callerBalanceAfter = await ethBalanceViaSub(api, caller); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + expect(callerBalanceAfter).to.be.equals(callerBalanceBefore); }); itWeb3('Sponsoring is limited, with setContractRateLimit. The limitation is working if transactions are sent more often, the sender pays the commission.', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const originalCallerBalance = await web3.eth.getBalance(caller); @@ -191,25 +329,24 @@ await helpers.methods.toggleAllowlist(flipper.options.address, true).send({from: owner}); await helpers.methods.toggleAllowed(flipper.options.address, caller, true).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 10).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, flipper.options.address); + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); + const originalFlipperBalance = await web3.eth.getBalance(sponsor); expect(originalFlipperBalance).to.be.not.equal('0'); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; expect(await web3.eth.getBalance(caller)).to.be.equals(originalCallerBalance); - const newFlipperBalance = await web3.eth.getBalance(flipper.options.address); + const newFlipperBalance = await web3.eth.getBalance(sponsor); expect(newFlipperBalance).to.be.not.equals(originalFlipperBalance); await flipper.methods.flip().send({from: caller}); - expect(await web3.eth.getBalance(flipper.options.address)).to.be.equal(newFlipperBalance); + expect(await web3.eth.getBalance(sponsor)).to.be.equal(newFlipperBalance); expect(await web3.eth.getBalance(caller)).to.be.not.equals(originalCallerBalance); }); @@ -219,131 +356,5 @@ const flipper = await deployFlipper(web3, owner); const helpers = contractHelpers(web3, owner); expect(await helpers.methods.getSponsoringRateLimit(flipper.options.address).call()).to.be.equals('7200'); - }); - - itWeb3('Sponsoring collection from evm address via access list', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionHelpers = evmCollectionHelpers(web3, owner); - let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); - const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); - const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); - let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; - expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; - expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); - - await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); - collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - expect(collectionSub.sponsorship.isConfirmed).to.be.true; - expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - - const user = createEthAccount(web3); - const nextTokenId = await collectionEvm.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - - const oldPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); - expect(oldPermissions.mintMode).to.be.false; - expect(oldPermissions.access).to.be.equal('Normal'); - - await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner}); - await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner}); - await collectionEvm.methods.setCollectionMintMode(true).send({from: owner}); - - const newPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); - expect(newPermissions.mintMode).to.be.true; - expect(newPermissions.access).to.be.equal('AllowList'); - - const ownerBalanceBefore = await ethBalanceViaSub(api, owner); - const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); - - { - const nextTokenId = await collectionEvm.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - const result = await collectionEvm.methods.mintWithTokenURI( - user, - nextTokenId, - 'Test URI', - ).send({from: user}); - const events = normalizeEvents(result.events); - - expect(events).to.be.deep.equal([ - { - address: collectionIdAddress, - event: 'Transfer', - args: { - from: '0x0000000000000000000000000000000000000000', - to: user, - tokenId: nextTokenId, - }, - }, - ]); - - const ownerBalanceAfter = await ethBalanceViaSub(api, owner); - const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); - - expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); - expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter); - expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true; - } - }); - - itWeb3('Check that transaction via EVM spend money from sponsor address', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionHelpers = evmCollectionHelpers(web3, owner); - let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); - const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); - const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - result = await collectionEvm.methods.setCollectionSponsor(sponsor).send(); - let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; - expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; - expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); - const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); - await sponsorCollection.methods.confirmCollectionSponsorship().send(); - collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - expect(collectionSub.sponsorship.isConfirmed).to.be.true; - expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - - const user = createEthAccount(web3); - await collectionEvm.methods.addCollectionAdmin(user).send(); - - const ownerBalanceBefore = await ethBalanceViaSub(api, owner); - const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); - - - const userCollectionEvm = evmCollection(web3, user, collectionIdAddress); - const nextTokenId = await userCollectionEvm.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - result = await userCollectionEvm.methods.mintWithTokenURI( - user, - nextTokenId, - 'Test URI', - ).send(); - - const events = normalizeEvents(result.events); - const address = collectionIdToAddress(collectionId); - - expect(events).to.be.deep.equal([ - { - address, - event: 'Transfer', - args: { - from: '0x0000000000000000000000000000000000000000', - to: user, - tokenId: nextTokenId, - }, - }, - ]); - expect(await userCollectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); - - const ownerBalanceAfter = await ethBalanceViaSub(api, owner); - expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); - const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); - expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; }); }); --- a/tests/src/eth/marketplace/marketplace.test.ts +++ b/tests/src/eth/marketplace/marketplace.test.ts @@ -39,6 +39,7 @@ describe('Matcher contract usage', () => { itWeb3('With UNQ', async ({api, web3, privateKeyWrapper}) => { const alice = privateKeyWrapper('//Alice'); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const matcherOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const matcherContract = new web3.eth.Contract(JSON.parse((await readFile(`${__dirname}/MarketPlace.abi`)).toString()), undefined, { from: matcherOwner, @@ -48,7 +49,9 @@ const helpers = contractHelpers(web3, matcherOwner); await helpers.methods.setSponsoringMode(matcher.options.address, SponsoringMode.Allowlisted).send({from: matcherOwner}); await helpers.methods.setSponsoringRateLimit(matcher.options.address, 1).send({from: matcherOwner}); - await transferBalanceToEth(api, alice, matcher.options.address); + + await helpers.methods.setSponsor(matcher.options.address, sponsor).send({from: matcherOwner}); + await helpers.methods.confirmSponsorship(matcher.options.address).send({from: sponsor}); const collectionId = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); await setCollectionLimitsExpectSuccess(alice, collectionId, {sponsorApproveTimeout: 1}); @@ -100,6 +103,7 @@ itWeb3('With escrow', async ({api, web3, privateKeyWrapper}) => { const alice = privateKeyWrapper('//Alice'); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const matcherOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const escrow = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const matcherContract = new web3.eth.Contract(JSON.parse((await readFile(`${__dirname}/MarketPlace.abi`)).toString()), undefined, { @@ -111,7 +115,9 @@ const helpers = contractHelpers(web3, matcherOwner); await helpers.methods.setSponsoringMode(matcher.options.address, SponsoringMode.Allowlisted).send({from: matcherOwner}); await helpers.methods.setSponsoringRateLimit(matcher.options.address, 1).send({from: matcherOwner}); - await transferBalanceToEth(api, alice, matcher.options.address); + + await helpers.methods.setSponsor(matcher.options.address, sponsor).send({from: matcherOwner}); + await helpers.methods.confirmSponsorship(matcher.options.address).send({from: sponsor}); const collectionId = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); await setCollectionLimitsExpectSuccess(alice, collectionId, {sponsorApproveTimeout: 1}); --- a/tests/src/eth/sponsoring.test.ts +++ b/tests/src/eth/sponsoring.test.ts @@ -15,13 +15,12 @@ // along with Unique Network. If not, see . import {expect} from 'chai'; -import {contractHelpers, createEthAccount, createEthAccountWithBalance, deployCollector, deployFlipper, itWeb3, SponsoringMode, transferBalanceToEth} from './util/helpers'; +import {contractHelpers, createEthAccount, createEthAccountWithBalance, deployCollector, deployFlipper, itWeb3, SponsoringMode} from './util/helpers'; describe('EVM sponsoring', () => { itWeb3('Fee is deducted from contract if sponsoring is enabled', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = createEthAccount(web3); const originalCallerBalance = await web3.eth.getBalance(caller); expect(originalCallerBalance).to.be.equal('0'); @@ -31,28 +30,29 @@ const helpers = contractHelpers(web3, owner); await helpers.methods.toggleAllowlist(flipper.options.address, true).send({from: owner}); await helpers.methods.toggleAllowed(flipper.options.address, caller, true).send({from: owner}); + + await helpers.methods.setSponsor(flipper.options.address, sponsor).send({from: owner}); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, flipper.options.address); - - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); - expect(originalFlipperBalance).to.be.not.equal('0'); + const originalSponsorBalance = await web3.eth.getBalance(sponsor); + expect(originalSponsorBalance).to.be.not.equal('0'); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; // Balance should be taken from flipper instead of caller expect(await web3.eth.getBalance(caller)).to.be.equals(originalCallerBalance); - expect(await web3.eth.getBalance(flipper.options.address)).to.be.not.equals(originalFlipperBalance); + expect(await web3.eth.getBalance(sponsor)).to.be.not.equals(originalSponsorBalance); }); + itWeb3('...but this doesn\'t applies to payable value', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const originalCallerBalance = await web3.eth.getBalance(caller); expect(originalCallerBalance).to.be.not.equal('0'); @@ -68,16 +68,17 @@ await helpers.methods.setSponsoringRateLimit(collector.options.address, 0).send({from: owner}); expect(await helpers.methods.sponsoringEnabled(collector.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, collector.options.address); + await helpers.methods.setSponsor(collector.options.address, sponsor).send({from: owner}); + await helpers.methods.confirmSponsorship(collector.options.address).send({from: sponsor}); - const originalCollectorBalance = await web3.eth.getBalance(collector.options.address); - expect(originalCollectorBalance).to.be.not.equal('0'); + const originalSponsorBalance = await web3.eth.getBalance(sponsor); + expect(originalSponsorBalance).to.be.not.equal('0'); await collector.methods.giveMoney().send({from: caller, value: '10000'}); // Balance will be taken from both caller (value) and from collector (fee) expect(await web3.eth.getBalance(caller)).to.be.equals((BigInt(originalCallerBalance) - 10000n).toString()); - expect(await web3.eth.getBalance(collector.options.address)).to.be.not.equals(originalCollectorBalance); + expect(await web3.eth.getBalance(sponsor)).to.be.not.equals(originalSponsorBalance); expect(await collector.methods.getCollected().call()).to.be.equal('10000'); }); }); --- a/tests/src/eth/util/contractHelpersAbi.json +++ b/tests/src/eth/util/contractHelpersAbi.json @@ -34,6 +34,19 @@ "type": "address" } ], + "name": "confirmSponsorship", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], "name": "contractOwner", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", @@ -47,6 +60,29 @@ "type": "address" } ], + "name": "getSponsor", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple0", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], "name": "getSponsoringRateLimit", "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], "stateMutability": "view", @@ -58,6 +94,72 @@ "internalType": "address", "name": "contractAddress", "type": "address" + } + ], + "name": "hasPendingSponsor", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "hasSponsor", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "removeSponsor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "selfSponsoredEnable", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { "internalType": "address", "name": "sponsor", "type": "address" } + ], + "name": "setSponsor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" }, { "internalType": "uint8", "name": "mode", "type": "uint8" } ], @@ -140,20 +242,6 @@ { "internalType": "bool", "name": "enabled", "type": "bool" } ], "name": "toggleAllowlist", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "contractAddress", - "type": "address" - }, - { "internalType": "bool", "name": "enabled", "type": "bool" } - ], - "name": "toggleSponsoring", "outputs": [], "stateMutability": "nonpayable", "type": "function" -- gitstuff