From b69837dd758f68c74a55a5288179a3a78c8838e0 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Thu, 07 Apr 2022 10:15:16 +0000 Subject: [PATCH] feat: budgets --- --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -33,7 +33,7 @@ COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, TokenId, CollectionStats, MAX_TOKEN_OWNERSHIP, CollectionMode, NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits, - CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit, + CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit, budget::Budget, }; pub use pallet::*; use sp_core::H160; @@ -763,6 +763,7 @@ to: T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo; fn burn_from( &self, @@ -770,6 +771,7 @@ from: T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo; fn set_variable_metadata( --- a/pallets/evm-coder-substrate/Cargo.toml +++ b/pallets/evm-coder-substrate/Cargo.toml @@ -17,6 +17,7 @@ frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" } frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" } frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" } +up-data-structs = { default-features = false, path = "../../primitives/data-structs" } [dependencies.codec] default-features = false --- a/pallets/evm-coder-substrate/src/lib.rs +++ b/pallets/evm-coder-substrate/src/lib.rs @@ -18,33 +18,43 @@ #[cfg(not(feature = "std"))] extern crate alloc; +#[cfg(not(feature = "std"))] +use alloc::format; +use frame_support::dispatch::Weight; + +use core::marker::PhantomData; +use sp_std::cell::RefCell; +use sp_std::vec::Vec; + +use frame_support::pallet_prelude::DispatchError; +use frame_support::traits::PalletInfo; +use frame_support::{ensure, sp_runtime::ModuleError}; +use up_data_structs::budget; +use pallet_evm::{ + ExitError, ExitRevert, ExitSucceed, GasWeightMapping, PrecompileFailure, PrecompileOutput, + PrecompileResult, runner::stack::MaybeMirroredLog, +}; +use ethereum::TransactionV2; +use sp_core::{H160, H256}; +use pallet_ethereum::EthereumTransactionSender; // #[cfg(feature = "runtime-benchmarks")] // pub mod benchmarking; +use evm_coder::{ + ToLog, + abi::{AbiReader, AbiWrite, AbiWriter}, + execution::{self, Result}, + types::{Msg, value}, +}; + pub use pallet::*; #[frame_support::pallet] pub mod pallet { - #[cfg(not(feature = "std"))] - use alloc::format; + use super::*; - use evm_coder::{ - ToLog, - abi::{AbiReader, AbiWrite, AbiWriter}, - execution::{self, Result}, - types::{Msg, value}, - }; - use frame_support::{ensure, sp_runtime::ModuleError}; - use pallet_evm::{ - ExitError, ExitRevert, ExitSucceed, GasWeightMapping, PrecompileFailure, PrecompileOutput, - PrecompileResult, runner::stack::MaybeMirroredLog, - }; use frame_system::ensure_signed; pub use frame_support::dispatch::DispatchResult; - use pallet_ethereum::EthereumTransactionSender; - use sp_std::cell::RefCell; - use sp_std::vec::Vec; - use sp_core::H160; use frame_support::{pallet_prelude::*, traits::PalletInfo}; use frame_system::pallet_prelude::*; @@ -76,214 +86,258 @@ Ok(()) } } +} - // From instabul hardfork configuration: https://github.com/rust-blockchain/evm/blob/fd4fd6acc0ca3208d6770fdb3ba407c94cdf97c6/runtime/src/lib.rs#L284 - pub const G_SLOAD_WORD: u64 = 800; - pub const G_SSTORE_WORD: u64 = 20000; +// From instabul hardfork configuration: https://github.com/rust-blockchain/evm/blob/fd4fd6acc0ca3208d6770fdb3ba407c94cdf97c6/runtime/src/lib.rs#L284 +pub const G_SLOAD_WORD: u64 = 800; +pub const G_SSTORE_WORD: u64 = 20000; - #[derive(Default)] - pub struct SubstrateRecorder { - contract: H160, - logs: RefCell>, - initial_gas: u64, - gas_limit: RefCell, - _phantom: PhantomData<*const T>, +pub fn generate_transaction() -> TransactionV2 { + use ethereum::{TransactionV0, TransactionAction, TransactionSignature}; + TransactionV2::Legacy(TransactionV0 { + nonce: 0.into(), + gas_price: 0.into(), + gas_limit: 0.into(), + action: TransactionAction::Call(H160([0; 20])), + value: 0.into(), + // zero selector, this transaction always has same sender, so all data should be acquired from logs + input: Vec::from([0, 0, 0, 0]), + // if v is not 27 - then we need to pass some other validity checks + signature: TransactionSignature::new(27, H256([0x88; 32]), H256([0x88; 32])).unwrap(), + }) +} + +pub struct GasCallsBudget<'r, T: Config> { + recorder: &'r SubstrateRecorder, + gas_per_call: u64, +} +impl budget::Budget for GasCallsBudget<'_, T> { + fn consume_custom(&self, calls: u32) -> bool { + let (gas, overflown) = (calls as u64).overflowing_add(self.gas_per_call); + if overflown { + return false; + } + self.recorder.consume_gas(gas).is_ok() } +} - impl SubstrateRecorder { - pub fn new(contract: H160, gas_limit: u64) -> Self { - Self { - contract, - logs: RefCell::new(Vec::new()), - initial_gas: gas_limit, - gas_limit: RefCell::new(gas_limit), - _phantom: PhantomData, - } +#[derive(Default)] +pub struct SubstrateRecorder { + contract: H160, + logs: RefCell>, + initial_gas: u64, + gas_limit: RefCell, + _phantom: PhantomData<*const T>, +} + +impl SubstrateRecorder { + pub fn new(contract: H160, gas_limit: u64) -> Self { + Self { + contract, + logs: RefCell::new(Vec::new()), + initial_gas: gas_limit, + gas_limit: RefCell::new(gas_limit), + _phantom: PhantomData, } + } + + pub fn is_empty(&self) -> bool { + self.logs.borrow().is_empty() + } + // Logs emitted with log_direct appear as substrate evm.Log event + pub fn log_direct(&self, log: impl ToLog) { + self.logs + .borrow_mut() + .push(MaybeMirroredLog::direct(log.to_log(self.contract))) + } + /// If log already has substrate equivalent - then we don't need to emit evm.Log + pub fn log_mirrored(&self, log: impl ToLog) { + self.logs + .borrow_mut() + .push(MaybeMirroredLog::mirrored(log.to_log(self.contract))) + } + pub fn retrieve_logs(self) -> Vec { + self.logs.into_inner() + } - pub fn is_empty(&self) -> bool { - self.logs.borrow().is_empty() - } - // Logs emitted with log_direct appear as substrate evm.Log event - pub fn log_direct(&self, log: impl ToLog) { - self.logs - .borrow_mut() - .push(MaybeMirroredLog::direct(log.to_log(self.contract))) - } - /// If log already has substrate equivalent - then we don't need to emit evm.Log - pub fn log_mirrored(&self, log: impl ToLog) { - self.logs - .borrow_mut() - .push(MaybeMirroredLog::mirrored(log.to_log(self.contract))) + pub fn gas_left(&self) -> u64 { + *self.gas_limit.borrow() + } + pub fn gas_calls_budget(&self, gas_per_call: u64) -> GasCallsBudget { + GasCallsBudget { + recorder: self, + gas_per_call, } - pub fn retrieve_logs(self) -> Vec { - self.logs.into_inner() + } + pub fn weight_calls_budget(&self, weight_per_call: Weight) -> GasCallsBudget { + GasCallsBudget { + recorder: self, + gas_per_call: T::GasWeightMapping::weight_to_gas(weight_per_call), } + } + pub fn consume_sload_sub(&self) -> DispatchResult { + self.consume_gas_sub(G_SLOAD_WORD) + } + pub fn consume_sstores_sub(&self, amount: usize) -> DispatchResult { + self.consume_gas_sub(G_SSTORE_WORD.saturating_mul(amount as u64)) + } + pub fn consume_sstore_sub(&self) -> DispatchResult { + self.consume_gas_sub(G_SSTORE_WORD) + } + pub fn consume_gas_sub(&self, gas: u64) -> DispatchResult { + ensure!(gas != u64::MAX, Error::::OutOfGas); + let mut gas_limit = self.gas_limit.borrow_mut(); + ensure!(gas <= *gas_limit, Error::::OutOfGas); + *gas_limit -= gas; + Ok(()) + } - pub fn gas_left(&self) -> u64 { - *self.gas_limit.borrow() + pub fn consume_sload(&self) -> Result<()> { + self.consume_gas(G_SLOAD_WORD) + } + pub fn consume_sstore(&self) -> Result<()> { + self.consume_gas(G_SSTORE_WORD) + } + pub fn consume_gas(&self, gas: u64) -> Result<()> { + if gas == u64::MAX { + return Err(execution::Error::Error(ExitError::OutOfGas)); } - pub fn consume_sload_sub(&self) -> DispatchResult { - self.consume_gas_sub(G_SLOAD_WORD) + let mut gas_limit = self.gas_limit.borrow_mut(); + if gas > *gas_limit { + return Err(execution::Error::Error(ExitError::OutOfGas)); } - pub fn consume_sstores_sub(&self, amount: usize) -> DispatchResult { - self.consume_gas_sub(G_SSTORE_WORD.saturating_mul(amount as u64)) - } - pub fn consume_sstore_sub(&self) -> DispatchResult { - self.consume_gas_sub(G_SSTORE_WORD) - } - pub fn consume_gas_sub(&self, gas: u64) -> DispatchResult { - ensure!(gas != u64::MAX, Error::::OutOfGas); - let mut gas_limit = self.gas_limit.borrow_mut(); - ensure!(gas <= *gas_limit, Error::::OutOfGas); - *gas_limit -= gas; - Ok(()) - } + *gas_limit -= gas; + Ok(()) + } + pub fn return_gas(&self, gas: u64) { + let mut gas_limit = self.gas_limit.borrow_mut(); + *gas_limit += gas; + } - pub fn consume_sload(&self) -> Result<()> { - self.consume_gas(G_SLOAD_WORD) - } - pub fn consume_sstore(&self) -> Result<()> { - self.consume_gas(G_SSTORE_WORD) - } - pub fn consume_gas(&self, gas: u64) -> Result<()> { - if gas == u64::MAX { - return Err(execution::Error::Error(ExitError::OutOfGas)); - } - let mut gas_limit = self.gas_limit.borrow_mut(); - if gas > *gas_limit { - return Err(execution::Error::Error(ExitError::OutOfGas)); - } - *gas_limit -= gas; - Ok(()) - } - pub fn return_gas(&self, gas: u64) { - let mut gas_limit = self.gas_limit.borrow_mut(); - *gas_limit += gas; - } + pub fn evm_to_precompile_output( + self, + result: evm_coder::execution::Result>, + ) -> Option { + use evm_coder::execution::Error; + Some(match result { + Ok(Some(v)) => Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + cost: self.initial_gas - self.gas_left(), + // TODO: preserve mirroring status + logs: self.retrieve_logs().into_iter().map(|l| l.log).collect(), + output: v.finish(), + }), + Ok(None) => return None, + Err(Error::Revert(e)) => { + let mut writer = AbiWriter::new_call(evm_coder::fn_selector!(Error(string))); + (&e as &str).abi_write(&mut writer); - pub fn evm_to_precompile_output( - self, - result: evm_coder::execution::Result>, - ) -> Option { - use evm_coder::execution::Error; - Some(match result { - Ok(Some(v)) => Ok(PrecompileOutput { - exit_status: ExitSucceed::Returned, + Err(PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, cost: self.initial_gas - self.gas_left(), - // TODO: preserve mirroring status - logs: self.retrieve_logs().into_iter().map(|l| l.log).collect(), - output: v.finish(), - }), - Ok(None) => return None, - Err(Error::Revert(e)) => { - let mut writer = AbiWriter::new_call(evm_coder::fn_selector!(Error(string))); - (&e as &str).abi_write(&mut writer); + output: writer.finish(), + }) + } + Err(Error::Fatal(f)) => Err(f.into()), + Err(Error::Error(e)) => Err(e.into()), + }) + } - Err(PrecompileFailure::Revert { - exit_status: ExitRevert::Reverted, - cost: self.initial_gas - self.gas_left(), - output: writer.finish(), - }) - } - Err(Error::Fatal(f)) => Err(f.into()), - Err(Error::Error(e)) => Err(e.into()), - }) + pub fn submit_logs(self) { + let logs = self.retrieve_logs(); + if logs.is_empty() { + return; } + T::EthereumTransactionSender::submit_logs_transaction( + Default::default(), + generate_transaction(), + logs, + ) + } +} - pub fn submit_logs(self) { - let logs = self.retrieve_logs(); - if logs.is_empty() { - return; +pub fn dispatch_to_evm(err: DispatchError) -> evm_coder::execution::Error { + use evm_coder::execution::Error as ExError; + match err { + DispatchError::Module(ModuleError { index, error, .. }) + if index + == T::PalletInfo::index::>() + .expect("evm-coder-substrate is a pallet, which should be added to runtime") + as u8 => + { + match error { + v if v == Error::::OutOfGas.as_u8() => ExError::Error(ExitError::OutOfGas), + v if v == Error::::OutOfFund.as_u8() => ExError::Error(ExitError::OutOfFund), + _ => unreachable!("this pallet only defines two possible errors"), } - T::EthereumTransactionSender::submit_logs_transaction(Default::default(), logs) } + DispatchError::Module(ModuleError { + message: Some(msg), .. + }) => ExError::Revert(msg.into()), + DispatchError::Module(ModuleError { index, error, .. }) => { + ExError::Revert(format!("error {} in pallet {}", error, index)) + } + e => ExError::Revert(format!("substrate error: {:?}", e)), } +} + +pub trait WithRecorder { + fn recorder(&self) -> &SubstrateRecorder; + fn into_recorder(self) -> SubstrateRecorder; +} - pub fn dispatch_to_evm(err: DispatchError) -> evm_coder::execution::Error { - use evm_coder::execution::Error as ExError; - match err { - DispatchError::Module(ModuleError { index, error, .. }) - if index - == T::PalletInfo::index::>() - .expect("evm-coder-substrate is a pallet, which should be added to runtime") - as u8 => - { - let mut read = &error as &[u8]; - match Error::::decode(&mut read) { - Ok(Error::::OutOfGas) => ExError::Error(ExitError::OutOfGas), - Ok(Error::::OutOfFund) => ExError::Error(ExitError::OutOfFund), - _ => unreachable!("this pallet only defines two possible errors"), - } - } - DispatchError::Module(ModuleError { - message: Some(msg), .. - }) => ExError::Revert(msg.into()), - DispatchError::Module(ModuleError { index, error, .. }) => { - ExError::Revert(format!("error {:?} in pallet {}", error, index)) - } - e => ExError::Revert(format!("substrate error: {:?}", e)), - } - } +/// Helper to simplify implementing bridge between evm-coder definitions and pallet-evm +pub fn call< + T: Config, + C: evm_coder::Call + evm_coder::Weighted, + E: evm_coder::Callable + WithRecorder, +>( + caller: H160, + mut e: E, + value: value, + input: &[u8], +) -> Option { + let result = call_internal(caller, &mut e, value, input); + e.into_recorder().evm_to_precompile_output(result) +} - pub trait WithRecorder { - fn recorder(&self) -> &SubstrateRecorder; - fn into_recorder(self) -> SubstrateRecorder; +fn call_internal< + T: Config, + C: evm_coder::Call + evm_coder::Weighted, + E: evm_coder::Callable + WithRecorder, +>( + caller: H160, + e: &mut E, + value: value, + input: &[u8], +) -> evm_coder::execution::Result> { + let (selector, mut reader) = AbiReader::new_call(input)?; + let call = C::parse(selector, &mut reader)?; + if call.is_none() { + return Ok(None); } + let call = call.unwrap(); - /// Helper to simplify implementing bridge between evm-coder definitions and pallet-evm - pub fn call< - T: Config, - C: evm_coder::Call + evm_coder::Weighted, - E: evm_coder::Callable + WithRecorder, - >( - caller: H160, - mut e: E, - value: value, - input: &[u8], - ) -> Option { - let result = call_internal(caller, &mut e, value, input); - e.into_recorder().evm_to_precompile_output(result) - } + let dispatch_info = call.weight(); + e.recorder() + .consume_gas(T::GasWeightMapping::weight_to_gas(dispatch_info.weight))?; - fn call_internal< - T: Config, - C: evm_coder::Call + evm_coder::Weighted, - E: evm_coder::Callable + WithRecorder, - >( - caller: H160, - e: &mut E, - value: value, - input: &[u8], - ) -> evm_coder::execution::Result> { - let (selector, mut reader) = AbiReader::new_call(input)?; - let call = C::parse(selector, &mut reader)?; - if call.is_none() { - return Ok(None); + match e.call(Msg { + call, + caller, + value, + }) { + Ok(v) => { + let unspent = v.post_info.calc_unspent(&dispatch_info); + e.recorder() + .return_gas(T::GasWeightMapping::weight_to_gas(unspent)); + Ok(Some(v.data)) } - let call = call.unwrap(); - - let dispatch_info = call.weight(); - e.recorder() - .consume_gas(T::GasWeightMapping::weight_to_gas(dispatch_info.weight))?; - - match e.call(Msg { - call, - caller, - value, - }) { - Ok(v) => { - let unspent = v.post_info.calc_unspent(&dispatch_info); - e.recorder() - .return_gas(T::GasWeightMapping::weight_to_gas(unspent)); - Ok(Some(v.data)) - } - Err(v) => { - let unspent = v.post_info.calc_unspent(&dispatch_info); - e.recorder() - .return_gas(T::GasWeightMapping::weight_to_gas(unspent)); - Err(v.data) - } + Err(v) => { + let unspent = v.post_info.calc_unspent(&dispatch_info); + e.recorder() + .return_gas(T::GasWeightMapping::weight_to_gas(unspent)); + Err(v.data) } } } --- a/pallets/fungible/src/benchmarking.rs +++ b/pallets/fungible/src/benchmarking.rs @@ -20,7 +20,7 @@ use sp_std::prelude::*; use pallet_common::benchmarking::create_collection_raw; use frame_benchmarking::{benchmarks, account}; -use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH}; +use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, budget::Unlimited}; use pallet_common::bench_init; const SEED: u32 = 1; @@ -52,7 +52,7 @@ bench_init!(to: cross_sub(i);); (to, 200) }).collect::>().try_into().unwrap(); - }: {>::create_multiple_items(&collection, &sender, data)} + }: {>::create_multiple_items(&collection, &sender, data)?} burn_item { bench_init!{ @@ -85,7 +85,7 @@ }; >::create_item(&collection, &owner, (sender.clone(), 200))?; >::set_allowance(&collection, &sender, &spender, 200)?; - }: {>::transfer_from(&collection, &spender, &sender, &receiver, 100)?} + }: {>::transfer_from(&collection, &spender, &sender, &receiver, 100, &Unlimited)?} burn_from { bench_init!{ @@ -94,5 +94,5 @@ }; >::create_item(&collection, &owner, (sender.clone(), 200))?; >::set_allowance(&collection, &sender, &burner, 200)?; - }: {>::burn_from(&collection, &burner, &sender, 100)?} + }: {>::burn_from(&collection, &burner, &sender, 100, &Unlimited)?} } --- a/pallets/fungible/src/common.rs +++ b/pallets/fungible/src/common.rs @@ -17,7 +17,7 @@ use core::marker::PhantomData; use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight, BoundedVec}; -use up_data_structs::{TokenId, CreateItemExData}; +use up_data_structs::{TokenId, CreateItemExData, budget::Budget}; use pallet_common::{CommonCollectionOperations, CommonWeightInfo, with_weight}; use sp_runtime::ArithmeticError; use sp_std::{vec::Vec, vec}; @@ -189,6 +189,7 @@ to: T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { ensure!( token == TokenId::default(), @@ -196,7 +197,7 @@ ); with_weight( - >::transfer_from(self, &sender, &from, &to, amount), + >::transfer_from(self, &sender, &from, &to, amount, nesting_budget), >::transfer_from(), ) } @@ -207,6 +208,7 @@ from: T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { ensure!( token == TokenId::default(), @@ -214,7 +216,7 @@ ); with_weight( - >::burn_from(self, &sender, &from, amount), + >::burn_from(self, &sender, &from, amount, nesting_budget), >::burn_from(), ) } --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -23,6 +23,7 @@ use sp_std::vec::Vec; use pallet_evm::account::CrossAccountId; use pallet_evm_coder_substrate::{call, dispatch_to_evm}; +use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _}; use crate::{ Allowance, Balance, Config, FungibleHandle, Pallet, SelfWeightOf, TotalSupply, @@ -96,8 +97,11 @@ let from = T::CrossAccountId::from_eth(from); let to = T::CrossAccountId::from_eth(to); let amount = amount.try_into().map_err(|_| "amount overflow")?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); - >::transfer_from(self, &caller, &from, &to, amount) + >::transfer_from(self, &caller, &from, &to, amount, &budget) .map_err(dispatch_to_evm::)?; Ok(true) } @@ -127,8 +131,12 @@ let caller = T::CrossAccountId::from_eth(caller); let from = T::CrossAccountId::from_eth(from); let amount = amount.try_into().map_err(|_| "amount overflow")?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); - >::burn_from(self, &caller, &from, amount).map_err(dispatch_to_evm::)?; + >::burn_from(self, &caller, &from, amount, &budget) + .map_err(dispatch_to_evm::)?; Ok(true) } } --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -21,6 +21,7 @@ use pallet_evm::account::CrossAccountId; use up_data_structs::{ AccessMode, CollectionId, TokenId, CreateCollectionData, mapping::TokenAddressMapping, + budget::Budget, }; use pallet_common::{ Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, @@ -361,6 +362,7 @@ spender: &T::CrossAccountId, from: &T::CrossAccountId, amount: u128, + nesting_budget: &dyn Budget, ) -> Result, DispatchError> { if spender.conv_eq(from) { return Ok(None); @@ -372,7 +374,12 @@ if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) { // TODO: should collection owner be allowed to perform this transfer? ensure!( - >::indirectly_owned(spender.clone(), source.0, source.1, 1)?, + >::indirectly_owned( + spender.clone(), + source.0, + source.1, + nesting_budget + )?, >::ApprovedValueTooLow, ); return Ok(None); @@ -394,8 +401,9 @@ from: &T::CrossAccountId, to: &T::CrossAccountId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResult { - let allowance = Self::check_allowed(collection, spender, from, amount)?; + let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?; // ========= @@ -411,8 +419,9 @@ spender: &T::CrossAccountId, from: &T::CrossAccountId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResult { - let allowance = Self::check_allowed(collection, spender, from, amount)?; + let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?; // ========= --- a/pallets/nonfungible/src/benchmarking.rs +++ b/pallets/nonfungible/src/benchmarking.rs @@ -20,7 +20,7 @@ use sp_std::prelude::*; use pallet_common::benchmarking::{create_collection_raw, create_data, create_var_data}; use frame_benchmarking::{benchmarks, account}; -use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, CUSTOM_DATA_LIMIT}; +use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, CUSTOM_DATA_LIMIT, budget::Unlimited}; use pallet_common::bench_init; use core::convert::TryInto; @@ -115,7 +115,7 @@ }; let item = create_max_item(&collection, &owner, sender.clone())?; >::set_allowance(&collection, &sender, item, Some(&spender))?; - }: {>::transfer_from(&collection, &spender, &sender, &receiver, item)?} + }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, &Unlimited)?} burn_from { bench_init!{ @@ -124,7 +124,7 @@ }; let item = create_max_item(&collection, &owner, sender.clone())?; >::set_allowance(&collection, &sender, item, Some(&burner))?; - }: {>::burn_from(&collection, &burner, &sender, item)?} + }: {>::burn_from(&collection, &burner, &sender, item, &Unlimited)?} set_variable_metadata { let b in 0..CUSTOM_DATA_LIMIT; --- a/pallets/nonfungible/src/common.rs +++ b/pallets/nonfungible/src/common.rs @@ -17,7 +17,7 @@ use core::marker::PhantomData; use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight, BoundedVec}; -use up_data_structs::{TokenId, CustomDataLimit, CreateItemExData, CollectionId}; +use up_data_structs::{TokenId, CustomDataLimit, CreateItemExData, CollectionId, budget::Budget}; use pallet_common::{CommonCollectionOperations, CommonWeightInfo, with_weight}; use sp_runtime::DispatchError; use sp_std::vec::Vec; @@ -192,12 +192,13 @@ to: T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { ensure!(amount <= 1, >::NonfungibleItemsHaveNoAmount); if amount == 1 { with_weight( - >::transfer_from(self, &sender, &from, &to, token), + >::transfer_from(self, &sender, &from, &to, token, nesting_budget), >::transfer_from(), ) } else { @@ -211,12 +212,13 @@ from: T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { ensure!(amount <= 1, >::NonfungibleItemsHaveNoAmount); if amount == 1 { with_weight( - >::burn_from(self, &sender, &from, token), + >::burn_from(self, &sender, &from, token, nesting_budget), >::burn_from(), ) } else { --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -30,6 +30,7 @@ }; use pallet_evm::account::CrossAccountId; use pallet_evm_coder_substrate::call; +use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _}; use crate::{ AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted, @@ -180,8 +181,11 @@ let from = T::CrossAccountId::from_eth(from); let to = T::CrossAccountId::from_eth(to); let token = token_id.try_into()?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); - >::transfer_from(self, &caller, &from, &to, token) + >::transfer_from(self, &caller, &from, &to, token, &budget) .map_err(dispatch_to_evm::)?; Ok(()) } @@ -350,8 +354,12 @@ let caller = T::CrossAccountId::from_eth(caller); let from = T::CrossAccountId::from_eth(from); let token = token_id.try_into()?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); - >::burn_from(self, &caller, &from, token).map_err(dispatch_to_evm::)?; + >::burn_from(self, &caller, &from, token, &budget) + .map_err(dispatch_to_evm::)?; Ok(()) } --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -20,7 +20,7 @@ use frame_support::{BoundedVec, ensure, fail}; use up_data_structs::{ AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData, - mapping::TokenAddressMapping, NestingRule, + mapping::TokenAddressMapping, NestingRule, budget::Budget, }; use pallet_evm::account::CrossAccountId; use pallet_common::{ @@ -511,6 +511,7 @@ spender: &T::CrossAccountId, from: &T::CrossAccountId, token: TokenId, + nesting_budget: &dyn Budget, ) -> DispatchResult { if spender.conv_eq(from) { return Ok(()); @@ -522,7 +523,12 @@ if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) { // TODO: should collection owner be allowed to perform this transfer? ensure!( - >::indirectly_owned(spender.clone(), source.0, source.1, 1)?, + >::indirectly_owned( + spender.clone(), + source.0, + source.1, + nesting_budget + )?, >::ApprovedValueTooLow, ); return Ok(()); @@ -543,8 +549,9 @@ from: &T::CrossAccountId, to: &T::CrossAccountId, token: TokenId, + nesting_budget: &dyn Budget, ) -> DispatchResult { - Self::check_allowed(collection, spender, from, token)?; + Self::check_allowed(collection, spender, from, token, nesting_budget)?; // ========= @@ -557,8 +564,9 @@ spender: &T::CrossAccountId, from: &T::CrossAccountId, token: TokenId, + nesting_budget: &dyn Budget, ) -> DispatchResult { - Self::check_allowed(collection, spender, from, token)?; + Self::check_allowed(collection, spender, from, token, nesting_budget)?; // ========= --- a/pallets/refungible/src/benchmarking.rs +++ b/pallets/refungible/src/benchmarking.rs @@ -20,7 +20,7 @@ use sp_std::prelude::*; use pallet_common::benchmarking::{create_collection_raw, create_data, create_var_data}; use frame_benchmarking::{benchmarks, account}; -use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, CUSTOM_DATA_LIMIT}; +use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, CUSTOM_DATA_LIMIT, budget::Unlimited}; use pallet_common::bench_init; use core::convert::TryInto; use core::iter::IntoIterator; @@ -165,7 +165,7 @@ }; let item = create_max_item(&collection, &owner, [(sender.clone(), 200), (receiver.clone(), 200)])?; >::set_allowance(&collection, &sender, &spender, item, 100)?; - }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, 100)?} + }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, 100, &Unlimited)?} // Target account is created transfer_from_creating { bench_init!{ @@ -174,7 +174,7 @@ }; let item = create_max_item(&collection, &owner, [(sender.clone(), 200)])?; >::set_allowance(&collection, &sender, &spender, item, 100)?; - }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, 100)?} + }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, 100, &Unlimited)?} // Source account is destroyed transfer_from_removing { bench_init!{ @@ -183,7 +183,7 @@ }; let item = create_max_item(&collection, &owner, [(sender.clone(), 200), (receiver.clone(), 200)])?; >::set_allowance(&collection, &sender, &spender, item, 200)?; - }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, 200)?} + }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, 200, &Unlimited)?} // Source account destroyed, target created transfer_from_creating_removing { bench_init!{ @@ -192,7 +192,7 @@ }; let item = create_max_item(&collection, &owner, [(sender.clone(), 200)])?; >::set_allowance(&collection, &sender, &spender, item, 200)?; - }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, 200)?} + }: {>::transfer_from(&collection, &spender, &sender, &receiver, item, 200, &Unlimited)?} // Both source account and token is destroyed burn_from { @@ -202,7 +202,7 @@ }; let item = create_max_item(&collection, &owner, [(sender.clone(), 200)])?; >::set_allowance(&collection, &sender, &burner, item, 200)?; - }: {>::burn_from(&collection, &burner, &sender, item, 200)?} + }: {>::burn_from(&collection, &burner, &sender, item, 200, &Unlimited)?} set_variable_metadata { let b in 0..CUSTOM_DATA_LIMIT; --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -18,7 +18,9 @@ use sp_std::collections::btree_map::BTreeMap; use frame_support::{dispatch::DispatchResultWithPostInfo, fail, weights::Weight, BoundedVec}; -use up_data_structs::{TokenId, CustomDataLimit, CreateItemExData, CreateRefungibleExData}; +use up_data_structs::{ + TokenId, CustomDataLimit, CreateItemExData, CreateRefungibleExData, budget::Budget, +}; use pallet_common::{CommonCollectionOperations, CommonWeightInfo, with_weight}; use sp_runtime::DispatchError; use sp_std::{vec::Vec, vec}; @@ -210,9 +212,10 @@ to: T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { with_weight( - >::transfer_from(self, &sender, &from, &to, token, amount), + >::transfer_from(self, &sender, &from, &to, token, amount, nesting_budget), >::transfer_from(), ) } @@ -223,9 +226,10 @@ from: T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { with_weight( - >::burn_from(self, &sender, &from, token, amount), + >::burn_from(self, &sender, &from, token, amount, nesting_budget), >::burn_from(), ) } --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -19,7 +19,7 @@ use frame_support::{ensure, BoundedVec}; use up_data_structs::{ AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId, - CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, + CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget, }; use pallet_evm::account::CrossAccountId; use pallet_common::{ @@ -544,6 +544,7 @@ from: &T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> Result, DispatchError> { if spender.conv_eq(from) { return Ok(None); @@ -555,7 +556,12 @@ if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) { // TODO: should collection owner be allowed to perform this transfer? ensure!( - >::indirectly_owned(spender.clone(), source.0, source.1, 1)?, + >::indirectly_owned( + spender.clone(), + source.0, + source.1, + nesting_budget + )?, >::ApprovedValueTooLow, ); return Ok(None); @@ -578,8 +584,10 @@ to: &T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResult { - let allowance = Self::check_allowed(collection, spender, from, token, amount)?; + let allowance = + Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?; // ========= @@ -596,8 +604,10 @@ from: &T::CrossAccountId, token: TokenId, amount: u128, + nesting_budget: &dyn Budget, ) -> DispatchResult { - let allowance = Self::check_allowed(collection, spender, from, token, amount)?; + let allowance = + Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?; // ========= --- a/pallets/structure/src/lib.rs +++ b/pallets/structure/src/lib.rs @@ -6,8 +6,14 @@ use frame_support::fail; pub use pallet::*; use pallet_common::{dispatch::CollectionDispatch, CollectionHandle}; -use up_data_structs::{CollectionId, TokenId, mapping::TokenAddressMapping}; +use up_data_structs::{CollectionId, TokenId, mapping::TokenAddressMapping, budget::Budget}; +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; +pub mod weights; + +pub type SelfWeightOf = ::WeightInfo; + #[frame_support::pallet] pub mod pallet { use frame_support::Parameter; @@ -35,6 +41,7 @@ #[pallet::config] pub trait Config: frame_system::Config + pallet_common::Config { + type WeightInfo: weights::WeightInfo; type Event: IsType<::Event> + From>; type Call: Parameter + UnfilteredDispatchable + GetDispatchInfo; } @@ -127,10 +134,10 @@ pub fn find_topmost_owner( collection: CollectionId, token: TokenId, - max_depth: u32, + budget: &dyn Budget, ) -> Result { let owner = Self::parent_chain(collection, token) - .take(max_depth as usize) + .take_while(|_| budget.consume()) .find(|p| matches!(p, Ok(Parent::Normal(_) | Parent::TokenNotFound))) .ok_or(>::DepthLimit)??; @@ -145,7 +152,7 @@ user: T::CrossAccountId, collection: CollectionId, token: TokenId, - max_depth: u32, + budget: &dyn Budget, ) -> Result { let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) { Some((collection, token)) => Parent::Token(collection, token), @@ -153,7 +160,7 @@ }; Ok(Self::parent_chain(collection, token) - .take(max_depth as usize) + .take_while(|_| budget.consume()) .any(|parent| Ok(&target_parent) == parent.as_ref())) } } --- a/pallets/unique/src/benchmarking.rs +++ b/pallets/unique/src/benchmarking.rs @@ -19,8 +19,8 @@ use super::*; use crate::Pallet; use frame_system::RawOrigin; +use frame_support::traits::{tokens::currency::Currency, Get}; use frame_benchmarking::{benchmarks, account}; -use up_data_structs::*; use sp_runtime::DispatchError; use pallet_common::benchmarking::{create_data, create_var_data, create_u16_data}; @@ -173,6 +173,7 @@ owner_can_transfer: Some(true), sponsored_data_rate_limit: None, transfers_enabled: Some(true), + nesting_rule: None, }; }: set_collection_limits(RawOrigin::Signed(caller.clone()), collection, cl) --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -43,7 +43,7 @@ MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH, AccessMode, CreateItemData, CollectionLimits, CollectionId, CollectionMode, TokenId, SchemaVersion, SponsorshipState, MetaUpdatePermission, CreateCollectionData, CustomDataLimit, - CreateItemExData, + CreateItemExData, budget, }; use pallet_evm::account::CrossAccountId; use pallet_common::{ @@ -807,8 +807,9 @@ #[transactional] pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); + let budget = budget::Value::new(2); - dispatch_call::(collection_id, |d| d.burn_from(sender, from, item_id, value)) + dispatch_call::(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget)) } /// Change ownership of the token. @@ -888,8 +889,9 @@ #[transactional] pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); + let budget = budget::Value::new(2); - dispatch_call::(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value)) + dispatch_call::(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget)) } /// Set off-chain data schema. --- /dev/null +++ b/primitives/data-structs/src/budget.rs @@ -0,0 +1,38 @@ +use core::cell::Cell; + +pub trait Budget { + /// Returns true while not exceeded + fn consume(&self) -> bool { + self.consume_custom(1) + } + /// Returns true while not exceeded + /// Implementations should use interior mutabilitiy + fn consume_custom(&self, calls: u32) -> bool; +} + +pub struct Unlimited; +impl Budget for Unlimited { + fn consume_custom(&self, _calls: u32) -> bool { + true + } +} + +pub struct Value(Cell); +impl Value { + pub fn new(v: u32) -> Self { + Self(Cell::new(v)) + } + pub fn refund(self) -> u32 { + self.0.get() + } +} +impl Budget for Value { + fn consume_custom(&self, calls: u32) -> bool { + let (result, overflown) = self.0.get().overflowing_sub(calls); + if overflown { + return false; + } + self.0.set(result); + true + } +} --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -35,6 +35,7 @@ use scale_info::TypeInfo; mod bounded; +pub mod budget; pub mod mapping; mod migration; -- gitstuff