diff --git a/pallets/dnt-fee-controller/src/lib.rs b/pallets/dnt-fee-controller/src/lib.rs index 627bb1e..9351210 100644 --- a/pallets/dnt-fee-controller/src/lib.rs +++ b/pallets/dnt-fee-controller/src/lib.rs @@ -18,6 +18,8 @@ // information. #![cfg_attr(not(feature = "std"), no_std)] +// expect_used is not denied because FRAME macros expand to expect() internally. +#![cfg_attr(not(test), deny(clippy::unwrap_used))] pub use pallet::*; @@ -55,13 +57,25 @@ pub mod pallet { ArithmeticError, } + #[pallet::type_value] + pub fn DefaultFeeVaultPrecompileAddress() -> H160 { + H160::from_low_u64_be(0x807) + } + + #[pallet::type_value] + pub fn DefaultValidatorPercentage() -> U256 { + U256::from(50) + } + #[pallet::storage] #[pallet::getter(fn fee_vault_precompile_address)] - pub type FeeVaultPrecompileAddressStorage = StorageValue<_, H160, OptionQuery>; + pub type FeeVaultPrecompileAddressStorage = + StorageValue<_, H160, ValueQuery, DefaultFeeVaultPrecompileAddress>; #[pallet::storage] #[pallet::getter(fn validator_percentage)] - pub type ValidatorPercentageStorage = StorageValue<_, U256, OptionQuery>; + pub type ValidatorPercentageStorage = + StorageValue<_, U256, ValueQuery, DefaultValidatorPercentage>; #[pallet::config] pub trait Config: @@ -83,10 +97,7 @@ pub mod pallet { impl Default for GenesisConfig { fn default() -> Self { Self { - fee_vault_precompile_address: ::from_str( - "0x0000000000000000000000000000000000000807", - ) - .unwrap(), + fee_vault_precompile_address: H160::from_low_u64_be(0x807), validator_percentage: 50.into(), _config: Default::default(), } @@ -117,7 +128,7 @@ pub mod pallet { } fn get_fee_vault() -> H160 { - Self::fee_vault_precompile_address().unwrap() + Self::fee_vault_precompile_address() } fn withdraw_fee( @@ -133,7 +144,7 @@ pub mod pallet { return Err(Error::::InvalidConversionRate); } - let fee_vault = FeeVaultPrecompileAddressStorage::::get().unwrap(); + let fee_vault = FeeVaultPrecompileAddressStorage::::get(); let mapped_amount = amount .checked_mul(conversion_rate.0) .map(|v| v.div_mod(conversion_rate.1).0); @@ -178,7 +189,7 @@ pub mod pallet { _ => return Err(Error::ArithmeticError), }; - let fee_vault = FeeVaultPrecompileAddressStorage::::get().unwrap(); + let fee_vault = FeeVaultPrecompileAddressStorage::::get(); T::ERC20Manager::withdraw_amount(token, fee_vault, mapped_amount) .map_err(|_| Error::::ERC20WithdrawFailed)?; T::ERC20Manager::deposit_amount(token, from, mapped_amount) @@ -212,7 +223,7 @@ pub mod pallet { }; let validator_share = match to { - Some(_) => ValidatorPercentageStorage::::get().unwrap(), + Some(_) => ValidatorPercentageStorage::::get(), _ => 100.into(), }; @@ -233,13 +244,9 @@ pub mod pallet { ) .map_err(|_| Error::::FeeVaultOverflow)?; - if to.is_some() { - pallet_fee_rewards_vault::Pallet::::add_claimable_reward( - to.unwrap(), - token, - dapp_fee, - ) - .map_err(|_| Error::::FeeVaultOverflow)?; + if let Some(dapp) = to { + pallet_fee_rewards_vault::Pallet::::add_claimable_reward(dapp, token, dapp_fee) + .map_err(|_| Error::::FeeVaultOverflow)?; } Ok((validator_fee, dapp_fee)) @@ -256,7 +263,7 @@ pub mod pallet { } pub fn get_validator_percentage() -> U256 { - ValidatorPercentageStorage::::get().unwrap() + ValidatorPercentageStorage::::get() } } } diff --git a/pallets/dnt-fee-controller/src/tests.rs b/pallets/dnt-fee-controller/src/tests.rs index 6a9a3c1..ff46309 100644 --- a/pallets/dnt-fee-controller/src/tests.rs +++ b/pallets/dnt-fee-controller/src/tests.rs @@ -27,6 +27,7 @@ use crate::mock::{ use super::*; use mock::{new_test_ext, MeaninglessAddress, MeaninglessAddress2, Test}; use runner::OnChargeDecentralizedNativeTokenFee; +use sp_core::H160; #[test] fn withdraw_fee_calls_deposit_and_withdraw() { @@ -125,7 +126,7 @@ fn pay_fees_calls_vault_pallet() { new_test_ext().execute_with(|| { let meaningless_amount = 100.into(); let validator_amount = - DNTFeeController::validator_percentage().unwrap() * meaningless_amount / 100; + DNTFeeController::validator_percentage() * meaningless_amount / 100; let result = as OnChargeDecentralizedNativeTokenFee>::pay_fees( MeaninglessTokenAddress::get(), (1.into(), 1.into()), @@ -274,10 +275,50 @@ fn fails_withdraw_if_erc20_manager_fails() { #[test] fn validator_percentage_updates() { new_test_ext().execute_with(|| { - assert_eq!(DNTFeeController::validator_percentage().unwrap(), 50.into()); + assert_eq!(DNTFeeController::validator_percentage(), 50.into()); assert!(DNTFeeController::set_validator_percentage(10.into()).is_ok()); - assert_eq!(DNTFeeController::validator_percentage().unwrap(), 10.into()); + assert_eq!(DNTFeeController::validator_percentage(), 10.into()); + }); +} + +#[test] +fn missing_storage_falls_back_to_defaults_without_panicking() { + new_test_ext().execute_with(|| { + FeeVaultPrecompileAddressStorage::::kill(); + ValidatorPercentageStorage::::kill(); + + assert_eq!( + as OnChargeDecentralizedNativeTokenFee>::get_fee_vault(), + H160::from_low_u64_be(0x807) + ); + assert_eq!(DNTFeeController::get_validator_percentage(), 50.into()); + + let result = as OnChargeDecentralizedNativeTokenFee>::withdraw_fee( + MeaninglessAddress::get(), + MeaninglessTokenAddress::get(), + (1.into(), 1.into()), + 100.into(), + ); + assert!(result.is_ok()); + + let result = as OnChargeDecentralizedNativeTokenFee>::correct_fee( + MeaninglessAddress::get(), + MeaninglessTokenAddress::get(), + (1.into(), 1.into()), + 100.into(), + 10.into(), + ); + assert!(result.is_ok()); + + let result = as OnChargeDecentralizedNativeTokenFee>::pay_fees( + MeaninglessTokenAddress::get(), + (1.into(), 1.into()), + 100.into(), + MeaninglessAddress::get(), + Some(MeaninglessAddress2::get()), + ); + assert_eq!(result.unwrap(), (50.into(), 50.into())); }); } diff --git a/pallets/sponsored-transactions/src/lib.rs b/pallets/sponsored-transactions/src/lib.rs index 3d1c0f6..aedbcea 100644 --- a/pallets/sponsored-transactions/src/lib.rs +++ b/pallets/sponsored-transactions/src/lib.rs @@ -18,6 +18,8 @@ // information. #![cfg_attr(not(feature = "std"), no_std)] +// expect_used is not denied because FRAME macros expand to expect() internally. +#![cfg_attr(not(test), deny(clippy::unwrap_used))] use sp_core::H160; @@ -185,7 +187,15 @@ pub mod pallet { let dispatch = pallet_ethereum::Pallet::::transact(origin, transaction) .map_err(|_| DispatchError::Other("Signature doesn't meet with sponsor address"))?; - let gas_used = Self::gas_from_actual_weight(dispatch.actual_weight.unwrap()) + // Fall back to the declared gas limit (worst case) if the dispatch + // reports no actual weight. + let transaction_weight = dispatch.actual_weight.unwrap_or_else(|| { + ::GasWeightMapping::gas_to_weight( + gas_limit.unique_saturated_into(), + true, + ) + }); + let gas_used = Self::gas_from_actual_weight(transaction_weight) .map_err(|_| DispatchError::Other("Arithmetic error due to overflow."))?; let gas_left = match gas_limit.checked_sub(gas_used.into()) { @@ -395,8 +405,12 @@ pub mod pallet { } fn get_meta_trx_signer(signature: Vec, message: H256) -> Option { + let signature: [u8; 65] = match signature.as_slice().try_into() { + Ok(signature) => signature, + Err(_) => return None, + }; let result = match sp_io::crypto::secp256k1_ecdsa_recover( - signature.as_slice().try_into().unwrap(), + &signature, message.as_fixed_bytes(), ) { Ok(pubkey) => { diff --git a/pallets/sponsored-transactions/src/tests.rs b/pallets/sponsored-transactions/src/tests.rs index 409c708..50f8778 100644 --- a/pallets/sponsored-transactions/src/tests.rs +++ b/pallets/sponsored-transactions/src/tests.rs @@ -214,3 +214,28 @@ fn check_correct_fee_management(called_arguments: Vec<(bool, H160, H160, U256)>) } assert_eq!(total_deposited, total_withdrawn); } + +#[test] +fn fail_to_execute_meta_transaction_with_short_meta_signature() { + new_test_ext().execute_with(|| { + let trx1 = get_transaction_from_bytes(RawTransaction0::get()); + + let from = recover_signer(&trx1).unwrap(); + let origin: ::RuntimeOrigin = + pallet_ethereum::Origin::EthereumTransaction(from).into(); + + // A signature shorter than 65 bytes must be rejected, not panic. + let error = crate::Pallet::::send_sponsored_transaction( + origin.clone(), + trx1.clone(), + Sponsor::get(), + vec![0u8; 10], + ) + .unwrap_err(); + + assert!(matches!( + error, + DispatchError::Other("Invalid metatransaction signature") + )); + }); +} diff --git a/pallets/token-fee-controller/validator-fee-selector/src/lib.rs b/pallets/token-fee-controller/validator-fee-selector/src/lib.rs index 0dd5490..ad4cee5 100644 --- a/pallets/token-fee-controller/validator-fee-selector/src/lib.rs +++ b/pallets/token-fee-controller/validator-fee-selector/src/lib.rs @@ -18,6 +18,8 @@ // information. #![cfg_attr(not(feature = "std"), no_std)] +// expect_used is not denied because FRAME macros expand to expect() internally. +#![cfg_attr(not(test), deny(clippy::unwrap_used))] pub use pallet::*; use sp_core::{H160, U256}; @@ -72,7 +74,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn default_controller)] - pub type DefaultController = StorageValue<_, H160, OptionQuery>; + pub type DefaultController = StorageValue<_, H160, ValueQuery>; #[pallet::storage] #[pallet::getter(fn conversion_rate_fee_tokens)] @@ -154,7 +156,7 @@ pub mod pallet { fn conversion_rate_controller(validator: H160) -> H160 { ValidatorConversionRateController::::get(validator) - .unwrap_or(DefaultController::::get().unwrap()) + .unwrap_or_else(DefaultController::::get) } fn conversion_rate(sender: H160, validator: H160, token: H160) -> (U256, U256) { diff --git a/pallets/token-fee-controller/validator-fee-selector/src/tests.rs b/pallets/token-fee-controller/validator-fee-selector/src/tests.rs index 6dcef00..5a2c988 100644 --- a/pallets/token-fee-controller/validator-fee-selector/src/tests.rs +++ b/pallets/token-fee-controller/validator-fee-selector/src/tests.rs @@ -139,4 +139,28 @@ fn not_supported_by_validator_if_not_supported_by_chain() { token, ), false); }); -} \ No newline at end of file +} +#[test] +fn missing_default_controller_does_not_panic() { + ExtBuilder::default().build().execute_with(|| { + crate::DefaultController::::kill(); + + assert_eq!( + ::conversion_rate_controller( + MeaninglessAccount::get() + ), + H160::zero() + ); + + // With a zero-address controller the simulator call yields no usable + // output, so the conversion rate falls back to (1, 1). + assert_eq!( + ::conversion_rate( + MeaninglessAccount::get(), + MeaninglessAccount::get(), + MeaninglessTokenAddress::get(), + ), + (1.into(), 1.into()) + ); + }); +} diff --git a/pallets/zero-gas-transactions/src/lib.rs b/pallets/zero-gas-transactions/src/lib.rs index d58938a..baa78d5 100644 --- a/pallets/zero-gas-transactions/src/lib.rs +++ b/pallets/zero-gas-transactions/src/lib.rs @@ -18,6 +18,8 @@ // information. #![cfg_attr(not(feature = "std"), no_std)] +// expect_used is not denied because FRAME macros expand to expect() internally. +#![cfg_attr(not(test), deny(clippy::unwrap_used))] use log; use pallet_evm::TransactionValidationError; @@ -177,13 +179,24 @@ pub mod pallet { let origin: T::RuntimeOrigin = pallet_ethereum::Origin::EthereumTransaction(from).into(); + let transaction_data: TransactionData = (&transaction).into(); + let transaction_gas_limit = transaction_data.gas_limit; + let dispatch = pallet_ethereum::Pallet::::transact(origin, transaction).map_err(|e| { log::debug!(target: LOG_TARGET, "Dispatch transaction error: {:?}", e); DispatchError::Other("Signature doesn't meet with sponsor address") })?; - let used_gas = Self::gas_from_actual_weight(dispatch.actual_weight.unwrap()) + // Fall back to the declared gas limit (worst case) if the dispatch + // reports no actual weight. + let transaction_weight = dispatch.actual_weight.unwrap_or_else(|| { + T::GasWeightMapping::gas_to_weight( + transaction_gas_limit.unique_saturated_into(), + true, + ) + }); + let used_gas = Self::gas_from_actual_weight(transaction_weight) .map_err(|_| DispatchError::Other("Arithmetic error due to overflows"))?; Ok(frame_support::dispatch::PostDispatchInfo { @@ -319,8 +332,12 @@ pub mod pallet { } fn get_zero_gas_trx_signer(signature: Vec, message: H256) -> Option { + let signature: [u8; 65] = match signature.as_slice().try_into() { + Ok(signature) => signature, + Err(_) => return None, + }; let result = match sp_io::crypto::secp256k1_ecdsa_recover( - signature.as_slice().try_into().unwrap(), + &signature, message.as_fixed_bytes(), ) { Ok(pubkey) => { diff --git a/pallets/zero-gas-transactions/src/tests.rs b/pallets/zero-gas-transactions/src/tests.rs index 86b9cd9..8bd6bb7 100644 --- a/pallets/zero-gas-transactions/src/tests.rs +++ b/pallets/zero-gas-transactions/src/tests.rs @@ -56,3 +56,24 @@ fn fail_to_execute_transaction_with_high_nonce() { )); }) } + +#[test] +fn fail_to_execute_transaction_with_short_validator_signature() { + new_test_ext().execute_with(|| { + let private_key = H256::random(); + let trx1 = legacy_erc20_creation_transaction(0.into(), &private_key); + + // A validator signature shorter than 65 bytes must be rejected, not panic. + let error = crate::Pallet::::send_zero_gas_transaction( + RawOrigin::None.into(), + Transaction::Legacy(trx1.clone()), + vec![0u8; 10], + ) + .unwrap_err(); + + assert!(matches!( + error.error, + sp_runtime::DispatchError::Other("Invalid zero gas transaction signature") + )); + }) +} diff --git a/precompiles/fee-rewards-vault-controller/src/lib.rs b/precompiles/fee-rewards-vault-controller/src/lib.rs index f81ccff..23885d1 100644 --- a/precompiles/fee-rewards-vault-controller/src/lib.rs +++ b/precompiles/fee-rewards-vault-controller/src/lib.rs @@ -392,7 +392,8 @@ where } handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; - pallet_dnt_fee_controller::Pallet::::set_validator_percentage(percentage).unwrap(); + pallet_dnt_fee_controller::Pallet::::set_validator_percentage(percentage) + .map_err(|_| revert("percentage is too high"))?; handle.record_log_costs_manual(1, 32)?; log1( diff --git a/primitives/runner/src/lib.rs b/primitives/runner/src/lib.rs index 831cd06..a89928c 100644 --- a/primitives/runner/src/lib.rs +++ b/primitives/runner/src/lib.rs @@ -18,6 +18,7 @@ // information. #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] use core::marker::PhantomData; @@ -714,6 +715,11 @@ impl<'config> SubstrateStackSubstate<'config> { sp_io::storage::start_transaction(); } + // Invariant: the evm executor pairs every exit_* with a prior enter(), so + // `parent` is always Some here. This mirrors upstream Frontier + // (frame/evm/src/runner/stack.rs); converting the expect into an error + // would desync the sp_io::storage transaction depth. + #[allow(clippy::expect_used)] pub fn exit_commit(&mut self) -> Result<(), ExitError> { let mut exited = *self.parent.take().expect("Cannot commit on root substate"); mem::swap(&mut exited, self); @@ -726,6 +732,7 @@ impl<'config> SubstrateStackSubstate<'config> { Ok(()) } + #[allow(clippy::expect_used)] pub fn exit_revert(&mut self) -> Result<(), ExitError> { let mut exited = *self.parent.take().expect("Cannot discard on root substate"); mem::swap(&mut exited, self); @@ -735,6 +742,7 @@ impl<'config> SubstrateStackSubstate<'config> { Ok(()) } + #[allow(clippy::expect_used)] pub fn exit_discard(&mut self) -> Result<(), ExitError> { let mut exited = *self.parent.take().expect("Cannot discard on root substate"); mem::swap(&mut exited, self); diff --git a/primitives/tools/src/custom_fee.rs b/primitives/tools/src/custom_fee.rs index 8b4bfd9..b591b3d 100644 --- a/primitives/tools/src/custom_fee.rs +++ b/primitives/tools/src/custom_fee.rs @@ -89,13 +89,9 @@ impl CustomFeeInfo { // Cross-multiply to compare without division // user_rate >= validator_rate is equivalent to: // user_num * validator_denom >= validator_num * user_denom - match self.user_conversion_rate_cap.0.checked_mul(validator_denom) { - Some(user_side) => match validator_conversion_rate.0.checked_mul(user_denom) { - Some(validator_side) => user_side >= validator_side, - None => false, // Validator side would overflow, assume user rate is lower - }, - None => true, // User side would overflow, assume user rate is higher - } + // full_mul widens to U512, so the comparison is exact and cannot overflow. + self.user_conversion_rate_cap.0.full_mul(validator_denom) + >= validator_conversion_rate.0.full_mul(user_denom) } } @@ -195,4 +191,63 @@ mod test { (U256::from(0), U256::from(1_000_000_000)) ); } + + fn info_with_cap(numerator: U256, denominator: U256) -> CustomFeeInfo { + CustomFeeInfo { + actual_fee: numerator, + max_priority_fee_per_gas: None, + user_conversion_rate_cap: (numerator, denominator), + } + } + + #[test] + fn conversion_rate_limit_equal_rates_match() { + let info = info_with_cap(U256::from(3), U256::from(2)); + assert!(info.match_validator_conversion_rate_limit((U256::from(3), U256::from(2)))); + assert!(info.match_validator_conversion_rate_limit((U256::from(6), U256::from(4)))); + } + + #[test] + fn conversion_rate_limit_user_rate_higher_matches() { + let info = info_with_cap(U256::from(3), U256::from(1)); + assert!(info.match_validator_conversion_rate_limit((U256::from(2), U256::from(1)))); + } + + #[test] + fn conversion_rate_limit_user_rate_lower_rejects() { + let info = info_with_cap(U256::from(1), U256::from(2)); + assert!(!info.match_validator_conversion_rate_limit((U256::from(2), U256::from(1)))); + } + + #[test] + fn conversion_rate_limit_zero_denominators_do_not_panic() { + // Zero denominators are normalized to 1 on both sides. + let info = info_with_cap(U256::from(5), U256::zero()); + assert!(info.match_validator_conversion_rate_limit((U256::from(5), U256::zero()))); + assert!(info.match_validator_conversion_rate_limit((U256::from(4), U256::zero()))); + assert!(!info.match_validator_conversion_rate_limit((U256::from(6), U256::zero()))); + } + + #[test] + fn conversion_rate_limit_user_side_overflow_is_exact() { + // user = MAX/1, validator = MAX/1 with big denominators: cross products + // exceed 256 bits but full_mul keeps the comparison exact. + let info = info_with_cap(U256::max_value(), U256::from(2)); + assert!(info.match_validator_conversion_rate_limit((U256::max_value(), U256::from(3)))); + // user rate (MAX/3) < validator rate (MAX/2) must reject even though + // both cross products overflow 256 bits. + let info = info_with_cap(U256::max_value(), U256::from(3)); + assert!(!info.match_validator_conversion_rate_limit((U256::max_value(), U256::from(2)))); + } + + #[test] + fn conversion_rate_limit_validator_side_overflow_is_exact() { + // validator product overflows 256 bits, user product does not: + // user rate is clearly lower, must reject. + let info = info_with_cap(U256::from(1), U256::from(1)); + assert!(!info.match_validator_conversion_rate_limit((U256::max_value(), U256::from(2)))); + // and the symmetric case: huge user rate against tiny validator rate. + let info = info_with_cap(U256::max_value(), U256::from(1)); + assert!(info.match_validator_conversion_rate_limit((U256::from(1), U256::from(2)))); + } } diff --git a/primitives/tools/src/eth.rs b/primitives/tools/src/eth.rs index 659be87..a10d4bb 100644 --- a/primitives/tools/src/eth.rs +++ b/primitives/tools/src/eth.rs @@ -164,7 +164,7 @@ pub fn transaction_gas_price( pub fn build_eip191_message_hash(message: Vec) -> H256 { let result = b"\x19Ethereum Signed Message:\n" .iter() - .chain(crate::misc::u64_to_buffer_in_ascii(message.len().try_into().unwrap()).iter()) + .chain(crate::misc::u64_to_buffer_in_ascii(message.len() as u64).iter()) .chain(message.iter()) .cloned() .collect::>(); diff --git a/primitives/tools/src/lib.rs b/primitives/tools/src/lib.rs index b3ce68c..d371b7c 100644 --- a/primitives/tools/src/lib.rs +++ b/primitives/tools/src/lib.rs @@ -18,6 +18,7 @@ // information. #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] pub mod custom_fee; pub mod eth; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index df6f744..1ebab46 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1658,13 +1658,10 @@ impl_runtime_apis! { fn is_compatible_fee(tx: ::Extrinsic, validator: AccountId) -> bool { match tx.0.function { RuntimeCall::Ethereum(transact { transaction }) | RuntimeCall::MetaTransactions(send_sponsored_transaction { transaction, .. }) => { - let source_address_option = stbl_tools::eth::recover_signer(&transaction); - - if source_address_option.is_none() { - return true; - } - - let source_address = source_address_option.unwrap(); + let source_address = match stbl_tools::eth::recover_signer(&transaction) { + Some(address) => address, + None => return true, + }; let source_fee_token = >::get_user_fee_token(source_address); let validator_conversion_rate = >::conversion_rate(source_address, validator.into(), source_fee_token); let fee = pallet_base_fee::BaseFeePerGas::::get();