From 11bca5fe9521c1c689f7078220b1d6ddd5ac876a Mon Sep 17 00:00:00 2001 From: dmoka Date: Wed, 17 Dec 2025 20:44:53 +0100 Subject: [PATCH 01/19] add init implementation for DCA extra gas --- Cargo.lock | 2 + integration-tests/src/dca.rs | 373 +++++++++++++++++- pallets/dca/Cargo.toml | 4 + pallets/dca/src/lib.rs | 69 +++- pallets/dca/src/tests/mock.rs | 25 ++ pallets/dispatcher/src/lib.rs | 18 +- runtime/hydradx/src/assets.rs | 3 + .../ConditionalGasEater.json | 361 +++++++++++++++++ .../contracts/ConditionalGasEater.sol | 81 ++++ traits/src/evm.rs | 9 +- 10 files changed, 938 insertions(+), 7 deletions(-) create mode 100644 scripts/test-contracts/artifacts/contracts/ConditionalGasEater.sol/ConditionalGasEater.json create mode 100644 scripts/test-contracts/contracts/ConditionalGasEater.sol diff --git a/Cargo.lock b/Cargo.lock index 2c4fd1fe8..8ee9ee618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9201,7 +9201,9 @@ dependencies = [ "pallet-balances", "pallet-broadcast", "pallet-currencies", + "pallet-dispatcher", "pallet-ema-oracle", + "pallet-evm", "pallet-omnipool", "pallet-route-executor", "pallet-xyk", diff --git a/integration-tests/src/dca.rs b/integration-tests/src/dca.rs index 2ad8b5427..4abb99058 100644 --- a/integration-tests/src/dca.rs +++ b/integration-tests/src/dca.rs @@ -3,10 +3,13 @@ use crate::count_dca_event; use crate::polkadot_test_net::*; use crate::{assert_balance, assert_reserved_balance}; +use ethabi::ethereum_types::H256; use frame_support::assert_noop; use frame_support::assert_ok; use frame_support::storage::with_transaction; use frame_system::RawOrigin; +use hydradx_runtime::evm::aave_trade_executor::Function; +use hydradx_runtime::evm::Executor; use hydradx_runtime::DOT_ASSET_LOCATION; use hydradx_runtime::XYK; use hydradx_runtime::{AssetPairAccountIdFor, NamedReserveId}; @@ -14,6 +17,7 @@ use hydradx_runtime::{ AssetRegistry, Balances, Currencies, InsufficientEDinHDX, Omnipool, Router, Runtime, RuntimeEvent, RuntimeOrigin, Stableswap, Tokens, Treasury, DCA, }; +use hydradx_traits::evm::CallContext; use hydradx_traits::registry::{AssetKind, Create}; use hydradx_traits::router::AssetPair; use hydradx_traits::router::PoolType; @@ -27,13 +31,14 @@ use pallet_dca::types::{Order, Schedule}; use pallet_omnipool::types::Tradability; use pallet_route_executor::MAX_NUMBER_OF_TRADES; use pallet_stableswap::MAX_ASSETS_IN_POOL; -use primitives::{AssetId, Balance}; +use primitives::{AssetId, Balance, EvmAddress}; use sp_runtime::traits::ConstU32; use sp_runtime::DispatchError; use sp_runtime::Permill; use sp_runtime::{BoundedVec, FixedU128}; use sp_runtime::{DispatchResult, TransactionOutcome}; use xcm_emulator::TestExt; + const TREASURY_ACCOUNT_INIT_BALANCE: Balance = 1000 * UNITS; mod omnipool { @@ -4494,6 +4499,10 @@ pub fn count_failed_trade_events() -> u32 { count_dca_event!(pallet_dca::Event::TradeFailed { .. }) } +pub fn count_trade_executed_events() -> u32 { + count_dca_event!(pallet_dca::Event::TradeExecuted { .. }) +} + pub fn count_terminated_trade_events() -> u32 { count_dca_event!(pallet_dca::Event::Terminated { .. }) } @@ -4683,3 +4692,365 @@ fn add_dot_as_payment_currency_with_details(amount: Balance, price: FixedU128) { //crate::dca::do_trade_to_populate_oracle(DOT, HDX, UNITS); } + +mod extra_gas_erc20 { + use super::*; + + use hydradx_runtime::{FixedU128, MultiTransactionPayment, Omnipool, Router}; + + use crate::polkadot_test_net::*; + use ethabi::ethereum_types::H160; + use frame_support::{assert_ok, traits::Hooks}; + use hydradx_runtime::evm::aave_trade_executor::AaveTradeExecutor; + use hydradx_runtime::evm::precompiles::erc20_mapping::HydraErc20Mapping; + use hydradx_runtime::evm::Erc20Currency; + use hydradx_runtime::EmaOracle; + use hydradx_runtime::Liquidation; + use hydradx_runtime::{ + Block, Currencies, Dispatcher, EVMAccounts, OriginCaller, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, + DCA, + }; + use hydradx_traits::evm::Erc20Mapping; + use hydradx_traits::evm::ERC20; + use hydradx_traits::evm::{CallContext, Erc20Encoding, EvmAddress, ExtraGasSupport}; + use hydradx_traits::router::PoolType::Aave; + use hydradx_traits::router::{PoolType, Trade}; + use liquidation_worker_support::MoneyMarketData; + use pallet_liquidation::BorrowingContract; + use primitives::constants::chain::OMNIPOOL_SOURCE; + use primitives::Balance; + use sp_core::U256; + use sp_runtime::Permill; + + #[test] + fn dca_succeeds_after_extra_gas_increased_due_to_out_of_gas_error() { + TestNet::reset(); + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + + let evm_address = EVMAccounts::evm_address(&Router::router_account()); + let contract = deploy_conditional_gas_eater(evm_address, 400_000, crate::erc20::deployer()); + let erc20 = crate::erc20::bind_erc20(contract); + assert_ok!(EmaOracle::add_oracle( + RuntimeOrigin::root(), + OMNIPOOL_SOURCE, + (LRNA, erc20) + )); + + //Add new erc20 to omnipool + let bal = Currencies::free_balance(erc20, &ALICE.into()); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(ALICE.into()), + pallet_omnipool::Pallet::::protocol_account(), + erc20, + bal / 10, + )); + assert_ok!(pallet_omnipool::Pallet::::add_token( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200), + Permill::from_percent(30), + ALICE.into(), + )); + + assert_ok!(MultiTransactionPayment::add_currency( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200) + )); + + hydradx_run_to_block(11); + + let schedule_id = create_schedule_with_onchain_route(erc20, 0, 200000 * UNITS, 0, Some(3)); + + let alice_init_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + hydradx_run_to_block(13); + let period = 5; + let retry_delay = 20; + let block_number = hydradx_runtime::System::block_number(); + + // Assert that extra gas was increased in one retry + assert_eq!(DCA::retries_on_error(schedule_id), 1); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + trade_failed_with_evm_out_of_gas_error(schedule_id); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(0, count_trade_executed_events()); + let alice_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + assert_eq!(alice_init_hdx_balance, alice_hdx_balance); + + hydradx_run_to_block(33); + assert_eq!(Dispatcher::extra_gas(), 0); + + //Assert that trade finally succeeded + assert_eq!(DCA::retries_on_error(schedule_id), 0); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + assert_trade_executed_succesfully(schedule_id); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(1, count_trade_executed_events()); + let alice_hdx_balance_after_retry = Currencies::free_balance(HDX, &ALICE.into()); + assert!(alice_hdx_balance_after_retry > alice_hdx_balance); + + hydradx_run_to_block(38); + assert_eq!(Dispatcher::extra_gas(), 0); + + //Assert that trade succeeded in the next run too + assert_eq!(DCA::retries_on_error(schedule_id), 0); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(2, count_trade_executed_events()); + let alice_final_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + assert!(alice_final_hdx_balance > alice_hdx_balance_after_retry); + }); + } + + #[test] + fn extra_gas_keep_incrementing_multiple_times_till_successfull() { + TestNet::reset(); + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + + let evm_address = EVMAccounts::evm_address(&Router::router_account()); + let contract = deploy_conditional_gas_eater(evm_address, 800_000, crate::erc20::deployer()); + let erc20 = crate::erc20::bind_erc20(contract); + assert_ok!(EmaOracle::add_oracle( + RuntimeOrigin::root(), + OMNIPOOL_SOURCE, + (LRNA, erc20) + )); + + //Add new erc20 to omnipool + let bal = Currencies::free_balance(erc20, &ALICE.into()); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(ALICE.into()), + pallet_omnipool::Pallet::::protocol_account(), + erc20, + bal / 10, + )); + assert_ok!(pallet_omnipool::Pallet::::add_token( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200), + Permill::from_percent(30), + ALICE.into(), + )); + + assert_ok!(MultiTransactionPayment::add_currency( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200) + )); + + hydradx_run_to_block(11); + + let schedule_id = create_schedule_with_onchain_route(erc20, 0, 200000 * UNITS, 0, Some(3)); + + let alice_init_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + hydradx_run_to_block(13); + let period = 5; + let retry_delay = 20; + let block_number = hydradx_runtime::System::block_number(); + + // Assert that extra gas was increased in one retry + assert_eq!(DCA::retries_on_error(schedule_id), 1); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + trade_failed_with_evm_out_of_gas_error(schedule_id); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(0, count_trade_executed_events()); + let alice_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + assert_eq!(alice_init_hdx_balance, alice_hdx_balance); + + hydradx_run_to_block(33); + + //It fails again as the gas increased was still not enough + assert_eq!(DCA::retries_on_error(schedule_id), 2); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 666_666); + assert_eq!(2, count_failed_trade_events()); + assert_eq!(0, count_trade_executed_events()); + let alice_hdx_balance_after_retry = Currencies::free_balance(HDX, &ALICE.into()); + assert_eq!(alice_hdx_balance_after_retry, alice_init_hdx_balance); + + hydradx_run_to_block(73); + + //Assert that trade succeeded in the next run + assert_eq!(DCA::retries_on_error(schedule_id), 0); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 666_666); + assert_trade_executed_succesfully(schedule_id); + assert_eq!(2, count_failed_trade_events()); + assert_eq!(1, count_trade_executed_events()); + let alice_final_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + assert!(alice_final_hdx_balance > alice_hdx_balance_after_retry); + }); + } + + #[test] + fn dca_with_extra_gas_should_completed_and_clear_up() { + TestNet::reset(); + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + + let evm_address = EVMAccounts::evm_address(&Router::router_account()); + let contract = deploy_conditional_gas_eater(evm_address, 400_000, crate::erc20::deployer()); + let erc20 = crate::erc20::bind_erc20(contract); + assert_ok!(EmaOracle::add_oracle( + RuntimeOrigin::root(), + OMNIPOOL_SOURCE, + (LRNA, erc20) + )); + + //Add new erc20 to omnipool + let bal = Currencies::free_balance(erc20, &ALICE.into()); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(ALICE.into()), + pallet_omnipool::Pallet::::protocol_account(), + erc20, + bal / 10, + )); + assert_ok!(pallet_omnipool::Pallet::::add_token( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200), + Permill::from_percent(30), + ALICE.into(), + )); + + assert_ok!(MultiTransactionPayment::add_currency( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200) + )); + + hydradx_run_to_block(11); + let amount_in = 200000 * UNITS; + let schedule_id = create_schedule_with_onchain_route(erc20, 0, 200000 * UNITS, 500000 * UNITS, Some(3)); + + let alice_init_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + hydradx_run_to_block(13); + let period = 5; + let retry_delay = 20; + let block_number = hydradx_runtime::System::block_number(); + + // Assert that extra gas was increased in one retry + assert_eq!(DCA::retries_on_error(schedule_id), 1); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + trade_failed_with_evm_out_of_gas_error(schedule_id); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(0, count_trade_executed_events()); + let alice_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + assert_eq!(alice_init_hdx_balance, alice_hdx_balance); + + hydradx_run_to_block(33); + + //Assert that trade finally succeeded + assert_eq!(DCA::retries_on_error(schedule_id), 0); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + assert_trade_executed_succesfully(schedule_id); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(1, count_trade_executed_events()); + let alice_hdx_balance_after_retry = Currencies::free_balance(HDX, &ALICE.into()); + assert!(alice_hdx_balance_after_retry > alice_hdx_balance); + + hydradx_run_to_block(38); + + //Assert that trade succeeded in the next run too + assert_eq!(DCA::retries_on_error(schedule_id), 0); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(2, count_trade_executed_events()); + let alice_hdx_balance_after_2nd_run = Currencies::free_balance(HDX, &ALICE.into()); + assert!(alice_hdx_balance_after_2nd_run > alice_hdx_balance_after_retry); + + hydradx_run_to_block(43); + + //Assert that trade succeeded in the next run too + assert_eq!(DCA::retries_on_error(schedule_id), 0); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 0); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(3, count_trade_executed_events()); + let alice_final_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + assert!(alice_final_hdx_balance > alice_hdx_balance_after_2nd_run); + assert_eq!(1, count_completed_event()); + }); + } + + fn create_schedule_with_onchain_route( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + total_amount: Balance, + max_retries: Option, + ) -> u32 { + use hydradx_runtime::Router; + use hydradx_traits::router::{AssetPair, RouteProvider}; + + let route = Router::get_route(AssetPair { asset_in, asset_out }); + + let schedule = Schedule { + owner: ALICE.into(), + period: 5u32, + total_amount, + max_retries, + stability_threshold: None, + slippage: Some(Permill::from_percent(90)), + order: Order::Sell { + asset_in, + asset_out, + amount_in, + min_amount_out: Balance::MIN, + route, // Use the BoundedVec directly from Router + }, + }; + + assert_ok!(DCA::schedule(RuntimeOrigin::signed(ALICE.into()), schedule, None)); + 0 // schedule_id + } + + /// Deploy ConditionalGasEater contract with constructor parameters + /// + /// # Arguments + /// * `router_address` - EVM address of the router pallet account + /// * `gas_to_waste` - Amount of gas to waste on transfers to the router + /// * `deployer` - EVM address of the contract deployer + /// + /// # Returns + /// The deployed contract's EVM address + fn deploy_conditional_gas_eater(router_address: EvmAddress, gas_to_waste: u64, deployer: EvmAddress) -> EvmAddress { + use ethabi::{encode, Token}; + + // Get base bytecode from compiled artifact + let mut bytecode = crate::utils::contracts::get_contract_bytecode("ConditionalGasEater"); + + // Encode constructor parameters: (address _routerAddress, uint256 _gasToWaste) + let constructor_params = encode(&[Token::Address(router_address.into()), Token::Uint(gas_to_waste.into())]); + + // Append encoded constructor params to bytecode + bytecode.extend(constructor_params); + + // Deploy contract with complete bytecode + crate::utils::contracts::deploy_contract_code(bytecode, deployer) + } + + fn trade_failed_with_evm_out_of_gas_error(schedule_id: u32) { + let events = last_hydra_events(20); + let has_out_of_gas_event = events.iter().any(|e| { + matches!(e, + RuntimeEvent::DCA(pallet_dca::Event::TradeFailed { id, error, .. }) + if *id == schedule_id && *error == pallet_dispatcher::Error::::EvmOutOfGas.into() + ) + }); + assert!( + has_out_of_gas_event, + "Expected TradeFailed event with EvmOutOfGas error" + ); + } + + fn assert_trade_executed_succesfully(schedule_id: u32) { + let events = last_hydra_events(20); + let has_trade_executed = events.iter().any(|e| { + matches!(e, + RuntimeEvent::DCA(pallet_dca::Event::TradeExecuted { id, .. }) + if *id == schedule_id + ) + }); + assert!(has_trade_executed, "Expected TradeExecuted event after retry"); + } +} diff --git a/pallets/dca/Cargo.toml b/pallets/dca/Cargo.toml index bc4801d66..1b0188137 100644 --- a/pallets/dca/Cargo.toml +++ b/pallets/dca/Cargo.toml @@ -31,6 +31,8 @@ cumulus-pallet-parachain-system = { workspace = true } # HydraDX dependencies pallet-xyk = { workspace = true } pallet-omnipool = { workspace = true } +pallet-dispatcher = { workspace = true } +pallet-evm = { workspace = true } hydradx-traits = { workspace = true } hydradx-adapters = { workspace = true } pallet-ema-oracle = { workspace = true } @@ -79,6 +81,8 @@ std = [ "hydradx-adapters/std", "pallet-omnipool/std", "pallet-ema-oracle/std", + "pallet-dispatcher/std", + "pallet-evm/std", ] runtime-benchmarks = [ diff --git a/pallets/dca/src/lib.rs b/pallets/dca/src/lib.rs index 6abfba99d..f19b42e51 100644 --- a/pallets/dca/src/lib.rs +++ b/pallets/dca/src/lib.rs @@ -78,13 +78,13 @@ use frame_system::{ pallet_prelude::{BlockNumberFor, OriginFor}, Origin, }; -use sp_runtime::traits::Zero; - +use hydradx_traits::evm::ExtraGasSupport; use orml_traits::{arithmetic::CheckedAdd, MultiCurrency, NamedMultiReservableCurrency}; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; use sp_runtime::helpers_128bit::multiply_by_rational_with_rounding; use sp_runtime::traits::CheckedMul; +use sp_runtime::traits::Zero; use sp_runtime::{ traits::{BlockNumberProvider, Saturating}, ArithmeticError, BoundedVec, DispatchError, FixedPointNumber, FixedU128, Percent, Permill, Rounding, @@ -97,6 +97,7 @@ use hydradx_traits::fee::{InspectTransactionFeeCurrency, SwappablePaymentAssetTr use hydradx_traits::router::{inverse_route, AmmTradeWeights, AmountInAndOut, RouteProvider, RouterT, Trade}; use hydradx_traits::{NativePriceOracle, OraclePeriod, PriceOracle}; use pallet_broadcast::types::ExecutionType; +use pallet_evm::GasWeightMapping; pub use weights::WeightInfo; #[cfg(test)] @@ -112,6 +113,7 @@ pub use pallet::*; pub const MAX_NUMBER_OF_RETRY_FOR_RESCHEDULING: u32 = 10; pub const FEE_MULTIPLIER_FOR_MIN_TRADE_LIMIT: Balance = 20; +pub const MAX_EXTRA_GAS: u64 = 1_000_000; #[frame_support::pallet] pub mod pallet { @@ -155,7 +157,7 @@ pub mod pallet { continue; }; - let weight_for_single_execution = Self::get_trade_weight(&schedule.order); + let weight_for_single_execution = Self::get_trade_weight_with_extra_gas(schedule_id, &schedule.order); weight.saturating_accrue(weight_for_single_execution); if let Err(e) = Self::prepare_schedule( @@ -192,6 +194,11 @@ pub mod pallet { error, }); + // If it fails with out of gas then we need to increase gas for next execution + if error == T::ExtraGasSupport::out_of_gas_error() { + Self::increment_extra_gas(schedule_id, &schedule); + } + if error != Error::::TradeLimitReached.into() && error != Error::::SlippageLimitReached.into() && !T::RetryOnError::contains(&error) @@ -259,6 +266,10 @@ pub mod pallet { ///Errors we want to explicitly retry on, in case of failing DCA type RetryOnError: Contains; + type ExtraGasSupport: ExtraGasSupport; + + type GasWeightMapping: GasWeightMapping; + ///Max price difference allowed between blocks #[pallet::constant] type MaxPriceDifferenceBetweenBlocks: Get; @@ -449,6 +460,13 @@ pub mod pallet { pub type ScheduleIdsPerBlock = StorageMap<_, Blake2_128Concat, BlockNumberFor, BoundedVec, ValueQuery>; + /// Stores the current extra gas value for each schedule. + /// Initialized to 0, increments on EvmOutOfGas, persists after successful execution. + /// Cleaned up when schedule terminates or completes. + #[pallet::storage] + #[pallet::getter(fn schedule_extra_gas)] + pub type ScheduleExtraGas = StorageMap<_, Blake2_128Concat, ScheduleId, u64, ValueQuery>; + #[pallet::call] impl Pallet { /// Creates a new DCA (Dollar-Cost Averaging) schedule and plans the next execution @@ -553,6 +571,7 @@ pub mod pallet { ScheduleOwnership::::insert(who.clone(), next_schedule_id, ()); RemainingAmounts::::insert(next_schedule_id, reserve_amount); RetriesOnError::::insert(next_schedule_id, 0); + ScheduleExtraGas::::insert(next_schedule_id, 0); //TODO: we might dont need to set it as 0 is default?! T::Currencies::reserve_named( &T::NamedReserveId::get(), @@ -762,6 +781,10 @@ impl Pallet { schedule_id: ScheduleId, schedule: &Schedule>, ) -> Result, DispatchError> { + // Set extra gas for dispatcher to use during EVM calls + let extra_gas = ScheduleExtraGas::::get(schedule_id); + T::ExtraGasSupport::set_extra_gas(extra_gas); + pallet_broadcast::Pallet::::add_to_context(|id| ExecutionType::DCA(schedule_id, id))?; let origin: OriginFor = Origin::::Signed(schedule.owner.clone()).into(); @@ -845,6 +868,9 @@ impl Pallet { pallet_broadcast::Pallet::::remove_from_context()?; + // Clean up extra gas + T::ExtraGasSupport::clear_extra_gas(); + trade_result } @@ -1210,6 +1236,20 @@ impl Pallet { } } + /// Calculates trade weight including extra gas for a schedule. + fn get_trade_weight_with_extra_gas(schedule_id: ScheduleId, order: &Order) -> Weight { + let base_weight = Self::get_trade_weight(order); + let extra_gas = ScheduleExtraGas::::get(schedule_id); + + if extra_gas == 0 { + return base_weight; + } + + // Convert extra gas to weight without base weight because we already account for that + let extra_weight = T::GasWeightMapping::gas_to_weight(extra_gas, false); + base_weight.saturating_add(extra_weight) + } + fn convert_native_amount_to_currency( asset_id: T::AssetId, native_asset_amount: Balance, @@ -1266,12 +1306,35 @@ impl Pallet { Ok(price_from_rational) } + /// Increments extra gas for a schedule for the case when EvmOutOfGas error occurs. + /// Uses linear increment: MAX_EXTRA_GAS / max_retries, capped at MAX_EXTRA_GAS. + fn increment_extra_gas(schedule_id: ScheduleId, schedule: &Schedule>) { + ScheduleExtraGas::::mutate(schedule_id, |extra_gas| { + let max_retries = schedule + .max_retries + .map(|r| r as u64) + .unwrap_or_else(|| T::MaxNumberOfRetriesOnError::get() as u64); + + let Some(gas_increment) = MAX_EXTRA_GAS.checked_div(max_retries) else { + log::error!( + "Gas increment calculation overflowed for schedule_id: {:?}", + schedule_id + ); + return; + }; + + let new_gas = extra_gas.saturating_add(gas_increment); + *extra_gas = new_gas.min(MAX_EXTRA_GAS); + }); + } + fn remove_schedule_from_storages(owner: &T::AccountId, schedule_id: ScheduleId) { Schedules::::remove(schedule_id); ScheduleOwnership::::remove(owner, schedule_id); RemainingAmounts::::remove(schedule_id); RetriesOnError::::remove(schedule_id); ScheduleExecutionBlock::::remove(schedule_id); + ScheduleExtraGas::::remove(schedule_id); } } diff --git a/pallets/dca/src/tests/mock.rs b/pallets/dca/src/tests/mock.rs index 712914109..f6d67eda6 100644 --- a/pallets/dca/src/tests/mock.rs +++ b/pallets/dca/src/tests/mock.rs @@ -712,6 +712,30 @@ impl Config for Test { type RetryOnError = (); type PolkadotNativeAssetId = PolkadotNativeCurrencyId; type SwappablePaymentAssetSupport = MockedInsufficientAssetSupport; + type ExtraGasSupport = ExtraGasSetterMock; + type GasWeightMapping = MockGasWeightMapping; +} + +pub struct ExtraGasSetterMock; + +impl ExtraGasSupport for ExtraGasSetterMock { + fn set_extra_gas(_gas: u64) {} + + fn clear_extra_gas() {} + + fn out_of_gas_error() -> DispatchError { + DispatchError::Other("Out of gas") + } +} + +pub struct MockGasWeightMapping; +impl pallet_evm::GasWeightMapping for MockGasWeightMapping { + fn gas_to_weight(_gas: u64, _without_base_weight: bool) -> Weight { + Weight::zero() + } + fn weight_to_gas(_weight: Weight) -> u64 { + 0 + } } pub struct MockedInsufficientAssetSupport; @@ -785,6 +809,7 @@ use frame_system::pallet_prelude::OriginFor; use hydra_dx_math::ema::EmaPrice; use hydra_dx_math::to_u128_wrapper; use hydra_dx_math::types::Ratio; +use hydradx_traits::evm::ExtraGasSupport; use hydradx_traits::fee::{GetDynamicFee, InspectTransactionFeeCurrency, SwappablePaymentAssetTrader}; use hydradx_traits::router::{ExecutorError, PoolType, RouteProvider, Trade, TradeExecution}; use pallet_currencies::fungibles::FungibleCurrencies; diff --git a/pallets/dispatcher/src/lib.rs b/pallets/dispatcher/src/lib.rs index 574c19d39..468087d1a 100644 --- a/pallets/dispatcher/src/lib.rs +++ b/pallets/dispatcher/src/lib.rs @@ -39,10 +39,10 @@ mod benchmarking; pub mod weights; use frame_support::dispatch::PostDispatchInfo; -use hydradx_traits::evm::EvmAddress; use hydradx_traits::evm::MaybeEvmCall; +use hydradx_traits::evm::{EvmAddress, ExtraGasSupport}; use pallet_evm::{ExitReason, GasWeightMapping}; -use sp_runtime::{traits::Dispatchable, DispatchResultWithInfo}; +use sp_runtime::{traits::Dispatchable, DispatchError, DispatchResultWithInfo}; pub use weights::WeightInfo; // Re-export pallet items so that they can be accessed from the crate namespace. @@ -369,3 +369,17 @@ impl Pallet { LastEvmCallExitReason::::put(reason); } } + +impl ExtraGasSupport for Pallet { + fn set_extra_gas(gas: u64) { + ExtraGas::::set(gas); + } + + fn clear_extra_gas() { + ExtraGas::::kill(); + } + + fn out_of_gas_error() -> DispatchError { + Error::::EvmOutOfGas.into() + } +} diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index 20046e983..41c22abe8 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -923,6 +923,7 @@ impl Contains for RetryOnErrorForDca { let errors: Vec = vec![ pallet_omnipool::Error::::AssetNotFound.into(), pallet_omnipool::Error::::NotAllowed.into(), + pallet_dispatcher::Error::::EvmOutOfGas.into(), ]; errors.contains(t) } @@ -972,6 +973,8 @@ impl pallet_dca::Config for Runtime { type RetryOnError = RetryOnErrorForDca; type PolkadotNativeAssetId = DotAssetId; type SwappablePaymentAssetSupport = XykPaymentAssetSupport; + type ExtraGasSupport = Dispatcher; + type GasWeightMapping = evm::FixedHydraGasWeightMapping; } // Provides weight info for the router. Router extrinsics can be executed with different AMMs, so we split the router weights into two parts: diff --git a/scripts/test-contracts/artifacts/contracts/ConditionalGasEater.sol/ConditionalGasEater.json b/scripts/test-contracts/artifacts/contracts/ConditionalGasEater.sol/ConditionalGasEater.json new file mode 100644 index 000000000..8c6f6200d --- /dev/null +++ b/scripts/test-contracts/artifacts/contracts/ConditionalGasEater.sol/ConditionalGasEater.json @@ -0,0 +1,361 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ConditionalGasEater", + "sourceName": "contracts/ConditionalGasEater.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_routerAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_gasToWaste", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGasToWaste", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRouterAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60c06040523480156200001157600080fd5b5060405162001c8538038062001c858339818101604052810190620000379190620004b8565b6040518060400160405280601381526020017f436f6e646974696f6e616c4761734561746572000000000000000000000000008152506040518060400160405280600781526020017f43474541544552000000000000000000000000000000000000000000000000008152508160039081620000b491906200076f565b508060049081620000c691906200076f565b5050508173ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff16815250508060a0818152505062000145336200011a6200014d60201b60201c565b600a620001289190620009e6565b633b9aca0062000139919062000a37565b6200015660201b60201c565b505062000b56565b60006012905090565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620001cb5760006040517fec442f05000000000000000000000000000000000000000000000000000000008152600401620001c2919062000a93565b60405180910390fd5b620001df60008383620001e360201b60201c565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603620002395780600260008282546200022c919062000ab0565b925050819055506200030f565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015620002c8578381836040517fe450d38c000000000000000000000000000000000000000000000000000000008152600401620002bf9392919062000afc565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036200035a5780600260008282540392505081905550620003a7565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405162000406919062000b39565b60405180910390a3505050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000620004458262000418565b9050919050565b620004578162000438565b81146200046357600080fd5b50565b60008151905062000477816200044c565b92915050565b6000819050919050565b62000492816200047d565b81146200049e57600080fd5b50565b600081519050620004b28162000487565b92915050565b60008060408385031215620004d257620004d162000413565b5b6000620004e28582860162000466565b9250506020620004f585828601620004a1565b9150509250929050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200058157607f821691505b60208210810362000597576200059662000539565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620006017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82620005c2565b6200060d8683620005c2565b95508019841693508086168417925050509392505050565b6000819050919050565b6000620006506200064a62000644846200047d565b62000625565b6200047d565b9050919050565b6000819050919050565b6200066c836200062f565b620006846200067b8262000657565b848454620005cf565b825550505050565b600090565b6200069b6200068c565b620006a881848462000661565b505050565b5b81811015620006d057620006c460008262000691565b600181019050620006ae565b5050565b601f8211156200071f57620006e9816200059d565b620006f484620005b2565b8101602085101562000704578190505b6200071c6200071385620005b2565b830182620006ad565b50505b505050565b600082821c905092915050565b6000620007446000198460080262000724565b1980831691505092915050565b60006200075f838362000731565b9150826002028217905092915050565b6200077a82620004ff565b67ffffffffffffffff8111156200079657620007956200050a565b5b620007a2825462000568565b620007af828285620006d4565b600060209050601f831160018114620007e75760008415620007d2578287015190505b620007de858262000751565b8655506200084e565b601f198416620007f7866200059d565b60005b828110156200082157848901518255600182019150602085019450602081019050620007fa565b868310156200084157848901516200083d601f89168262000731565b8355505b6001600288020188555050505b505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60008160011c9050919050565b6000808291508390505b6001851115620008e457808604811115620008bc57620008bb62000856565b5b6001851615620008cc5780820291505b8081029050620008dc8562000885565b94506200089c565b94509492505050565b600082620008ff5760019050620009d2565b816200090f5760009050620009d2565b8160018114620009285760028114620009335762000969565b6001915050620009d2565b60ff84111562000948576200094762000856565b5b8360020a91508482111562000962576200096162000856565b5b50620009d2565b5060208310610133831016604e8410600b8410161715620009a35782820a9050838111156200099d576200099c62000856565b5b620009d2565b620009b2848484600162000892565b92509050818404811115620009cc57620009cb62000856565b5b81810290505b9392505050565b600060ff82169050919050565b6000620009f3826200047d565b915062000a0083620009d9565b925062000a2f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8484620008ed565b905092915050565b600062000a44826200047d565b915062000a51836200047d565b925082820262000a61816200047d565b9150828204841483151762000a7b5762000a7a62000856565b5b5092915050565b62000a8d8162000438565b82525050565b600060208201905062000aaa600083018462000a82565b92915050565b600062000abd826200047d565b915062000aca836200047d565b925082820190508082111562000ae55762000ae462000856565b5b92915050565b62000af6816200047d565b82525050565b600060608201905062000b13600083018662000a82565b62000b22602083018562000aeb565b62000b31604083018462000aeb565b949350505050565b600060208201905062000b50600083018462000aeb565b92915050565b60805160a0516110ed62000b9860003960008181610316015281816104b601526104df01526000818161033701528181610500015261059a01526110ed6000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c806370a082311161007157806370a082311461016857806395d89b41146101985780639861d75f146101b6578063a9059cbb146101d4578063d54f7d5e14610204578063dd62ed3e14610222576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610252565b6040516100c39190610cc5565b60405180910390f35b6100e660048036038101906100e19190610d80565b6102e4565b6040516100f39190610ddb565b60405180910390f35b610104610307565b6040516101119190610e05565b60405180910390f35b610134600480360381019061012f9190610e20565b610311565b6040516101419190610ddb565b60405180910390f35b6101526103cf565b60405161015f9190610e8f565b60405180910390f35b610182600480360381019061017d9190610eaa565b6103d8565b60405161018f9190610e05565b60405180910390f35b6101a0610420565b6040516101ad9190610cc5565b60405180910390f35b6101be6104b2565b6040516101cb9190610e05565b60405180910390f35b6101ee60048036038101906101e99190610d80565b6104da565b6040516101fb9190610ddb565b60405180910390f35b61020c610596565b6040516102199190610ee6565b60405180910390f35b61023c60048036038101906102379190610f01565b6105be565b6040516102499190610e05565b60405180910390f35b60606003805461026190610f70565b80601f016020809104026020016040519081016040528092919081815260200182805461028d90610f70565b80156102da5780601f106102af576101008083540402835291602001916102da565b820191906000526020600020905b8154815290600101906020018083116102bd57829003601f168201915b5050505050905090565b6000806102ef610645565b90506102fc81858561064d565b600191505092915050565b6000600254905090565b6000827f00000000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036103b95760005a905060005b825a8361039d9190610fd0565b10156103b65780806103ae90611004565b915050610390565b50505b6103c486868661065f565b925050509392505050565b60006012905090565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60606004805461042f90610f70565b80601f016020809104026020016040519081016040528092919081815260200182805461045b90610f70565b80156104a85780601f1061047d576101008083540402835291602001916104a8565b820191906000526020600020905b81548152906001019060200180831161048b57829003601f168201915b5050505050905090565b60007f0000000000000000000000000000000000000000000000000000000000000000905090565b6000827f00000000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036105825760005a905060005b825a836105669190610fd0565b101561057f57808061057790611004565b915050610559565b50505b61058c858561068e565b9250505092915050565b60007f0000000000000000000000000000000000000000000000000000000000000000905090565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b61065a83838360016106b1565b505050565b60008061066a610645565b9050610677858285610888565b61068285858561091c565b60019150509392505050565b600080610699610645565b90506106a681858561091c565b600191505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036107235760006040517fe602df0500000000000000000000000000000000000000000000000000000000815260040161071a9190610ee6565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036107955760006040517f94280d6200000000000000000000000000000000000000000000000000000000815260040161078c9190610ee6565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508015610882578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516108799190610e05565b60405180910390a35b50505050565b600061089484846105be565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146109165781811015610906578281836040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526004016108fd9392919061104c565b60405180910390fd5b610915848484840360006106b1565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361098e5760006040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016109859190610ee6565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610a005760006040517fec442f050000000000000000000000000000000000000000000000000000000081526004016109f79190610ee6565b60405180910390fd5b610a0b838383610a10565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a62578060026000828254610a569190611083565b92505081905550610b35565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610aee578381836040517fe450d38c000000000000000000000000000000000000000000000000000000008152600401610ae59392919061104c565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610b7e5780600260008282540392505081905550610bcb565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610c289190610e05565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610c6f578082015181840152602081019050610c54565b60008484015250505050565b6000601f19601f8301169050919050565b6000610c9782610c35565b610ca18185610c40565b9350610cb1818560208601610c51565b610cba81610c7b565b840191505092915050565b60006020820190508181036000830152610cdf8184610c8c565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610d1782610cec565b9050919050565b610d2781610d0c565b8114610d3257600080fd5b50565b600081359050610d4481610d1e565b92915050565b6000819050919050565b610d5d81610d4a565b8114610d6857600080fd5b50565b600081359050610d7a81610d54565b92915050565b60008060408385031215610d9757610d96610ce7565b5b6000610da585828601610d35565b9250506020610db685828601610d6b565b9150509250929050565b60008115159050919050565b610dd581610dc0565b82525050565b6000602082019050610df06000830184610dcc565b92915050565b610dff81610d4a565b82525050565b6000602082019050610e1a6000830184610df6565b92915050565b600080600060608486031215610e3957610e38610ce7565b5b6000610e4786828701610d35565b9350506020610e5886828701610d35565b9250506040610e6986828701610d6b565b9150509250925092565b600060ff82169050919050565b610e8981610e73565b82525050565b6000602082019050610ea46000830184610e80565b92915050565b600060208284031215610ec057610ebf610ce7565b5b6000610ece84828501610d35565b91505092915050565b610ee081610d0c565b82525050565b6000602082019050610efb6000830184610ed7565b92915050565b60008060408385031215610f1857610f17610ce7565b5b6000610f2685828601610d35565b9250506020610f3785828601610d35565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610f8857607f821691505b602082108103610f9b57610f9a610f41565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610fdb82610d4a565b9150610fe683610d4a565b9250828203905081811115610ffe57610ffd610fa1565b5b92915050565b600061100f82610d4a565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361104157611040610fa1565b5b600182019050919050565b60006060820190506110616000830186610ed7565b61106e6020830185610df6565b61107b6040830184610df6565b949350505050565b600061108e82610d4a565b915061109983610d4a565b92508282019050808211156110b1576110b0610fa1565b5b9291505056fea264697066735822122099283c20e2da776f189d26b6a4aaa27f82776cf48f422188549437a98515ee5664736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100a95760003560e01c806370a082311161007157806370a082311461016857806395d89b41146101985780639861d75f146101b6578063a9059cbb146101d4578063d54f7d5e14610204578063dd62ed3e14610222576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610252565b6040516100c39190610cc5565b60405180910390f35b6100e660048036038101906100e19190610d80565b6102e4565b6040516100f39190610ddb565b60405180910390f35b610104610307565b6040516101119190610e05565b60405180910390f35b610134600480360381019061012f9190610e20565b610311565b6040516101419190610ddb565b60405180910390f35b6101526103cf565b60405161015f9190610e8f565b60405180910390f35b610182600480360381019061017d9190610eaa565b6103d8565b60405161018f9190610e05565b60405180910390f35b6101a0610420565b6040516101ad9190610cc5565b60405180910390f35b6101be6104b2565b6040516101cb9190610e05565b60405180910390f35b6101ee60048036038101906101e99190610d80565b6104da565b6040516101fb9190610ddb565b60405180910390f35b61020c610596565b6040516102199190610ee6565b60405180910390f35b61023c60048036038101906102379190610f01565b6105be565b6040516102499190610e05565b60405180910390f35b60606003805461026190610f70565b80601f016020809104026020016040519081016040528092919081815260200182805461028d90610f70565b80156102da5780601f106102af576101008083540402835291602001916102da565b820191906000526020600020905b8154815290600101906020018083116102bd57829003601f168201915b5050505050905090565b6000806102ef610645565b90506102fc81858561064d565b600191505092915050565b6000600254905090565b6000827f00000000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036103b95760005a905060005b825a8361039d9190610fd0565b10156103b65780806103ae90611004565b915050610390565b50505b6103c486868661065f565b925050509392505050565b60006012905090565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60606004805461042f90610f70565b80601f016020809104026020016040519081016040528092919081815260200182805461045b90610f70565b80156104a85780601f1061047d576101008083540402835291602001916104a8565b820191906000526020600020905b81548152906001019060200180831161048b57829003601f168201915b5050505050905090565b60007f0000000000000000000000000000000000000000000000000000000000000000905090565b6000827f00000000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036105825760005a905060005b825a836105669190610fd0565b101561057f57808061057790611004565b915050610559565b50505b61058c858561068e565b9250505092915050565b60007f0000000000000000000000000000000000000000000000000000000000000000905090565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b61065a83838360016106b1565b505050565b60008061066a610645565b9050610677858285610888565b61068285858561091c565b60019150509392505050565b600080610699610645565b90506106a681858561091c565b600191505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036107235760006040517fe602df0500000000000000000000000000000000000000000000000000000000815260040161071a9190610ee6565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036107955760006040517f94280d6200000000000000000000000000000000000000000000000000000000815260040161078c9190610ee6565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508015610882578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516108799190610e05565b60405180910390a35b50505050565b600061089484846105be565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146109165781811015610906578281836040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526004016108fd9392919061104c565b60405180910390fd5b610915848484840360006106b1565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361098e5760006040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016109859190610ee6565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610a005760006040517fec442f050000000000000000000000000000000000000000000000000000000081526004016109f79190610ee6565b60405180910390fd5b610a0b838383610a10565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a62578060026000828254610a569190611083565b92505081905550610b35565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610aee578381836040517fe450d38c000000000000000000000000000000000000000000000000000000008152600401610ae59392919061104c565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610b7e5780600260008282540392505081905550610bcb565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610c289190610e05565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610c6f578082015181840152602081019050610c54565b60008484015250505050565b6000601f19601f8301169050919050565b6000610c9782610c35565b610ca18185610c40565b9350610cb1818560208601610c51565b610cba81610c7b565b840191505092915050565b60006020820190508181036000830152610cdf8184610c8c565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610d1782610cec565b9050919050565b610d2781610d0c565b8114610d3257600080fd5b50565b600081359050610d4481610d1e565b92915050565b6000819050919050565b610d5d81610d4a565b8114610d6857600080fd5b50565b600081359050610d7a81610d54565b92915050565b60008060408385031215610d9757610d96610ce7565b5b6000610da585828601610d35565b9250506020610db685828601610d6b565b9150509250929050565b60008115159050919050565b610dd581610dc0565b82525050565b6000602082019050610df06000830184610dcc565b92915050565b610dff81610d4a565b82525050565b6000602082019050610e1a6000830184610df6565b92915050565b600080600060608486031215610e3957610e38610ce7565b5b6000610e4786828701610d35565b9350506020610e5886828701610d35565b9250506040610e6986828701610d6b565b9150509250925092565b600060ff82169050919050565b610e8981610e73565b82525050565b6000602082019050610ea46000830184610e80565b92915050565b600060208284031215610ec057610ebf610ce7565b5b6000610ece84828501610d35565b91505092915050565b610ee081610d0c565b82525050565b6000602082019050610efb6000830184610ed7565b92915050565b60008060408385031215610f1857610f17610ce7565b5b6000610f2685828601610d35565b9250506020610f3785828601610d35565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610f8857607f821691505b602082108103610f9b57610f9a610f41565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610fdb82610d4a565b9150610fe683610d4a565b9250828203905081811115610ffe57610ffd610fa1565b5b92915050565b600061100f82610d4a565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361104157611040610fa1565b5b600182019050919050565b60006060820190506110616000830186610ed7565b61106e6020830185610df6565b61107b6040830184610df6565b949350505050565b600061108e82610d4a565b915061109983610d4a565b92508282019050808211156110b1576110b0610fa1565b5b9291505056fea264697066735822122099283c20e2da776f189d26b6a4aaa27f82776cf48f422188549437a98515ee5664736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/scripts/test-contracts/contracts/ConditionalGasEater.sol b/scripts/test-contracts/contracts/ConditionalGasEater.sol new file mode 100644 index 000000000..a5e5c0587 --- /dev/null +++ b/scripts/test-contracts/contracts/ConditionalGasEater.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + + +/** + * @title ConditionalGasEater + * @dev ERC20 Token that conditionally wastes gas only when transferring to a specific router address + * @notice Used for testing DCA extra gas handling in router trades + */ +contract ConditionalGasEater is ERC20 { + address private immutable routerAddress; + uint256 private immutable gasToWaste; + + /** + * @param _routerAddress The address of the router pallet account (EVM H160) + * @param _gasToWaste Amount of gas to waste on transfers to the router + */ + constructor(address _routerAddress, uint256 _gasToWaste) + ERC20("ConditionalGasEater", "CGEATER") + { + routerAddress = _routerAddress; + gasToWaste = _gasToWaste; + _mint(msg.sender, 1_000_000_000 * 10 ** decimals()); + } + + /** + * @dev Wastes gas only if recipient is the router address + * @param recipient The transfer recipient + * @param amountToUse Amount of gas to waste + */ + modifier wasteGasConditionally(address recipient, uint256 amountToUse) { + if (recipient == routerAddress) { + uint256 startGas = gasleft(); + uint256 counter = 0; + while(startGas - gasleft() < amountToUse) { + counter++; + } + } + _; + } + + /** + * @dev Overrides transfer to conditionally waste gas + */ + function transfer(address recipient, uint256 amount) + public + override + wasteGasConditionally(recipient, gasToWaste) + returns (bool) + { + return super.transfer(recipient, amount); + } + + /** + * @dev Overrides transferFrom to conditionally waste gas + */ + function transferFrom(address sender, address recipient, uint256 amount) + public + override + wasteGasConditionally(recipient, gasToWaste) + returns (bool) + { + return super.transferFrom(sender, recipient, amount); + } + + /** + * @dev Getter for routerAddress (useful for testing) + */ + function getRouterAddress() public view returns (address) { + return routerAddress; + } + + /** + * @dev Getter for gasToWaste (useful for testing) + */ + function getGasToWaste() public view returns (uint256) { + return gasToWaste; + } +} diff --git a/traits/src/evm.rs b/traits/src/evm.rs index c37ade42c..826e6bcd4 100644 --- a/traits/src/evm.rs +++ b/traits/src/evm.rs @@ -2,7 +2,7 @@ use codec::{Decode, Encode}; use frame_support::sp_runtime; use frame_support::sp_runtime::app_crypto::sp_core; use frame_support::sp_runtime::app_crypto::sp_core::{H160, U256}; -use frame_support::sp_runtime::{DispatchResult, RuntimeDebug}; +use frame_support::sp_runtime::{DispatchError, DispatchResult, RuntimeDebug}; use pallet_evm::ExitReason; use sp_std::vec::Vec; @@ -123,3 +123,10 @@ pub trait Erc20OnDust { currency_id: CurrencyId, ) -> frame_support::dispatch::DispatchResult; } + +pub trait ExtraGasSupport { + fn set_extra_gas(gas: u64); + fn clear_extra_gas(); + + fn out_of_gas_error() -> DispatchError; +} From 1cf28010ad4a2f0adfb18bb5e231e8c03f9cb086 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 22 Dec 2025 21:46:58 +0100 Subject: [PATCH 02/19] map reverted error to out of gas, based on usage --- integration-tests/src/dca.rs | 154 ++++++ integration-tests/src/evm.rs | 49 +- pallets/hsm/src/tests/mock.rs | 14 + pallets/liquidation/src/tests/mock.rs | 8 + runtime/hydradx/src/assets.rs | 2 + runtime/hydradx/src/evm/evm_error_decoder.rs | 25 +- runtime/hydradx/src/evm/executor.rs | 13 +- runtime/hydradx/src/helpers.rs | 7 + .../GasWaster.json | 43 ++ .../SubcallGasExhaustionToken.json | 439 ++++++++++++++++++ .../contracts/SubcallGasExhaustionToken.sol | 118 +++++ traits/src/evm.rs | 2 + 12 files changed, 871 insertions(+), 3 deletions(-) create mode 100644 scripts/test-contracts/artifacts/contracts/SubcallGasExhaustionToken.sol/GasWaster.json create mode 100644 scripts/test-contracts/artifacts/contracts/SubcallGasExhaustionToken.sol/SubcallGasExhaustionToken.json create mode 100644 scripts/test-contracts/contracts/SubcallGasExhaustionToken.sol diff --git a/integration-tests/src/dca.rs b/integration-tests/src/dca.rs index 4abb99058..0f277f0e5 100644 --- a/integration-tests/src/dca.rs +++ b/integration-tests/src/dca.rs @@ -4972,6 +4972,126 @@ mod extra_gas_erc20 { }); } + #[test] + fn subcall_gas_exhaustion_should_be_detected_and_retried() { + // This test reproduces the AAVE scenario where: + // 1. Main contract makes subcall to helper contract + // 2. Subcall gets 63/64 of gas (EIP-150) + // 3. Subcall runs out of gas and returns false + // 4. Main contract detects failure and reverts with empty message + // 5. Our detector should catch this as OutOfGas (gas_used > 90%) + + TestNet::reset(); + + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + + let evm_address = EVMAccounts::evm_address(&Router::router_account()); + + // Deploy contract with subcall that will exhaust gas + // Use 400_000 gas to ensure we hit >90% threshold + let gas_to_waste: u64 = 400_000; + let contract = deploy_subcall_gas_exhaustion_token(evm_address, gas_to_waste, crate::erc20::deployer()); + let erc20 = crate::erc20::bind_erc20(contract); + + // Add to omnipool with oracle + assert_ok!(EmaOracle::add_oracle( + hydradx_runtime::RuntimeOrigin::root(), + OMNIPOOL_SOURCE, + (LRNA, erc20), + )); + + //Add new erc20 to omnipool + let bal = Currencies::free_balance(erc20, &ALICE.into()); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(ALICE.into()), + pallet_omnipool::Pallet::::protocol_account(), + erc20, + bal / 10, + )); + assert_ok!(pallet_omnipool::Pallet::::add_token( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200), + Permill::from_percent(30), + ALICE.into(), + )); + + assert_ok!(MultiTransactionPayment::add_currency( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200) + )); + + hydradx_run_to_block(11); + + let amount_in = 200000 * UNITS; + let schedule_id = create_schedule_with_onchain_route( + erc20, + HDX, + amount_in, + 500000 * UNITS, + Some(3), // Allow retries + ); + + // First execution attempt - should fail with subcall gas exhaustion + hydradx_run_to_block(13); + + //Assert + assert_eq!( + DCA::retries_on_error(schedule_id), + 1, + "Should have 1 retry after subcall gas exhaustion" + ); + assert_eq!( + DCA::schedule_extra_gas(schedule_id), + 333_333, + "Extra gas should be increased after first failure" + ); + + trade_failed_with_evm_out_of_gas_error(schedule_id); + assert_eq!(count_failed_trade_events(), 1); + assert_eq!(count_trade_executed_events(), 0); + + // Retry with extra gas - should succeed + hydradx_run_to_block(33); + + //Assert + assert_eq!( + DCA::retries_on_error(schedule_id), + 0, + "Retries should reset to 0 after success" + ); + assert_eq!( + DCA::schedule_extra_gas(schedule_id), + 333_333, + "Extra gas should persist for future executions" + ); + + assert_trade_executed_succesfully(schedule_id); + assert_eq!( + count_failed_trade_events(), + 1, + "Should still have 1 failed event from first attempt" + ); + assert_eq!( + count_trade_executed_events(), + 1, + "Should have 1 successful trade after retry" + ); + + // Act - Next scheduled trade - should execute successfully without retries + hydradx_run_to_block(38); + + //Assert + assert_eq!( + count_trade_executed_events(), + 2, + "Should have 2 successful trades total" + ); + }); + } + fn create_schedule_with_onchain_route( asset_in: AssetId, asset_out: AssetId, @@ -5053,4 +5173,38 @@ mod extra_gas_erc20 { }); assert!(has_trade_executed, "Expected TradeExecuted event after retry"); } + + /// Deploy SubcallGasExhaustionToken contract with constructor parameters + /// + /// This contract mimics AAVE's behavior where: + /// - Main contract makes subcall to helper GasWaster contract + /// - Subcall gets 63/64 of remaining gas (EIP-150) + /// - When subcall exhausts gas, main contract reverts with empty message + /// + /// # Arguments + /// * `router_address` - EVM address of the router pallet account + /// * `gas_to_waste` - Amount of gas to waste in the subcall + /// * `deployer` - EVM address of the contract deployer + /// + /// # Returns + /// The deployed contract's EVM address + fn deploy_subcall_gas_exhaustion_token( + router_address: EvmAddress, + gas_to_waste: u64, + deployer: EvmAddress, + ) -> EvmAddress { + use ethabi::{encode, Token}; + + // Get base bytecode from compiled artifact + let mut bytecode = crate::utils::contracts::get_contract_bytecode("SubcallGasExhaustionToken"); + + // Encode constructor parameters: (address _routerAddress, uint256 _gasToWaste) + let constructor_params = encode(&[Token::Address(router_address.into()), Token::Uint(gas_to_waste.into())]); + + // Append encoded constructor params to bytecode + bytecode.extend(constructor_params); + + // Deploy contract with complete bytecode + crate::utils::contracts::deploy_contract_code(bytecode, deployer) + } } diff --git a/integration-tests/src/evm.rs b/integration-tests/src/evm.rs index 75fab26a9..3cbbb7d28 100644 --- a/integration-tests/src/evm.rs +++ b/integration-tests/src/evm.rs @@ -4358,7 +4358,8 @@ mod evm_error_decoder { exit_reason, value, contract, - }; + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let _result = EvmErrorDecoder::convert(call_result); } @@ -4384,6 +4385,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Error(ExitError::Other("Some error".into())), value, contract: hydradx_runtime::Liquidation::get(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let _result = EvmErrorDecoder::convert(call_result); @@ -4400,6 +4403,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: vec![], contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let _error = EvmErrorDecoder::convert(call_result); @@ -4411,6 +4416,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: vec![0x01, 0x02], contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let _error = EvmErrorDecoder::convert(call_result); @@ -4439,6 +4446,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: value.clone(), contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4454,6 +4463,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: value.clone(), contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4469,6 +4480,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: value.clone(), contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4484,6 +4497,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: value.clone(), contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4499,6 +4514,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: value.clone(), contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4523,6 +4540,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4541,6 +4560,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4558,6 +4579,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4583,6 +4606,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4604,6 +4629,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4626,6 +4653,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: nested_data, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4651,6 +4680,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4672,6 +4703,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4688,6 +4721,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: U256::from(400_000), }; let result = EvmErrorDecoder::convert(call_result); @@ -4704,6 +4739,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4730,6 +4767,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: value.clone(), contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let _result = EvmErrorDecoder::convert(call_result); @@ -4770,6 +4809,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value, contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let _result = EvmErrorDecoder::convert(call_result.clone()); @@ -4785,6 +4826,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Revert(ExitRevert::Reverted), value: vec![*byte1, *byte2], contract: sp_core::H160::zero(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let _result = EvmErrorDecoder::convert(call_result.clone()); @@ -4806,6 +4849,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: error_data, contract: hydradx_runtime::Liquidation::get(), + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); @@ -4830,6 +4875,8 @@ mod evm_error_decoder { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: error_data, contract: H160::from_low_u64_be(12345), // Different contract + gas_used: sp_core::U256::zero(), + gas_limit: sp_core::U256::zero(), }; let result = EvmErrorDecoder::convert(call_result); diff --git a/pallets/hsm/src/tests/mock.rs b/pallets/hsm/src/tests/mock.rs index e950dc144..abcb2ce68 100644 --- a/pallets/hsm/src/tests/mock.rs +++ b/pallets/hsm/src/tests/mock.rs @@ -326,6 +326,8 @@ impl EVM for MockEvm { exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }; } @@ -359,6 +361,8 @@ impl EVM for MockEvm { exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } @@ -383,6 +387,8 @@ impl EVM for MockEvm { exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } @@ -423,6 +429,8 @@ impl EVM for MockEvm { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }; } else { panic!("incorrect data len"); @@ -437,6 +445,8 @@ impl EVM for MockEvm { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: bytes, contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }; } ERC20Function::GetFacilitatorBucket => { @@ -453,6 +463,8 @@ impl EVM for MockEvm { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: bytes, contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } @@ -463,6 +475,8 @@ impl EVM for MockEvm { exit_reason: ExitReason::Error(ExitError::DesignatedInvalid), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), } } diff --git a/pallets/liquidation/src/tests/mock.rs b/pallets/liquidation/src/tests/mock.rs index 3620e0def..282fb9b45 100644 --- a/pallets/liquidation/src/tests/mock.rs +++ b/pallets/liquidation/src/tests/mock.rs @@ -109,6 +109,8 @@ impl EVM for EvmMock { exit_reason: ExitReason::Error(ExitError::DesignatedInvalid), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }; }; @@ -137,6 +139,8 @@ impl EVM for EvmMock { exit_reason: ExitReason::Error(ExitError::DesignatedInvalid), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } @@ -145,6 +149,8 @@ impl EVM for EvmMock { exit_reason: ExitReason::Error(ExitError::DesignatedInvalid), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), } } } @@ -153,6 +159,8 @@ impl EVM for EvmMock { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: vec![], contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), } } diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index 6b1452899..fd5a5b6c7 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -1773,6 +1773,7 @@ impl hydradx_traits::evm::EVM for DummyEvm { exit_reason: pallet_evm::ExitReason::Succeed(pallet_evm::ExitSucceed::Returned), value: vec![], contract: context.contract, + gas_used: U256::zero(), } } @@ -1781,6 +1782,7 @@ impl hydradx_traits::evm::EVM for DummyEvm { exit_reason: pallet_evm::ExitReason::Succeed(pallet_evm::ExitSucceed::Returned), value: vec![], contract: context.contract, + gas_used: U256::zero(), } } } diff --git a/runtime/hydradx/src/evm/evm_error_decoder.rs b/runtime/hydradx/src/evm/evm_error_decoder.rs index 3fe181e68..59ed2aba3 100644 --- a/runtime/hydradx/src/evm/evm_error_decoder.rs +++ b/runtime/hydradx/src/evm/evm_error_decoder.rs @@ -2,7 +2,8 @@ use crate::Liquidation; use codec::{Decode, DecodeLimit}; use frame_support::traits::Get; use hydradx_traits::evm::CallResult; -use pallet_evm::{ExitError, ExitReason}; +use pallet_evm::{ExitError, ExitReason, ExitRevert}; +use sp_core::U256; use sp_runtime::format; use sp_runtime::traits::Convert; use sp_runtime::DispatchError; @@ -23,6 +24,28 @@ impl Convert for EvmErrorDecoder { return pallet_dispatcher::Error::::EvmOutOfGas.into(); } + // EVM Subcalls exits with Revert with empty data on gas exhaustion, so we check for that case + if let ExitReason::Revert(ExitRevert::Reverted) = call_result.exit_reason { + if call_result.value.is_empty() { + let threshold = call_result + .gas_limit + .checked_mul(U256::from(90)) + .and_then(|v| v.checked_div(U256::from(100))) + .unwrap_or(U256::zero()); + + if call_result.gas_used >= threshold { + log::warn!( + target: "evm::error_decoder", + "Detected subcall gas exhaustion: {:?} gas used out of {:?} limit (contract: {:?})", + call_result.gas_used, + call_result.gas_limit, + call_result.contract + ); + return pallet_dispatcher::Error::::EvmOutOfGas.into(); + } + } + } + // DOS Prevention: Limit error data size to prevent memory exhaustion attacks if call_result.value.len() > MAX_ERROR_DATA_LENGTH { log::warn!( diff --git a/runtime/hydradx/src/evm/executor.rs b/runtime/hydradx/src/evm/executor.rs index 8ca2b1680..a5af6f22b 100644 --- a/runtime/hydradx/src/evm/executor.rs +++ b/runtime/hydradx/src/evm/executor.rs @@ -109,6 +109,8 @@ where exit_reason, value: Vec::new(), contract: context.contract, + gas_used: U256::from(gas_limit), + gas_limit: U256::from(gas_limit), }; } } @@ -117,6 +119,8 @@ where exit_reason: info.exit_reason, value: info.value, contract: context.contract, + gas_used: info.used_gas.effective, + gas_limit: U256::from(gas_limit), } } Err(runner_error) => { @@ -127,6 +131,8 @@ where exit_reason: exit_reason, value: Vec::new(), contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::from(gas_limit), } } } @@ -143,14 +149,17 @@ where let result = Self::execute(context.origin, gas_limit, |executor| { let result = executor.transact_call(context.sender, context.contract, U256::zero(), data, gas_limit, vec![]); + let gas_used_val = executor.used_gas(); if extra_gas > 0 { - extra_gas_used = executor.used_gas().saturating_sub(gas); + extra_gas_used = gas_used_val.saturating_sub(gas); log::trace!(target: "evm::executor", "View used extra gas -{:?}", extra_gas_used); } CallResult { exit_reason: result.0, value: result.1, contract: context.contract, + gas_used: U256::from(gas_used_val), + gas_limit: U256::from(gas_limit), } }); TransactionOutcome::Rollback(Ok::(result)) @@ -159,6 +168,8 @@ where exit_reason: ExitReason::Fatal(Other("TransactionalError".into())), value: Vec::new(), contract: context.contract, + gas_used: U256::zero(), + gas_limit: U256::zero(), }); if extra_gas_used > 0 { diff --git a/runtime/hydradx/src/helpers.rs b/runtime/hydradx/src/helpers.rs index 5129596eb..72b6e4ac4 100644 --- a/runtime/hydradx/src/helpers.rs +++ b/runtime/hydradx/src/helpers.rs @@ -57,6 +57,7 @@ pub mod benchmark_helpers { contract: H160::zero(), exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], + gas_used: U256::zero(), }; } } @@ -81,6 +82,7 @@ pub mod benchmark_helpers { contract: H160::zero(), exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], + gas_used: U256::zero(), }; } } @@ -117,6 +119,7 @@ pub mod benchmark_helpers { contract: H160::zero(), exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: vec![], + gas_used: U256::zero(), }; } } @@ -129,6 +132,7 @@ pub mod benchmark_helpers { contract: H160::zero(), exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: bytes, + gas_used: U256::zero(), }; } ERC20Function::GetFacilitatorBucket => { @@ -145,6 +149,7 @@ pub mod benchmark_helpers { contract: H160::zero(), exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: bytes, + gas_used: U256::zero(), }; } } @@ -155,6 +160,7 @@ pub mod benchmark_helpers { contract: H160::zero(), exit_reason: ExitReason::Revert(Reverted), value: vec![], + gas_used: U256::zero(), }; } @@ -163,6 +169,7 @@ pub mod benchmark_helpers { contract: H160::zero(), exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], + gas_used: U256::zero(), }; } } diff --git a/scripts/test-contracts/artifacts/contracts/SubcallGasExhaustionToken.sol/GasWaster.json b/scripts/test-contracts/artifacts/contracts/SubcallGasExhaustionToken.sol/GasWaster.json new file mode 100644 index 000000000..811b7ba8d --- /dev/null +++ b/scripts/test-contracts/artifacts/contracts/SubcallGasExhaustionToken.sol/GasWaster.json @@ -0,0 +1,43 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "GasWaster", + "sourceName": "contracts/SubcallGasExhaustionToken.sol", + "abi": [ + { + "inputs": [], + "name": "counter", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "gasToWaste", + "type": "uint256" + } + ], + "name": "wasteGas", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b50610284806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806361bc221a1461003b5780637fa4dcd814610059575b600080fd5b610043610089565b60405161005091906100f4565b60405180910390f35b610073600480360381019061006e9190610140565b61008f565b6040516100809190610188565b60405180910390f35b60005481565b6000805a90505b825a826100a391906101d2565b1080156100b0575060005a115b156100d1576000808154809291906100c790610206565b9190505550610096565b6001915050919050565b6000819050919050565b6100ee816100db565b82525050565b600060208201905061010960008301846100e5565b92915050565b600080fd5b61011d816100db565b811461012857600080fd5b50565b60008135905061013a81610114565b92915050565b6000602082840312156101565761015561010f565b5b60006101648482850161012b565b91505092915050565b60008115159050919050565b6101828161016d565b82525050565b600060208201905061019d6000830184610179565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006101dd826100db565b91506101e8836100db565b9250828203905081811115610200576101ff6101a3565b5b92915050565b6000610211826100db565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610243576102426101a3565b5b60018201905091905056fea2646970667358221220f04eeceb69d3bcf9e1e44e9c546dcf85ad065033112abfd369d704a698a8f20364736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c806361bc221a1461003b5780637fa4dcd814610059575b600080fd5b610043610089565b60405161005091906100f4565b60405180910390f35b610073600480360381019061006e9190610140565b61008f565b6040516100809190610188565b60405180910390f35b60005481565b6000805a90505b825a826100a391906101d2565b1080156100b0575060005a115b156100d1576000808154809291906100c790610206565b9190505550610096565b6001915050919050565b6000819050919050565b6100ee816100db565b82525050565b600060208201905061010960008301846100e5565b92915050565b600080fd5b61011d816100db565b811461012857600080fd5b50565b60008135905061013a81610114565b92915050565b6000602082840312156101565761015561010f565b5b60006101648482850161012b565b91505092915050565b60008115159050919050565b6101828161016d565b82525050565b600060208201905061019d6000830184610179565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006101dd826100db565b91506101e8836100db565b9250828203905081811115610200576101ff6101a3565b5b92915050565b6000610211826100db565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610243576102426101a3565b5b60018201905091905056fea2646970667358221220f04eeceb69d3bcf9e1e44e9c546dcf85ad065033112abfd369d704a698a8f20364736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/scripts/test-contracts/artifacts/contracts/SubcallGasExhaustionToken.sol/SubcallGasExhaustionToken.json b/scripts/test-contracts/artifacts/contracts/SubcallGasExhaustionToken.sol/SubcallGasExhaustionToken.json new file mode 100644 index 000000000..926098aeb --- /dev/null +++ b/scripts/test-contracts/artifacts/contracts/SubcallGasExhaustionToken.sol/SubcallGasExhaustionToken.json @@ -0,0 +1,439 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "SubcallGasExhaustionToken", + "sourceName": "contracts/SubcallGasExhaustionToken.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_routerAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_gasToWaste", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "GasWastingEnabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "gasLeft", + "type": "uint256" + } + ], + "name": "SubcallFailed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "enableGasWasting", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasToWaste", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasWaster", + "outputs": [ + { + "internalType": "contract GasWaster", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGasWasterAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "routerAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_enable", + "type": "bool" + } + ], + "name": "setEnableGasWasting", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b50604051620022df380380620022df833981810160405281019062000037919062000558565b6040518060400160405280600f81526020017f53756263616c6c476173546f6b656e00000000000000000000000000000000008152506040518060400160405280600381526020017f53475400000000000000000000000000000000000000000000000000000000008152508160039081620000b491906200080f565b508060049081620000c691906200080f565b50505081600660006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550806007819055506001600860006101000a81548160ff0219169083151502179055506040516200013a90620004a5565b604051809103906000f08015801562000157573d6000803e3d6000fd5b50600560006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550620001d733620001ad620001df60201b60201c565b600a620001bb919062000a86565b620f4240620001cb919062000ad7565b620001e860201b60201c565b505062000bf6565b60006012905090565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036200025d5760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040162000254919062000b33565b60405180910390fd5b62000271600083836200027560201b60201c565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603620002cb578060026000828254620002be919062000b50565b92505081905550620003a1565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156200035a578381836040517fe450d38c000000000000000000000000000000000000000000000000000000008152600401620003519392919062000b9c565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620003ec578060026000828254039250508190555062000439565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405162000498919062000bd9565b60405180910390a3505050565b6102a4806200203b83390190565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000620004e582620004b8565b9050919050565b620004f781620004d8565b81146200050357600080fd5b50565b6000815190506200051781620004ec565b92915050565b6000819050919050565b62000532816200051d565b81146200053e57600080fd5b50565b600081519050620005528162000527565b92915050565b60008060408385031215620005725762000571620004b3565b5b6000620005828582860162000506565b9250506020620005958582860162000541565b9150509250929050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200062157607f821691505b602082108103620006375762000636620005d9565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620006a17fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000662565b620006ad868362000662565b95508019841693508086168417925050509392505050565b6000819050919050565b6000620006f0620006ea620006e4846200051d565b620006c5565b6200051d565b9050919050565b6000819050919050565b6200070c83620006cf565b620007246200071b82620006f7565b8484546200066f565b825550505050565b600090565b6200073b6200072c565b6200074881848462000701565b505050565b5b8181101562000770576200076460008262000731565b6001810190506200074e565b5050565b601f821115620007bf5762000789816200063d565b620007948462000652565b81016020851015620007a4578190505b620007bc620007b38562000652565b8301826200074d565b50505b505050565b600082821c905092915050565b6000620007e460001984600802620007c4565b1980831691505092915050565b6000620007ff8383620007d1565b9150826002028217905092915050565b6200081a826200059f565b67ffffffffffffffff811115620008365762000835620005aa565b5b62000842825462000608565b6200084f82828562000774565b600060209050601f83116001811462000887576000841562000872578287015190505b6200087e8582620007f1565b865550620008ee565b601f19841662000897866200063d565b60005b82811015620008c1578489015182556001820191506020850194506020810190506200089a565b86831015620008e15784890151620008dd601f891682620007d1565b8355505b6001600288020188555050505b505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60008160011c9050919050565b6000808291508390505b600185111562000984578086048111156200095c576200095b620008f6565b5b60018516156200096c5780820291505b80810290506200097c8562000925565b94506200093c565b94509492505050565b6000826200099f576001905062000a72565b81620009af576000905062000a72565b8160018114620009c85760028114620009d35762000a09565b600191505062000a72565b60ff841115620009e857620009e7620008f6565b5b8360020a91508482111562000a025762000a01620008f6565b5b5062000a72565b5060208310610133831016604e8410600b841016171562000a435782820a90508381111562000a3d5762000a3c620008f6565b5b62000a72565b62000a52848484600162000932565b9250905081840481111562000a6c5762000a6b620008f6565b5b81810290505b9392505050565b600060ff82169050919050565b600062000a93826200051d565b915062000aa08362000a79565b925062000acf7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84846200098d565b905092915050565b600062000ae4826200051d565b915062000af1836200051d565b925082820262000b01816200051d565b9150828204841483151762000b1b5762000b1a620008f6565b5b5092915050565b62000b2d81620004d8565b82525050565b600060208201905062000b4a600083018462000b22565b92915050565b600062000b5d826200051d565b915062000b6a836200051d565b925082820190508082111562000b855762000b84620008f6565b5b92915050565b62000b96816200051d565b82525050565b600060608201905062000bb3600083018662000b22565b62000bc2602083018562000b8b565b62000bd1604083018462000b8b565b949350505050565b600060208201905062000bf0600083018462000b8b565b92915050565b6114358062000c066000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c806391f4735311610097578063a9059cbb11610066578063a9059cbb14610278578063ce2539e8146102a8578063dd62ed3e146102c6578063ed2a7b04146102f6576100f5565b806391f473531461020257806395d89b4114610220578063a69ce5e71461023e578063a6fd65671461025c576100f5565b806323b872dd116100d357806323b872dd14610166578063313ce567146101965780633268cc56146101b457806370a08231146101d2576100f5565b806306fdde03146100fa578063095ea7b31461011857806318160ddd14610148575b600080fd5b610102610314565b60405161010f9190610f74565b60405180910390f35b610132600480360381019061012d919061102f565b6103a6565b60405161013f919061108a565b60405180910390f35b6101506103c9565b60405161015d91906110b4565b60405180910390f35b610180600480360381019061017b91906110cf565b6103d3565b60405161018d919061108a565b60405180910390f35b61019e61053e565b6040516101ab919061113e565b60405180910390f35b6101bc610547565b6040516101c99190611168565b60405180910390f35b6101ec60048036038101906101e79190611183565b61056d565b6040516101f991906110b4565b60405180910390f35b61020a6105b5565b604051610217919061120f565b60405180910390f35b6102286105db565b6040516102359190610f74565b60405180910390f35b61024661066d565b604051610253919061108a565b60405180910390f35b61027660048036038101906102719190611256565b610680565b005b610292600480360381019061028d919061102f565b6106d4565b60405161029f919061108a565b60405180910390f35b6102b061083d565b6040516102bd91906110b4565b60405180910390f35b6102e060048036038101906102db9190611283565b610843565b6040516102ed91906110b4565b60405180910390f35b6102fe6108ca565b60405161030b9190611168565b60405180910390f35b606060038054610323906112f2565b80601f016020809104026020016040519081016040528092919081815260200182805461034f906112f2565b801561039c5780601f106103715761010080835404028352916020019161039c565b820191906000526020600020905b81548152906001019060200180831161037f57829003601f168201915b5050505050905090565b6000806103b16108f4565b90506103be8185856108fc565b600191505092915050565b6000600254905090565b6000600860009054906101000a900460ff16801561043e5750600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16145b1561052a576000600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16637fa4dcd86007546040518263ffffffff1660e01b81526004016104a291906110b4565b6020604051808303816000875af11580156104c1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104e59190611338565b905080610528577fdbc5d06f4f877f959b1ff12d2161cdd693fa8e442ee53f1790b2804b24881f055a60405161051b91906110b4565b60405180910390a1600080fd5b505b61053584848461090e565b90509392505050565b60006012905090565b600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6060600480546105ea906112f2565b80601f0160208091040260200160405190810160405280929190818152602001828054610616906112f2565b80156106635780601f1061063857610100808354040283529160200191610663565b820191906000526020600020905b81548152906001019060200180831161064657829003601f168201915b5050505050905090565b600860009054906101000a900460ff1681565b80600860006101000a81548160ff0219169083151502179055507f1ac70fe8ed988f54e0df47c56a8f8942e4fccf01e0b44e6cd09cc38d8b5ad1ef816040516106c9919061108a565b60405180910390a150565b6000600860009054906101000a900460ff16801561073f5750600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16145b1561082b576000600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16637fa4dcd86007546040518263ffffffff1660e01b81526004016107a391906110b4565b6020604051808303816000875af11580156107c2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107e69190611338565b905080610829577fdbc5d06f4f877f959b1ff12d2161cdd693fa8e442ee53f1790b2804b24881f055a60405161081c91906110b4565b60405180910390a1600080fd5b505b610835838361093d565b905092915050565b60075481565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b6000600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600033905090565b6109098383836001610960565b505050565b6000806109196108f4565b9050610926858285610b37565b610931858585610bcb565b60019150509392505050565b6000806109486108f4565b9050610955818585610bcb565b600191505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036109d25760006040517fe602df050000000000000000000000000000000000000000000000000000000081526004016109c99190611168565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a445760006040517f94280d62000000000000000000000000000000000000000000000000000000008152600401610a3b9190611168565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508015610b31578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92584604051610b2891906110b4565b60405180910390a35b50505050565b6000610b438484610843565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610bc55781811015610bb5578281836040517ffb8f41b2000000000000000000000000000000000000000000000000000000008152600401610bac93929190611365565b60405180910390fd5b610bc484848484036000610960565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610c3d5760006040517f96c6fd1e000000000000000000000000000000000000000000000000000000008152600401610c349190611168565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610caf5760006040517fec442f05000000000000000000000000000000000000000000000000000000008152600401610ca69190611168565b60405180910390fd5b610cba838383610cbf565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610d11578060026000828254610d0591906113cb565b92505081905550610de4565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610d9d578381836040517fe450d38c000000000000000000000000000000000000000000000000000000008152600401610d9493929190611365565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610e2d5780600260008282540392505081905550610e7a565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610ed791906110b4565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610f1e578082015181840152602081019050610f03565b60008484015250505050565b6000601f19601f8301169050919050565b6000610f4682610ee4565b610f508185610eef565b9350610f60818560208601610f00565b610f6981610f2a565b840191505092915050565b60006020820190508181036000830152610f8e8184610f3b565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610fc682610f9b565b9050919050565b610fd681610fbb565b8114610fe157600080fd5b50565b600081359050610ff381610fcd565b92915050565b6000819050919050565b61100c81610ff9565b811461101757600080fd5b50565b60008135905061102981611003565b92915050565b6000806040838503121561104657611045610f96565b5b600061105485828601610fe4565b92505060206110658582860161101a565b9150509250929050565b60008115159050919050565b6110848161106f565b82525050565b600060208201905061109f600083018461107b565b92915050565b6110ae81610ff9565b82525050565b60006020820190506110c960008301846110a5565b92915050565b6000806000606084860312156110e8576110e7610f96565b5b60006110f686828701610fe4565b935050602061110786828701610fe4565b92505060406111188682870161101a565b9150509250925092565b600060ff82169050919050565b61113881611122565b82525050565b6000602082019050611153600083018461112f565b92915050565b61116281610fbb565b82525050565b600060208201905061117d6000830184611159565b92915050565b60006020828403121561119957611198610f96565b5b60006111a784828501610fe4565b91505092915050565b6000819050919050565b60006111d56111d06111cb84610f9b565b6111b0565b610f9b565b9050919050565b60006111e7826111ba565b9050919050565b60006111f9826111dc565b9050919050565b611209816111ee565b82525050565b60006020820190506112246000830184611200565b92915050565b6112338161106f565b811461123e57600080fd5b50565b6000813590506112508161122a565b92915050565b60006020828403121561126c5761126b610f96565b5b600061127a84828501611241565b91505092915050565b6000806040838503121561129a57611299610f96565b5b60006112a885828601610fe4565b92505060206112b985828601610fe4565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061130a57607f821691505b60208210810361131d5761131c6112c3565b5b50919050565b6000815190506113328161122a565b92915050565b60006020828403121561134e5761134d610f96565b5b600061135c84828501611323565b91505092915050565b600060608201905061137a6000830186611159565b61138760208301856110a5565b61139460408301846110a5565b949350505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006113d682610ff9565b91506113e183610ff9565b92508282019050808211156113f9576113f861139c565b5b9291505056fea264697066735822122025faa3de26c129b60192a77ef7674148a6eb24196b975195ff96279c451cc1c764736f6c63430008180033608060405234801561001057600080fd5b50610284806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806361bc221a1461003b5780637fa4dcd814610059575b600080fd5b610043610089565b60405161005091906100f4565b60405180910390f35b610073600480360381019061006e9190610140565b61008f565b6040516100809190610188565b60405180910390f35b60005481565b6000805a90505b825a826100a391906101d2565b1080156100b0575060005a115b156100d1576000808154809291906100c790610206565b9190505550610096565b6001915050919050565b6000819050919050565b6100ee816100db565b82525050565b600060208201905061010960008301846100e5565b92915050565b600080fd5b61011d816100db565b811461012857600080fd5b50565b60008135905061013a81610114565b92915050565b6000602082840312156101565761015561010f565b5b60006101648482850161012b565b91505092915050565b60008115159050919050565b6101828161016d565b82525050565b600060208201905061019d6000830184610179565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006101dd826100db565b91506101e8836100db565b9250828203905081811115610200576101ff6101a3565b5b92915050565b6000610211826100db565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610243576102426101a3565b5b60018201905091905056fea2646970667358221220f04eeceb69d3bcf9e1e44e9c546dcf85ad065033112abfd369d704a698a8f20364736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100f55760003560e01c806391f4735311610097578063a9059cbb11610066578063a9059cbb14610278578063ce2539e8146102a8578063dd62ed3e146102c6578063ed2a7b04146102f6576100f5565b806391f473531461020257806395d89b4114610220578063a69ce5e71461023e578063a6fd65671461025c576100f5565b806323b872dd116100d357806323b872dd14610166578063313ce567146101965780633268cc56146101b457806370a08231146101d2576100f5565b806306fdde03146100fa578063095ea7b31461011857806318160ddd14610148575b600080fd5b610102610314565b60405161010f9190610f74565b60405180910390f35b610132600480360381019061012d919061102f565b6103a6565b60405161013f919061108a565b60405180910390f35b6101506103c9565b60405161015d91906110b4565b60405180910390f35b610180600480360381019061017b91906110cf565b6103d3565b60405161018d919061108a565b60405180910390f35b61019e61053e565b6040516101ab919061113e565b60405180910390f35b6101bc610547565b6040516101c99190611168565b60405180910390f35b6101ec60048036038101906101e79190611183565b61056d565b6040516101f991906110b4565b60405180910390f35b61020a6105b5565b604051610217919061120f565b60405180910390f35b6102286105db565b6040516102359190610f74565b60405180910390f35b61024661066d565b604051610253919061108a565b60405180910390f35b61027660048036038101906102719190611256565b610680565b005b610292600480360381019061028d919061102f565b6106d4565b60405161029f919061108a565b60405180910390f35b6102b061083d565b6040516102bd91906110b4565b60405180910390f35b6102e060048036038101906102db9190611283565b610843565b6040516102ed91906110b4565b60405180910390f35b6102fe6108ca565b60405161030b9190611168565b60405180910390f35b606060038054610323906112f2565b80601f016020809104026020016040519081016040528092919081815260200182805461034f906112f2565b801561039c5780601f106103715761010080835404028352916020019161039c565b820191906000526020600020905b81548152906001019060200180831161037f57829003601f168201915b5050505050905090565b6000806103b16108f4565b90506103be8185856108fc565b600191505092915050565b6000600254905090565b6000600860009054906101000a900460ff16801561043e5750600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16145b1561052a576000600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16637fa4dcd86007546040518263ffffffff1660e01b81526004016104a291906110b4565b6020604051808303816000875af11580156104c1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104e59190611338565b905080610528577fdbc5d06f4f877f959b1ff12d2161cdd693fa8e442ee53f1790b2804b24881f055a60405161051b91906110b4565b60405180910390a1600080fd5b505b61053584848461090e565b90509392505050565b60006012905090565b600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6060600480546105ea906112f2565b80601f0160208091040260200160405190810160405280929190818152602001828054610616906112f2565b80156106635780601f1061063857610100808354040283529160200191610663565b820191906000526020600020905b81548152906001019060200180831161064657829003601f168201915b5050505050905090565b600860009054906101000a900460ff1681565b80600860006101000a81548160ff0219169083151502179055507f1ac70fe8ed988f54e0df47c56a8f8942e4fccf01e0b44e6cd09cc38d8b5ad1ef816040516106c9919061108a565b60405180910390a150565b6000600860009054906101000a900460ff16801561073f5750600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16145b1561082b576000600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16637fa4dcd86007546040518263ffffffff1660e01b81526004016107a391906110b4565b6020604051808303816000875af11580156107c2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107e69190611338565b905080610829577fdbc5d06f4f877f959b1ff12d2161cdd693fa8e442ee53f1790b2804b24881f055a60405161081c91906110b4565b60405180910390a1600080fd5b505b610835838361093d565b905092915050565b60075481565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b6000600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600033905090565b6109098383836001610960565b505050565b6000806109196108f4565b9050610926858285610b37565b610931858585610bcb565b60019150509392505050565b6000806109486108f4565b9050610955818585610bcb565b600191505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036109d25760006040517fe602df050000000000000000000000000000000000000000000000000000000081526004016109c99190611168565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a445760006040517f94280d62000000000000000000000000000000000000000000000000000000008152600401610a3b9190611168565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508015610b31578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92584604051610b2891906110b4565b60405180910390a35b50505050565b6000610b438484610843565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610bc55781811015610bb5578281836040517ffb8f41b2000000000000000000000000000000000000000000000000000000008152600401610bac93929190611365565b60405180910390fd5b610bc484848484036000610960565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610c3d5760006040517f96c6fd1e000000000000000000000000000000000000000000000000000000008152600401610c349190611168565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610caf5760006040517fec442f05000000000000000000000000000000000000000000000000000000008152600401610ca69190611168565b60405180910390fd5b610cba838383610cbf565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610d11578060026000828254610d0591906113cb565b92505081905550610de4565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610d9d578381836040517fe450d38c000000000000000000000000000000000000000000000000000000008152600401610d9493929190611365565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610e2d5780600260008282540392505081905550610e7a565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610ed791906110b4565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610f1e578082015181840152602081019050610f03565b60008484015250505050565b6000601f19601f8301169050919050565b6000610f4682610ee4565b610f508185610eef565b9350610f60818560208601610f00565b610f6981610f2a565b840191505092915050565b60006020820190508181036000830152610f8e8184610f3b565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610fc682610f9b565b9050919050565b610fd681610fbb565b8114610fe157600080fd5b50565b600081359050610ff381610fcd565b92915050565b6000819050919050565b61100c81610ff9565b811461101757600080fd5b50565b60008135905061102981611003565b92915050565b6000806040838503121561104657611045610f96565b5b600061105485828601610fe4565b92505060206110658582860161101a565b9150509250929050565b60008115159050919050565b6110848161106f565b82525050565b600060208201905061109f600083018461107b565b92915050565b6110ae81610ff9565b82525050565b60006020820190506110c960008301846110a5565b92915050565b6000806000606084860312156110e8576110e7610f96565b5b60006110f686828701610fe4565b935050602061110786828701610fe4565b92505060406111188682870161101a565b9150509250925092565b600060ff82169050919050565b61113881611122565b82525050565b6000602082019050611153600083018461112f565b92915050565b61116281610fbb565b82525050565b600060208201905061117d6000830184611159565b92915050565b60006020828403121561119957611198610f96565b5b60006111a784828501610fe4565b91505092915050565b6000819050919050565b60006111d56111d06111cb84610f9b565b6111b0565b610f9b565b9050919050565b60006111e7826111ba565b9050919050565b60006111f9826111dc565b9050919050565b611209816111ee565b82525050565b60006020820190506112246000830184611200565b92915050565b6112338161106f565b811461123e57600080fd5b50565b6000813590506112508161122a565b92915050565b60006020828403121561126c5761126b610f96565b5b600061127a84828501611241565b91505092915050565b6000806040838503121561129a57611299610f96565b5b60006112a885828601610fe4565b92505060206112b985828601610fe4565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061130a57607f821691505b60208210810361131d5761131c6112c3565b5b50919050565b6000815190506113328161122a565b92915050565b60006020828403121561134e5761134d610f96565b5b600061135c84828501611323565b91505092915050565b600060608201905061137a6000830186611159565b61138760208301856110a5565b61139460408301846110a5565b949350505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006113d682610ff9565b91506113e183610ff9565b92508282019050808211156113f9576113f861139c565b5b9291505056fea264697066735822122025faa3de26c129b60192a77ef7674148a6eb24196b975195ff96279c451cc1c764736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/scripts/test-contracts/contracts/SubcallGasExhaustionToken.sol b/scripts/test-contracts/contracts/SubcallGasExhaustionToken.sol new file mode 100644 index 000000000..e10876e68 --- /dev/null +++ b/scripts/test-contracts/contracts/SubcallGasExhaustionToken.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @title GasWaster + * @dev Helper contract that wastes gas in a function call + * This is deployed as a separate contract to force EIP-150's 63/64 gas rule + */ +contract GasWaster { + uint256 public counter; + + /// @notice Wastes approximately gasToWaste amount of gas + /// @param gasToWaste The amount of gas to consume + /// @return success Always returns true if it completes + function wasteGas(uint256 gasToWaste) external returns (bool) { + uint256 gasStart = gasleft(); + + // Waste gas by incrementing a storage variable in a loop + // This will consume gas until we've used approximately gasToWaste + // Use subtraction that won't underflow: check consumed gas vs target + while (gasStart - gasleft() < gasToWaste && gasleft() > 0) { + counter++; + } + + return true; + } +} + +/** + * @title SubcallGasExhaustionToken + * @dev ERC20 token that makes subcalls to waste gas, mimicking AAVE's liquidation behavior + * + * This contract reproduces the scenario where: + * 1. Main contract (this token) is called during a transfer + * 2. It makes a subcall to GasWaster contract + * 3. Due to EIP-150's 63/64 rule, the subcall only gets 63/64 of remaining gas + * 4. If the subcall runs out of gas, it returns false (not a revert) + * 5. This contract detects the failure and explicitly reverts with no message + * 6. This mimics AAVE's liquidationCall behavior + */ +contract SubcallGasExhaustionToken is ERC20 { + GasWaster public gasWaster; + address public routerAddress; + uint256 public gasToWaste; + bool public enableGasWasting; + + event GasWastingEnabled(bool enabled); + event SubcallFailed(uint256 gasLeft); + + constructor( + address _routerAddress, + uint256 _gasToWaste + ) ERC20("SubcallGasToken", "SGT") { + routerAddress = _routerAddress; + gasToWaste = _gasToWaste; + enableGasWasting = true; + + // Deploy GasWaster helper contract during construction + // This creates a separate contract that will be called via external call + gasWaster = new GasWaster(); + + // Mint initial supply to deployer + _mint(msg.sender, 1000000 * 10**decimals()); + } + + /// @notice Override transfer to add conditional subcall gas wasting + /// @dev Only wastes gas when transferring to the router address + function transfer(address to, uint256 amount) public override returns (bool) { + // Only waste gas when transferring to router (mimics AAVE liquidation scenario) + if (enableGasWasting && to == routerAddress) { + // Make external call to waste gas + // This gets 63/64 of remaining gas per EIP-150 + // If the subcall runs out of gas, it returns false instead of reverting + bool success = gasWaster.wasteGas(gasToWaste); + + if (!success) { + // If subcall failed (due to gas exhaustion), revert with no message + // This mimics AAVE's behavior when a subcall fails + emit SubcallFailed(gasleft()); + revert(); + } + } + + return super.transfer(to, amount); + } + + /// @notice Override transferFrom to add conditional subcall gas wasting + /// @dev Only wastes gas when transferring to the router address + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + // Only waste gas when transferring to router + if (enableGasWasting && to == routerAddress) { + // Make external call to waste gas (gets 63/64 of gas per EIP-150) + bool success = gasWaster.wasteGas(gasToWaste); + + if (!success) { + // Revert with no message - mimics AAVE liquidation revert + emit SubcallFailed(gasleft()); + revert(); + } + } + + return super.transferFrom(from, to, amount); + } + + /// @notice Enable or disable gas wasting for testing + /// @dev This allows testing successful transfers after fixing gas issues + function setEnableGasWasting(bool _enable) external { + enableGasWasting = _enable; + emit GasWastingEnabled(_enable); + } + + /// @notice Get the address of the GasWaster helper contract + function getGasWasterAddress() external view returns (address) { + return address(gasWaster); + } +} diff --git a/traits/src/evm.rs b/traits/src/evm.rs index 826e6bcd4..a186f5354 100644 --- a/traits/src/evm.rs +++ b/traits/src/evm.rs @@ -72,6 +72,8 @@ pub struct CallResult { pub exit_reason: ExitReason, pub value: Vec, pub contract: sp_core::H160, + pub gas_used: sp_core::U256, + pub gas_limit: sp_core::U256, } pub trait EVM { From d402730c8fd65509d9bf93bdfe81e490ee5da27a Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 22 Dec 2025 21:47:26 +0100 Subject: [PATCH 03/19] formatting --- runtime/hydradx/src/evm/executor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/hydradx/src/evm/executor.rs b/runtime/hydradx/src/evm/executor.rs index a5af6f22b..e9c9eb47d 100644 --- a/runtime/hydradx/src/evm/executor.rs +++ b/runtime/hydradx/src/evm/executor.rs @@ -120,7 +120,7 @@ where value: info.value, contract: context.contract, gas_used: info.used_gas.effective, - gas_limit: U256::from(gas_limit), + gas_limit: U256::from(gas_limit), } } Err(runner_error) => { @@ -132,7 +132,7 @@ where value: Vec::new(), contract: context.contract, gas_used: U256::zero(), - gas_limit: U256::from(gas_limit), + gas_limit: U256::from(gas_limit), } } } @@ -159,7 +159,7 @@ where value: result.1, contract: context.contract, gas_used: U256::from(gas_used_val), - gas_limit: U256::from(gas_limit), + gas_limit: U256::from(gas_limit), } }); TransactionOutcome::Rollback(Ok::(result)) From 7cddb8bbbea661e2597d69cc746656ecec391aff Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 15:42:29 +0100 Subject: [PATCH 04/19] bump versions --- Cargo.lock | 14 +++++++------- integration-tests/Cargo.toml | 2 +- pallets/dca/Cargo.toml | 2 +- pallets/dispatcher/Cargo.toml | 4 ++-- pallets/hsm/Cargo.toml | 2 +- pallets/liquidation/Cargo.toml | 2 +- runtime/hydradx/Cargo.toml | 3 +-- runtime/hydradx/src/lib.rs | 2 +- traits/Cargo.toml | 2 +- 9 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7675f788..2f764fd4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,7 +5598,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "375.0.0" +version = "376.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", @@ -5764,7 +5764,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "4.4.2" +version = "4.4.3" dependencies = [ "frame-support", "impl-trait-for-tuples", @@ -9248,7 +9248,7 @@ dependencies = [ [[package]] name = "pallet-dca" -version = "1.9.5" +version = "1.9.6" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", @@ -9357,7 +9357,7 @@ dependencies = [ [[package]] name = "pallet-dispatcher" -version = "1.3.0" +version = "1.4.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9838,7 +9838,7 @@ dependencies = [ [[package]] name = "pallet-hsm" -version = "1.5.0" +version = "1.5.1" dependencies = [ "ethabi", "evm", @@ -10027,7 +10027,7 @@ dependencies = [ [[package]] name = "pallet-liquidation" -version = "2.1.0" +version = "2.1.1" dependencies = [ "ethabi", "ethereum", @@ -14765,7 +14765,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "runtime-integration-tests" -version = "1.61.0" +version = "1.62.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index e560fbe43..7f7cee677 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.61.0" +version = "1.62.0" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" diff --git a/pallets/dca/Cargo.toml b/pallets/dca/Cargo.toml index 1b0188137..0603aa460 100644 --- a/pallets/dca/Cargo.toml +++ b/pallets/dca/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-dca' -version = "1.9.5" +version = "1.9.6" description = 'A pallet to manage DCA scheduling' authors = ['GalacticCouncil'] edition = '2021' diff --git a/pallets/dispatcher/Cargo.toml b/pallets/dispatcher/Cargo.toml index f06769b66..4441b5c67 100644 --- a/pallets/dispatcher/Cargo.toml +++ b/pallets/dispatcher/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-dispatcher" -version = "1.3.0" +version = "1.4.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" @@ -13,7 +13,7 @@ readme = "README.md" # parity codec = { workspace = true, features = ["derive", "max-encoded-len"] } scale-info = { workspace = true } -hex = {workspace = true} +hex = { workspace = true } # primitives sp-runtime = { workspace = true } diff --git a/pallets/hsm/Cargo.toml b/pallets/hsm/Cargo.toml index 1a0cae17c..20beb0a5f 100644 --- a/pallets/hsm/Cargo.toml +++ b/pallets/hsm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-hsm" -version = "1.5.0" +version = "1.5.1" edition = "2021" description = "Hollar stability module" authors = ["GalacticCouncil"] diff --git a/pallets/liquidation/Cargo.toml b/pallets/liquidation/Cargo.toml index e68cfe421..d0414a05c 100644 --- a/pallets/liquidation/Cargo.toml +++ b/pallets/liquidation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-liquidation" -version = "2.1.0" +version = "2.1.1" description = "A pallet for money market liquidations" authors = ["GalacticCouncil"] edition = "2021" diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 2056d73b8..2e9ffaf1f 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "375.0.0" +version = "376.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" @@ -475,7 +475,6 @@ try-runtime = [ "pallet-broadcast/try-runtime", "pallet-dispatcher/try-runtime", "pallet-signet/try-runtime", - ] metadata-hash = [ diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 41d8fb744..8b07aa9b2 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -122,7 +122,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 375, + spec_version: 376, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/traits/Cargo.toml b/traits/Cargo.toml index 78e766b59..b4e015ba2 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-traits" -version = "4.4.2" +version = "4.4.3" description = "Shared traits" authors = ["GalacticCouncil"] edition = "2021" From fe45511ddd1d2fdababc450cca1b9969356f8672 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 16:01:50 +0100 Subject: [PATCH 05/19] make clippy happy --- runtime/hydradx/src/assets.rs | 2 ++ runtime/hydradx/src/helpers.rs | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index fd5a5b6c7..4adeff08b 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -1774,6 +1774,7 @@ impl hydradx_traits::evm::EVM for DummyEvm { value: vec![], contract: context.contract, gas_used: U256::zero(), + gas_limit: U256::zero(), } } @@ -1783,6 +1784,7 @@ impl hydradx_traits::evm::EVM for DummyEvm { value: vec![], contract: context.contract, gas_used: U256::zero(), + gas_limit: U256::zero(), } } } diff --git a/runtime/hydradx/src/helpers.rs b/runtime/hydradx/src/helpers.rs index 72b6e4ac4..3f1cfb0f1 100644 --- a/runtime/hydradx/src/helpers.rs +++ b/runtime/hydradx/src/helpers.rs @@ -58,6 +58,7 @@ pub mod benchmark_helpers { exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } @@ -83,6 +84,7 @@ pub mod benchmark_helpers { exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } @@ -120,6 +122,7 @@ pub mod benchmark_helpers { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: vec![], gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } @@ -133,6 +136,7 @@ pub mod benchmark_helpers { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: bytes, gas_used: U256::zero(), + gas_limit: U256::zero(), }; } ERC20Function::GetFacilitatorBucket => { @@ -150,6 +154,7 @@ pub mod benchmark_helpers { exit_reason: ExitReason::Succeed(ExitSucceed::Returned), value: bytes, gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } @@ -161,6 +166,7 @@ pub mod benchmark_helpers { exit_reason: ExitReason::Revert(Reverted), value: vec![], gas_used: U256::zero(), + gas_limit: U256::zero(), }; } @@ -170,6 +176,7 @@ pub mod benchmark_helpers { exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), value: vec![], gas_used: U256::zero(), + gas_limit: U256::zero(), }; } } From 5fb37054dabf76ac349abc8f720d8174c5babbe0 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 07:58:44 +0100 Subject: [PATCH 06/19] remove unnessary initalization as deault value is zero --- pallets/dca/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/pallets/dca/src/lib.rs b/pallets/dca/src/lib.rs index f19b42e51..775b158e5 100644 --- a/pallets/dca/src/lib.rs +++ b/pallets/dca/src/lib.rs @@ -571,7 +571,6 @@ pub mod pallet { ScheduleOwnership::::insert(who.clone(), next_schedule_id, ()); RemainingAmounts::::insert(next_schedule_id, reserve_amount); RetriesOnError::::insert(next_schedule_id, 0); - ScheduleExtraGas::::insert(next_schedule_id, 0); //TODO: we might dont need to set it as 0 is default?! T::Currencies::reserve_named( &T::NamedReserveId::get(), From 7c16e5065132e227da99d7cad520c5411f01b6e6 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 08:00:25 +0100 Subject: [PATCH 07/19] add missing doc --- traits/src/evm.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/traits/src/evm.rs b/traits/src/evm.rs index 52dab120b..3c39138e0 100644 --- a/traits/src/evm.rs +++ b/traits/src/evm.rs @@ -125,9 +125,13 @@ pub trait Erc20OnDust { ) -> frame_support::dispatch::DispatchResult; } +/// Support for providing extra gas to EVM calls. +/// Used when an operation runs out of gas and needs additional gas for retries. pub trait ExtraGasSupport { + /// Set extra gas to be added to subsequent EVM calls fn set_extra_gas(gas: u64); + /// Clear any previously set extra gas fn clear_extra_gas(); - + /// Returns the dispatch error that indicates an out of gas condition fn out_of_gas_error() -> DispatchError; } From cb63874d487b9bd24668c1c51178ce9809c45ade Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 08:10:23 +0100 Subject: [PATCH 08/19] simplify gas comparison --- runtime/hydradx/src/evm/evm_error_decoder.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/runtime/hydradx/src/evm/evm_error_decoder.rs b/runtime/hydradx/src/evm/evm_error_decoder.rs index 59ed2aba3..d23aec5a7 100644 --- a/runtime/hydradx/src/evm/evm_error_decoder.rs +++ b/runtime/hydradx/src/evm/evm_error_decoder.rs @@ -27,13 +27,9 @@ impl Convert for EvmErrorDecoder { // EVM Subcalls exits with Revert with empty data on gas exhaustion, so we check for that case if let ExitReason::Revert(ExitRevert::Reverted) = call_result.exit_reason { if call_result.value.is_empty() { - let threshold = call_result - .gas_limit - .checked_mul(U256::from(90)) - .and_then(|v| v.checked_div(U256::from(100))) - .unwrap_or(U256::zero()); - - if call_result.gas_used >= threshold { + if call_result.gas_used.saturating_mul(U256::from(100)) + >= call_result.gas_limit.saturating_mul(U256::from(90)) + { log::warn!( target: "evm::error_decoder", "Detected subcall gas exhaustion: {:?} gas used out of {:?} limit (contract: {:?})", From 02b70911815f7dafa0c79e57a186893c8737c9ef Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 10:08:16 +0100 Subject: [PATCH 09/19] fix charging correct fee with extra gas and added tests --- integration-tests/src/dca.rs | 116 ++++++++++++++++++++++--- pallets/dca/src/lib.rs | 36 ++++---- pallets/dca/src/tests/mock.rs | 10 ++- pallets/dca/src/tests/on_initialize.rs | 97 +++++++++++++++++---- pallets/dca/src/tests/schedule.rs | 6 +- 5 files changed, 208 insertions(+), 57 deletions(-) diff --git a/integration-tests/src/dca.rs b/integration-tests/src/dca.rs index 0f277f0e5..fe5e0f2cc 100644 --- a/integration-tests/src/dca.rs +++ b/integration-tests/src/dca.rs @@ -2010,17 +2010,20 @@ mod omnipool { init_omnipool_with_oracle_for_block_10(); let amount_to_sell = 1000 * UNITS; - let fee = DCA::get_transaction_fee(&Order::Sell { - asset_in: HDX, - asset_out: DAI, - amount_in: amount_to_sell, - min_amount_out: Balance::MIN, - route: create_bounded_vec(vec![Trade { - pool: PoolType::Omnipool, + let fee = DCA::get_transaction_fee( + &Order::Sell { asset_in: HDX, asset_out: DAI, - }]), - }) + amount_in: amount_to_sell, + min_amount_out: Balance::MIN, + route: create_bounded_vec(vec![Trade { + pool: PoolType::Omnipool, + asset_in: HDX, + asset_out: DAI, + }]), + }, + None, + ) .unwrap(); let alice_init_hdx_balance = 2 * (1000 * UNITS + fee) + 1; @@ -2123,8 +2126,8 @@ mod fee { set_relaychain_block_number(11); //Assert - let fee_for_dot = DCA::get_transaction_fee(&sell_with_hdx_fee).unwrap(); - let fee_for_insufficient = DCA::get_transaction_fee(&sell_with_insufficient_fee).unwrap(); + let fee_for_dot = DCA::get_transaction_fee(&sell_with_hdx_fee, None).unwrap(); + let fee_for_insufficient = DCA::get_transaction_fee(&sell_with_insufficient_fee, None).unwrap(); let diff = fee_for_insufficient - fee_for_dot; let relative_fee_difference = FixedU128::from_rational(diff, fee_for_dot); @@ -2202,8 +2205,8 @@ mod fee { set_relaychain_block_number(11); - let fee_for_dot = DCA::get_transaction_fee(&buy_with_hdx_fee).unwrap(); - let fee_for_insufficient = DCA::get_transaction_fee(&buy_with_insufficient_fee).unwrap(); + let fee_for_dot = DCA::get_transaction_fee(&buy_with_hdx_fee, None).unwrap(); + let fee_for_insufficient = DCA::get_transaction_fee(&buy_with_insufficient_fee, None).unwrap(); let diff = fee_for_insufficient - fee_for_dot; let relative_fee_difference = FixedU128::from_rational(diff, fee_for_dot); @@ -4803,6 +4806,93 @@ mod extra_gas_erc20 { }); } + #[test] + fn dca_retry_includes_extra_gas_in_fee_with_evm_token() { + TestNet::reset(); + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + + let evm_address = EVMAccounts::evm_address(&Router::router_account()); + let contract = deploy_conditional_gas_eater(evm_address, 400_000, crate::erc20::deployer()); + let erc20 = crate::erc20::bind_erc20(contract); + assert_ok!(EmaOracle::add_oracle( + RuntimeOrigin::root(), + OMNIPOOL_SOURCE, + (LRNA, erc20) + )); + + //Add new erc20 to omnipool + let bal = Currencies::free_balance(erc20, &ALICE.into()); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(ALICE.into()), + pallet_omnipool::Pallet::::protocol_account(), + erc20, + bal / 10, + )); + assert_ok!(pallet_omnipool::Pallet::::add_token( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200), + Permill::from_percent(30), + ALICE.into(), + )); + + assert_ok!(MultiTransactionPayment::add_currency( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200) + )); + + hydradx_run_to_block(11); + + let sell_amount = 200000 * UNITS; + let schedule_id = create_schedule_with_onchain_route(erc20, 0, sell_amount, 0, Some(3)); + + // Get the schedule to access the order + let schedule = DCA::schedules(schedule_id).expect("Schedule should exist"); + + let alice_init_balance = Currencies::free_balance(erc20, &ALICE.into()); + + // Run to block 13 - first execution fails with out of gas + hydradx_run_to_block(13); + + assert_eq!(DCA::retries_on_error(schedule_id), 1); + let expected_extra_gas = 333_333; // MAX_EXTRA_GAS / 3 + assert_eq!(DCA::schedule_extra_gas(schedule_id), expected_extra_gas); + + let base_fee = DCA::get_transaction_fee(&schedule.order, None).unwrap(); + let fee_with_extra = DCA::get_transaction_fee(&schedule.order, Some(schedule_id)).unwrap(); + + assert!( + fee_with_extra > base_fee, + "Fee with extra gas ({}) should be > base fee ({})", + fee_with_extra, + base_fee + ); + + // The first attempt charged base fee (before extra gas was added) + let balance_after_first_attempt = Currencies::free_balance(erc20, &ALICE.into()); + let first_fee_charged = alice_init_balance - balance_after_first_attempt; + assert_eq!(first_fee_charged, base_fee); + + let alice_balance_before_retry = Currencies::free_balance(erc20, &ALICE.into()); + + hydradx_run_to_block(33); + + let fee_with_extra = DCA::get_transaction_fee(&schedule.order, Some(schedule_id)).unwrap(); + + // Verify + assert_eq!(DCA::retries_on_error(schedule_id), 0); + assert_trade_executed_succesfully(schedule_id); + assert_eq!(DCA::schedule_extra_gas(schedule_id), expected_extra_gas); + + // The retry should charge fee with extra gas + let balance_after_retry = Currencies::free_balance(erc20, &ALICE.into()); + let total_fees_charged = alice_balance_before_retry - balance_after_retry - sell_amount; + assert_eq!(fee_with_extra, total_fees_charged); + }); + } + #[test] fn extra_gas_keep_incrementing_multiple_times_till_successfull() { TestNet::reset(); diff --git a/pallets/dca/src/lib.rs b/pallets/dca/src/lib.rs index 775b158e5..c7454157a 100644 --- a/pallets/dca/src/lib.rs +++ b/pallets/dca/src/lib.rs @@ -157,7 +157,7 @@ pub mod pallet { continue; }; - let weight_for_single_execution = Self::get_trade_weight_with_extra_gas(schedule_id, &schedule.order); + let weight_for_single_execution = Self::get_trade_weight(&schedule.order, Some(schedule_id)); weight.saturating_accrue(weight_for_single_execution); if let Err(e) = Self::prepare_schedule( @@ -522,7 +522,7 @@ pub mod pallet { Error::::StabilityThresholdTooHigh ); - let transaction_fee = Self::get_transaction_fee(&schedule.order)?; + let transaction_fee = Self::get_transaction_fee(&schedule.order, None)?; let amount_in = match schedule.order { Order::Sell { amount_in, .. } => amount_in, @@ -891,7 +891,7 @@ impl Pallet { let remaining_amount: Balance = RemainingAmounts::::get(schedule_id).defensive_ok_or(Error::::InvalidState)?; - let transaction_fee = Self::get_transaction_fee(&schedule.order)?; + let transaction_fee = Self::get_transaction_fee(&schedule.order, Some(schedule_id))?; let min_amount_for_replanning = transaction_fee.saturating_mul(FEE_MULTIPLIER_FOR_MIN_TRADE_LIMIT); if remaining_amount < min_amount_for_replanning || remaining_amount < T::MinimumTradingLimit::get() { Self::complete_schedule(schedule_id, schedule); @@ -1001,8 +1001,8 @@ impl Pallet { Ok(first_trade.amount_in) } - pub fn get_transaction_fee(order: &Order) -> Result { - Self::convert_weight_to_fee(Self::get_trade_weight(order), order.get_asset_in()) + pub fn get_transaction_fee(order: &Order, schedule_id: Option) -> Result { + Self::convert_weight_to_fee(Self::get_trade_weight(order, schedule_id), order.get_asset_in()) } fn unallocate_amount( @@ -1206,10 +1206,9 @@ impl Pallet { Ok(fee_amount_in_sold_asset) } - // returns DCA overhead weight + router execution weight - fn get_trade_weight(order: &Order) -> Weight { + fn get_trade_weight(order: &Order, schedule_id: Option) -> Weight { let route = &order.get_route_or_default::(); - match order { + let base_weight = match order { Order::Sell { .. } => { let on_initialize_weight = if T::SwappablePaymentAssetSupport::is_transaction_fee_currency(order.get_asset_in()) { @@ -1232,21 +1231,18 @@ impl Pallet { on_initialize_weight .saturating_add(T::AmmTradeWeights::buy_and_calculate_buy_trade_amounts_weight(route)) } - } - } - - /// Calculates trade weight including extra gas for a schedule. - fn get_trade_weight_with_extra_gas(schedule_id: ScheduleId, order: &Order) -> Weight { - let base_weight = Self::get_trade_weight(order); - let extra_gas = ScheduleExtraGas::::get(schedule_id); + }; - if extra_gas == 0 { - return base_weight; + if let Some(id) = schedule_id { + let extra_gas = ScheduleExtraGas::::get(id); + if extra_gas > 0 { + // Convert extra gas to weight without base weight because we already account for that + let extra_weight = T::GasWeightMapping::gas_to_weight(extra_gas, false); + return base_weight.saturating_add(extra_weight); + } } - // Convert extra gas to weight without base weight because we already account for that - let extra_weight = T::GasWeightMapping::gas_to_weight(extra_gas, false); - base_weight.saturating_add(extra_weight) + base_weight } fn convert_native_amount_to_currency( diff --git a/pallets/dca/src/tests/mock.rs b/pallets/dca/src/tests/mock.rs index f6d67eda6..5ce27e6de 100644 --- a/pallets/dca/src/tests/mock.rs +++ b/pallets/dca/src/tests/mock.rs @@ -730,11 +730,13 @@ impl ExtraGasSupport for ExtraGasSetterMock { pub struct MockGasWeightMapping; impl pallet_evm::GasWeightMapping for MockGasWeightMapping { - fn gas_to_weight(_gas: u64, _without_base_weight: bool) -> Weight { - Weight::zero() + fn gas_to_weight(gas: u64, _without_base_weight: bool) -> Weight { + // Convert gas to weight with a simple ratio: 1 gas = 1 weight unit + // This ensures extra gas actually affects the weight calculation in tests + Weight::from_parts(gas, 0) } - fn weight_to_gas(_weight: Weight) -> u64 { - 0 + fn weight_to_gas(weight: Weight) -> u64 { + weight.ref_time() } } diff --git a/pallets/dca/src/tests/on_initialize.rs b/pallets/dca/src/tests/on_initialize.rs index 8af647de8..52e2fe011 100644 --- a/pallets/dca/src/tests/on_initialize.rs +++ b/pallets/dca/src/tests/on_initialize.rs @@ -253,7 +253,7 @@ fn sell_schedule_should_sell_remaining_when_there_is_not_enough_left() { set_to_blocknumber(702); //Assert - let fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_executed_sell_trades!(vec![ SellExecution { asset_in: HDX, @@ -370,7 +370,7 @@ fn one_buy_dca_execution_should_unreserve_exact_amount_in() { set_to_blocknumber(502); //Assert - let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_executed_buy_trades!(vec![BuyExecution { asset_in: HDX, asset_out: BTC, @@ -448,7 +448,7 @@ fn one_buy_dca_execution_should_calculate_exact_amount_in_when_multiple_pools_in } ]); - let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_eq!( total_amount - CALCULATED_AMOUNT_IN_FOR_OMNIPOOL_BUY - buy_fee_in_native, Currencies::reserved_balance(HDX, &ALICE) @@ -650,7 +650,7 @@ fn full_buy_dca_should_be_completed_when_some_successful_dca_execution_happened_ asset_out: BTC, }]), }; - let buy_fee_in_native = DCA::get_transaction_fee(&order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&order, None).unwrap(); let total_amount = 2 * (CALCULATED_AMOUNT_IN_FOR_OMNIPOOL_BUY + buy_fee_in_native) + buy_fee_in_native / 2; let schedule = ScheduleBuilder::new() @@ -970,7 +970,7 @@ fn full_buy_dca_should_be_completed_without_leftover_fees_are_included_in_budget asset_out: BTC, }]), }; - let buy_fee_in_native = DCA::get_transaction_fee(&order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&order, None).unwrap(); let total_amount = 50 * ONE + 5 * buy_fee_in_native; @@ -1284,7 +1284,7 @@ fn dca_trade_unallocation_should_be_rolled_back_when_trade_fails() { assert_number_of_executed_buy_trades!(0); assert_scheduled_ids!(522, vec![schedule_id]); - let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_eq!( Currencies::reserved_balance(HDX, &ALICE), total_amount - buy_fee_in_native @@ -1595,7 +1595,7 @@ fn execution_fee_should_be_taken_from_user_in_sold_currency_in_case_of_successfu set_to_blocknumber(502); //Assert - let buy_fee_in_dai = DCA::get_transaction_fee(&schedule.order).unwrap(); + let buy_fee_in_dai = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_balance!(TreasuryAccount::get(), DAI, buy_fee_in_dai); assert_number_of_executed_buy_trades!(1); assert_eq!( @@ -1645,7 +1645,7 @@ fn execution_fee_should_be_still_taken_from_user_in_sold_currency_in_case_of_fai set_to_blocknumber(502); //Assert - let fee_in_dai = DCA::get_transaction_fee(&schedule.order).unwrap(); + let fee_in_dai = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_balance!(TreasuryAccount::get(), DAI, fee_in_dai); assert_number_of_executed_buy_trades!(0); assert_eq!(Currencies::reserved_balance(DAI, &ALICE), budget - fee_in_dai); @@ -1693,7 +1693,7 @@ fn execution_fee_should_be_taken_from_user_in_sold_currency_in_case_of_successfu set_to_blocknumber(502); //Assert - let sell_fee_in_dai = DCA::get_transaction_fee(&schedule.order).unwrap(); + let sell_fee_in_dai = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_balance!(TreasuryAccount::get(), DAI, sell_fee_in_dai); assert_eq!( Currencies::reserved_balance(DAI, &ALICE), @@ -1744,7 +1744,7 @@ fn sell_dca_native_execution_fee_should_be_taken_and_sent_to_treasury() { set_to_blocknumber(502); //Assert - let sell_fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let sell_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_balance!(TreasuryAccount::get(), HDX, sell_fee_in_native); assert_eq!( Currencies::reserved_balance(HDX, &ALICE), @@ -1835,7 +1835,7 @@ fn buy_dca_native_execution_fee_should_be_taken_and_sent_to_treasury() { set_to_blocknumber(502); //Assert - let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_balance!(TreasuryAccount::get(), HDX, buy_fee_in_native); assert_eq!( @@ -1925,7 +1925,7 @@ fn one_sell_dca_execution_should_be_rescheduled_when_price_diff_is_more_than_max set_to_blocknumber(502); //Assert - let fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_executed_sell_trades!(vec![]); assert_eq!(total_amount - fee_in_native, Currencies::reserved_balance(HDX, &ALICE)); @@ -2051,7 +2051,7 @@ fn one_buy_dca_execution_should_be_rescheduled_when_price_diff_is_more_than_max_ //Act set_to_blocknumber(502); - let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); //Assert assert_executed_buy_trades!(vec![]); assert_eq!( @@ -2105,7 +2105,7 @@ fn specified_slippage_should_be_used_in_circuit_breaker_price_check() { set_to_blocknumber(502); //Assert - let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_executed_buy_trades!(vec![]); assert_eq!( total_amount - buy_fee_in_native, @@ -2309,7 +2309,7 @@ fn dca_sell_schedule_should_be_terminated_when_schedule_allocation_is_more_than_ asset_out: BTC, }]), }; - let fee_in_native = DCA::get_transaction_fee(&order).unwrap(); + let fee_in_native = DCA::get_transaction_fee(&order, None).unwrap(); let total_amount = 2 * (amount_in + fee_in_native); let schedule = ScheduleBuilder::new() .with_period(ONE_HUNDRED_BLOCKS) @@ -2587,7 +2587,7 @@ fn dca_should_continue_when_remainder_is_equal_to_min_trading_limit() { asset_out: BTC, }]), }; - let fee_in_native = DCA::get_transaction_fee(&order).unwrap(); + let fee_in_native = DCA::get_transaction_fee(&order, None).unwrap(); let total_amount = 2 * (ONE + fee_in_native) + min_trade_limit; let schedule = ScheduleBuilder::new() @@ -2699,13 +2699,76 @@ fn dca_schedule_should_still_take_fee_when_order_fails() { )); //Act and assert - let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order).unwrap(); + let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); set_to_blocknumber(502); assert_number_of_executed_buy_trades!(0); assert_balance!(TreasuryAccount::get(), HDX, buy_fee_in_native); }); } +#[test] +fn dca_should_include_extra_gas_in_fee_calculation_on_retry() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10000 * ONE)]) + .build() + .execute_with(|| { + // Arrange + proceed_to_blocknumber(1, 500); + + let total_amount = 1000 * ONE; + let amount_to_sell = *AMOUNT_OUT_FOR_OMNIPOOL_SELL; + + let schedule = ScheduleBuilder::new() + .with_total_amount(total_amount) + .with_period(ONE_HUNDRED_BLOCKS) + .with_max_retries(Some(3)) + .with_order(Order::Sell { + asset_in: HDX, + asset_out: BTC, + amount_in: amount_to_sell, + min_amount_out: Balance::MIN, + route: create_bounded_vec(vec![Trade { + pool: PoolType::Omnipool, + asset_in: HDX, + asset_out: BTC, + }]), + }) + .build(); + + let schedule_id = 0; + assert_ok!(DCA::schedule(RuntimeOrigin::signed(ALICE), schedule.clone(), None)); + + let fee_without_extra_gas = DCA::get_transaction_fee(&schedule.order, None).unwrap(); + + // Simulate that extra gas was incremented after an out-of-gas error + let expected_gas_increment = crate::MAX_EXTRA_GAS / 3; // max_retries = 3 + crate::ScheduleExtraGas::::insert(schedule_id, expected_gas_increment); + + let fee_with_extra_gas = DCA::get_transaction_fee(&schedule.order, Some(schedule_id)).unwrap(); + + assert!( + fee_with_extra_gas > fee_without_extra_gas, + "Fee with extra gas ({}) should be higher than base fee ({})", + fee_with_extra_gas, + fee_without_extra_gas + ); + + // Act + set_to_blocknumber(502); + + //Assert + assert_number_of_executed_sell_trades!(1); + + // Verify the fee charged included the extra gas + let expected_reserved = total_amount - amount_to_sell - fee_with_extra_gas; + assert_eq!( + Currencies::reserved_balance(HDX, &ALICE), + expected_reserved, + "Reserved balance should reflect fee with extra gas was charged" + ); + }); +} + pub fn proceed_to_blocknumber(from: u64, to: u64) { for block_number in RangeInclusive::new(from, to) { System::set_block_number(block_number); diff --git a/pallets/dca/src/tests/schedule.rs b/pallets/dca/src/tests/schedule.rs index daa4c62cd..be3f4519c 100644 --- a/pallets/dca/src/tests/schedule.rs +++ b/pallets/dca/src/tests/schedule.rs @@ -780,7 +780,7 @@ fn sell_schedule_should_work_when_total_amount_is_equal_to_2times_amount_in_plus }]), }; - let fee_in_native = DCA::get_transaction_fee(&order).unwrap(); + let fee_in_native = DCA::get_transaction_fee(&order, None).unwrap(); let total_amount = 2 * (amount_in + fee_in_native); let schedule = ScheduleBuilder::new() .with_total_amount(total_amount) @@ -940,7 +940,7 @@ pub fn get_fee_for_sell_in_hdx() -> Balance { }]), }; - DCA::get_transaction_fee(&order).unwrap() + DCA::get_transaction_fee(&order, None).unwrap() } pub fn get_fee_for_buy_in_hdx() -> Balance { let order = Order::Buy { @@ -955,5 +955,5 @@ pub fn get_fee_for_buy_in_hdx() -> Balance { }]), }; - DCA::get_transaction_fee(&order).unwrap() + DCA::get_transaction_fee(&order, None).unwrap() } From b5c44610486cdc40a1ccf0df93d262b5b150bbc3 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 10:41:11 +0100 Subject: [PATCH 10/19] update lock file --- Cargo.lock | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8922abaa..0c83e7724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9248,7 +9248,7 @@ dependencies = [ [[package]] name = "pallet-dca" -version = "1.9.5" +version = "1.9.6" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", @@ -9265,7 +9265,9 @@ dependencies = [ "pallet-balances", "pallet-broadcast", "pallet-currencies", + "pallet-dispatcher", "pallet-ema-oracle", + "pallet-evm", "pallet-omnipool", "pallet-route-executor", "pallet-xyk", @@ -9355,7 +9357,7 @@ dependencies = [ [[package]] name = "pallet-dispatcher" -version = "1.3.0" +version = "1.4.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9836,7 +9838,7 @@ dependencies = [ [[package]] name = "pallet-hsm" -version = "1.5.0" +version = "1.5.1" dependencies = [ "ethabi", "evm", @@ -10025,7 +10027,7 @@ dependencies = [ [[package]] name = "pallet-liquidation" -version = "2.1.0" +version = "2.1.1" dependencies = [ "ethabi", "ethereum", @@ -14763,7 +14765,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "runtime-integration-tests" -version = "1.61.1" +version = "1.62.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", From 5e66f7fc083b4892cb6b91b53a12b57e0dd4711a Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 12:24:49 +0100 Subject: [PATCH 11/19] update benchmarks --- runtime/hydradx/src/benchmarking/dca.rs | 7 ++ runtime/hydradx/src/weights/pallet_dca.rs | 88 ++++++++++++++--------- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/runtime/hydradx/src/benchmarking/dca.rs b/runtime/hydradx/src/benchmarking/dca.rs index ba7863d27..7b1554fb4 100644 --- a/runtime/hydradx/src/benchmarking/dca.rs +++ b/runtime/hydradx/src/benchmarking/dca.rs @@ -19,6 +19,7 @@ use crate::{ AccountId, AssetId, Balance, BlockNumber, Currencies, MaxSchedulesPerBlock, NamedReserveId, Runtime, DCA, XYK, }; +use pallet_dca::ScheduleExtraGas; use crate::benchmarking::{register_asset, set_period, setup_insufficient_asset_with_dot}; use frame_benchmarking::account; @@ -217,7 +218,9 @@ runtime_benchmarks! { let schedule_2 = schedule_buy_fake(seller.clone(), HDX, DAI, amount_buy); let execution_block = 1005u32; + let schedule_id = DCA::next_schedule_id() + 1; assert_ok!(DCA::schedule(RawOrigin::Signed(seller.clone()).into(), schedule1.clone(), Option::Some(execution_block))); + ScheduleExtraGas::::insert(schedule_id, 1_000_000);//We add some extra gas to make sure we cover the worst case assert_eq!(Currencies::free_balance(DAI, &seller),0); let reserved_balance = get_named_reseve_balance(asset_in, seller.clone()); @@ -304,7 +307,9 @@ runtime_benchmarks! { let schedule1 = schedule_sell_fake(seller.clone(), HDX, DAI, amount_sell); let execution_block = 1005u32; + let schedule_id = DCA::next_schedule_id() + 1; assert_ok!(DCA::schedule(RawOrigin::Signed(seller.clone()).into(), schedule1.clone(), Option::Some(execution_block))); + ScheduleExtraGas::::insert(schedule_id, 1_000_000); assert_eq!(Currencies::free_balance(DAI, &seller),0); let reserved_balance = get_named_reseve_balance(HDX, seller.clone()); @@ -519,6 +524,8 @@ runtime_benchmarks! { set_period(99); let execution_block = 100u32; assert_ok!(DCA::schedule(RawOrigin::Signed(caller).into(), schedule1, Option::Some(execution_block))); + ScheduleExtraGas::::insert(schedule_id, 1_000_000); + }: _(RawOrigin::Root, schedule_id, None) verify { diff --git a/runtime/hydradx/src/weights/pallet_dca.rs b/runtime/hydradx/src/weights/pallet_dca.rs index 90f3debae..699077161 100644 --- a/runtime/hydradx/src/weights/pallet_dca.rs +++ b/runtime/hydradx/src/weights/pallet_dca.rs @@ -19,13 +19,13 @@ //! Autogenerated weights for `pallet_dca` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 43.0.0 -//! DATE: 2025-07-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-01-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `bench-bot`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! HOSTNAME: `Mac.chello.hu`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: -// ./bin/hydradx +// ./target/release/hydradx // benchmark // pallet // --wasm-execution=compiled @@ -64,6 +64,8 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::ScheduleIdsPerBlock` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) /// Storage: `DCA::Schedules` (r:1 w:0) /// Proof: `DCA::Schedules` (`max_values`: None, `max_size`: Some(243), added: 2718, mode: `MaxEncodedLen`) + /// Storage: `DCA::ScheduleExtraGas` (r:1 w:0) + /// Proof: `DCA::ScheduleExtraGas` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `DCA::RemainingAmounts` (r:1 w:1) /// Proof: `DCA::RemainingAmounts` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) /// Storage: `Balances::Reserves` (r:1 w:1) @@ -78,11 +80,11 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::ScheduleExecutionBlock` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn on_initialize_with_buy_trade() -> Weight { // Proof Size summary in bytes: - // Measured: `19824` + // Measured: `19910` // Estimated: `31230` - // Minimum execution time: 232_736_000 picoseconds. - Weight::from_parts(235_309_000, 31230) - .saturating_add(T::DbWeight::get().reads(18_u64)) + // Minimum execution time: 176_000_000 picoseconds. + Weight::from_parts(191_000_000, 31230) + .saturating_add(T::DbWeight::get().reads(19_u64)) .saturating_add(T::DbWeight::get().writes(9_u64)) } /// Storage: `DCA::ScheduleIdsPerBlock` (r:12 w:2) @@ -91,6 +93,8 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::Schedules` (`max_values`: None, `max_size`: Some(243), added: 2718, mode: `MaxEncodedLen`) /// Storage: `MultiTransactionPayment::AcceptedCurrencies` (r:2 w:0) /// Proof: `MultiTransactionPayment::AcceptedCurrencies` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `DCA::ScheduleExtraGas` (r:1 w:0) + /// Proof: `DCA::ScheduleExtraGas` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::NextAssetId` (r:1 w:0) /// Proof: `AssetRegistry::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::LocationAssets` (r:1 w:0) @@ -107,8 +111,12 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::RemainingAmounts` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) /// Storage: `Tokens::Reserves` (r:1 w:1) /// Proof: `Tokens::Reserves` (`max_values`: None, `max_size`: Some(1261), added: 3736, mode: `MaxEncodedLen`) - /// Storage: `Duster::AccountBlacklist` (r:2 w:0) - /// Proof: `Duster::AccountBlacklist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `EVMAccounts::AccountExtension` (r:1 w:0) + /// Proof: `EVMAccounts::AccountExtension` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `HSM::FlashMinter` (r:1 w:0) + /// Proof: `HSM::FlashMinter` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Duster::AccountWhitelist` (r:2 w:0) + /// Proof: `Duster::AccountWhitelist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::BannedAssets` (r:2 w:0) /// Proof: `AssetRegistry::BannedAssets` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:1) @@ -127,17 +135,19 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::ScheduleExecutionBlock` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn on_initialize_with_buy_trade_with_insufficient_fee_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `25932` + // Measured: `26447` // Estimated: `31230` - // Minimum execution time: 436_702_000 picoseconds. - Weight::from_parts(438_785_000, 31230) - .saturating_add(T::DbWeight::get().reads(38_u64)) + // Minimum execution time: 323_000_000 picoseconds. + Weight::from_parts(343_000_000, 31230) + .saturating_add(T::DbWeight::get().reads(41_u64)) .saturating_add(T::DbWeight::get().writes(12_u64)) } /// Storage: `DCA::ScheduleIdsPerBlock` (r:12 w:2) /// Proof: `DCA::ScheduleIdsPerBlock` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) /// Storage: `DCA::Schedules` (r:1 w:0) /// Proof: `DCA::Schedules` (`max_values`: None, `max_size`: Some(243), added: 2718, mode: `MaxEncodedLen`) + /// Storage: `DCA::ScheduleExtraGas` (r:1 w:0) + /// Proof: `DCA::ScheduleExtraGas` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `DCA::RemainingAmounts` (r:1 w:1) /// Proof: `DCA::RemainingAmounts` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) /// Storage: `Balances::Reserves` (r:1 w:1) @@ -152,11 +162,11 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::ScheduleExecutionBlock` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn on_initialize_with_sell_trade() -> Weight { // Proof Size summary in bytes: - // Measured: `19572` + // Measured: `19658` // Estimated: `31230` - // Minimum execution time: 235_385_000 picoseconds. - Weight::from_parts(236_914_000, 31230) - .saturating_add(T::DbWeight::get().reads(18_u64)) + // Minimum execution time: 175_000_000 picoseconds. + Weight::from_parts(181_000_000, 31230) + .saturating_add(T::DbWeight::get().reads(19_u64)) .saturating_add(T::DbWeight::get().writes(9_u64)) } /// Storage: `DCA::ScheduleIdsPerBlock` (r:12 w:2) @@ -165,6 +175,8 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::Schedules` (`max_values`: None, `max_size`: Some(243), added: 2718, mode: `MaxEncodedLen`) /// Storage: `MultiTransactionPayment::AcceptedCurrencies` (r:2 w:0) /// Proof: `MultiTransactionPayment::AcceptedCurrencies` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `DCA::ScheduleExtraGas` (r:1 w:0) + /// Proof: `DCA::ScheduleExtraGas` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::NextAssetId` (r:1 w:0) /// Proof: `AssetRegistry::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::LocationAssets` (r:1 w:0) @@ -181,8 +193,12 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::RemainingAmounts` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) /// Storage: `Tokens::Reserves` (r:1 w:1) /// Proof: `Tokens::Reserves` (`max_values`: None, `max_size`: Some(1261), added: 3736, mode: `MaxEncodedLen`) - /// Storage: `Duster::AccountBlacklist` (r:2 w:0) - /// Proof: `Duster::AccountBlacklist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `EVMAccounts::AccountExtension` (r:1 w:0) + /// Proof: `EVMAccounts::AccountExtension` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `HSM::FlashMinter` (r:1 w:0) + /// Proof: `HSM::FlashMinter` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Duster::AccountWhitelist` (r:2 w:0) + /// Proof: `Duster::AccountWhitelist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::BannedAssets` (r:2 w:0) /// Proof: `AssetRegistry::BannedAssets` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:1) @@ -201,11 +217,11 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::ScheduleExecutionBlock` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn on_initialize_with_sell_trade_with_insufficient_fee_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `26192` + // Measured: `26707` // Estimated: `31230` - // Minimum execution time: 437_739_000 picoseconds. - Weight::from_parts(439_902_000, 31230) - .saturating_add(T::DbWeight::get().reads(38_u64)) + // Minimum execution time: 320_000_000 picoseconds. + Weight::from_parts(332_000_000, 31230) + .saturating_add(T::DbWeight::get().reads(41_u64)) .saturating_add(T::DbWeight::get().writes(12_u64)) } /// Storage: `DCA::ScheduleIdsPerBlock` (r:1 w:0) @@ -214,8 +230,8 @@ impl pallet_dca::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `1113` // Estimated: `3510` - // Minimum execution time: 19_845_000 picoseconds. - Weight::from_parts(20_382_000, 3510) + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(14_000_000, 3510) .saturating_add(T::DbWeight::get().reads(1_u64)) } /// Storage: `MultiTransactionPayment::AcceptedCurrencies` (r:2 w:0) @@ -250,10 +266,10 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::RemainingAmounts` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) fn schedule() -> Weight { // Proof Size summary in bytes: - // Measured: `19239` + // Measured: `19305` // Estimated: `28710` - // Minimum execution time: 236_323_000 picoseconds. - Weight::from_parts(237_792_000, 28710) + // Minimum execution time: 168_000_000 picoseconds. + Weight::from_parts(173_000_000, 28710) .saturating_add(T::DbWeight::get().reads(24_u64)) .saturating_add(T::DbWeight::get().writes(9_u64)) } @@ -271,16 +287,18 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `DCA::ScheduleIdsPerBlock` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) /// Storage: `DCA::RetriesOnError` (r:0 w:1) /// Proof: `DCA::RetriesOnError` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `DCA::ScheduleExtraGas` (r:0 w:1) + /// Proof: `DCA::ScheduleExtraGas` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `DCA::ScheduleOwnership` (r:0 w:1) /// Proof: `DCA::ScheduleOwnership` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) fn terminate() -> Weight { // Proof Size summary in bytes: - // Measured: `2609` + // Measured: `2642` // Estimated: `4714` - // Minimum execution time: 93_408_000 picoseconds. - Weight::from_parts(94_751_000, 4714) + // Minimum execution time: 63_000_000 picoseconds. + Weight::from_parts(67_000_000, 4714) .saturating_add(T::DbWeight::get().reads(6_u64)) - .saturating_add(T::DbWeight::get().writes(8_u64)) + .saturating_add(T::DbWeight::get().writes(9_u64)) } /// Storage: `DCA::ScheduleOwnership` (r:1 w:0) /// Proof: `DCA::ScheduleOwnership` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) @@ -290,10 +308,10 @@ impl pallet_dca::WeightInfo for HydraWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn unlock_reserves() -> Weight { // Proof Size summary in bytes: - // Measured: `1959` + // Measured: `2144` // Estimated: `4714` - // Minimum execution time: 61_168_000 picoseconds. - Weight::from_parts(61_872_000, 4714) + // Minimum execution time: 39_000_000 picoseconds. + Weight::from_parts(40_000_000, 4714) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } From 72b329d282906b88242e9e0a2afea07960e173db Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 12:25:00 +0100 Subject: [PATCH 12/19] formatting --- pallets/dca/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pallets/dca/src/lib.rs b/pallets/dca/src/lib.rs index c7454157a..0d4962715 100644 --- a/pallets/dca/src/lib.rs +++ b/pallets/dca/src/lib.rs @@ -1001,7 +1001,10 @@ impl Pallet { Ok(first_trade.amount_in) } - pub fn get_transaction_fee(order: &Order, schedule_id: Option) -> Result { + pub fn get_transaction_fee( + order: &Order, + schedule_id: Option, + ) -> Result { Self::convert_weight_to_fee(Self::get_trade_weight(order, schedule_id), order.get_asset_in()) } From 42dbc013bd7df04cb8d3f1d3a2463f5d9881a228 Mon Sep 17 00:00:00 2001 From: dmoka Date: Tue, 6 Jan 2026 08:21:40 +0100 Subject: [PATCH 13/19] bump runtime version --- runtime/hydradx/Cargo.toml | 2 +- runtime/hydradx/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 2e9ffaf1f..d950ab61b 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "376.0.0" +version = "377.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 8b07aa9b2..2a6c63af3 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -122,7 +122,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 376, + spec_version: 377, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 1d81410a320d56cc451a7d8683c57ddfd9e2cd2d Mon Sep 17 00:00:00 2001 From: dmoka Date: Tue, 6 Jan 2026 08:21:49 +0100 Subject: [PATCH 14/19] update lock file --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0c83e7724..e9ba883b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,7 +5598,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "376.0.0" +version = "377.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", From 68f408dc1ad9581cf60c03cd3ee6171431b27aa6 Mon Sep 17 00:00:00 2001 From: dmoka Date: Tue, 6 Jan 2026 11:06:08 +0100 Subject: [PATCH 15/19] using extra gas also when we take the tx fee, so we prevent such out of gas errors too --- integration-tests/src/dca.rs | 74 ++++++++++++++++++++++++++++++++++++ pallets/dca/src/lib.rs | 28 ++++++++++++-- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/integration-tests/src/dca.rs b/integration-tests/src/dca.rs index fe5e0f2cc..0d184ac7a 100644 --- a/integration-tests/src/dca.rs +++ b/integration-tests/src/dca.rs @@ -5182,6 +5182,80 @@ mod extra_gas_erc20 { }); } + #[test] + fn dca_retries_when_fee_payment_fails_with_out_of_gas() { + TestNet::reset(); + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + + // Deploy ConditionalGasEater with FeeReceiver address (not router) + // This makes fee transfers trigger gas eating + let fee_receiver = ::FeeReceiver::get(); + let fee_receiver_evm = EVMAccounts::evm_address(&fee_receiver); + let contract = deploy_conditional_gas_eater(fee_receiver_evm, 400_000, crate::erc20::deployer()); + let erc20 = crate::erc20::bind_erc20(contract); + + assert_ok!(EmaOracle::add_oracle( + RuntimeOrigin::root(), + OMNIPOOL_SOURCE, + (LRNA, erc20) + )); + + // Add new erc20 to omnipool + let bal = Currencies::free_balance(erc20, &ALICE.into()); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(ALICE.into()), + pallet_omnipool::Pallet::::protocol_account(), + erc20, + bal / 10, + )); + assert_ok!(pallet_omnipool::Pallet::::add_token( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200), + Permill::from_percent(30), + ALICE.into(), + )); + + // Add as transaction fee currency (fees go through transfer path) + assert_ok!(MultiTransactionPayment::add_currency( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200) + )); + + hydradx_run_to_block(11); + + let schedule_id = create_schedule_with_onchain_route(erc20, HDX, 200000 * UNITS, 0, Some(3)); + + let alice_init_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + hydradx_run_to_block(13); + + // Assert: fee payment failed with EvmOutOfGas, extra gas increased, schedule retried + assert_eq!(DCA::retries_on_error(schedule_id), 1); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + trade_failed_with_evm_out_of_gas_error(schedule_id); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(0, count_trade_executed_events()); + + // Balance unchanged because fee payment failed before trade + let alice_hdx_balance = Currencies::free_balance(HDX, &ALICE.into()); + assert_eq!(alice_init_hdx_balance, alice_hdx_balance); + + // Retry with extra gas - should succeed + hydradx_run_to_block(33); + + // Assert: trade finally succeeded + assert_eq!(DCA::retries_on_error(schedule_id), 0); + assert_eq!(DCA::schedule_extra_gas(schedule_id), 333_333); + assert_trade_executed_succesfully(schedule_id); + assert_eq!(1, count_failed_trade_events()); + assert_eq!(1, count_trade_executed_events()); + let alice_hdx_balance_after_retry = Currencies::free_balance(HDX, &ALICE.into()); + assert!(alice_hdx_balance_after_retry > alice_hdx_balance); + }); + } + fn create_schedule_with_onchain_route( asset_in: AssetId, asset_out: AssetId, diff --git a/pallets/dca/src/lib.rs b/pallets/dca/src/lib.rs index 0d4962715..6e212623e 100644 --- a/pallets/dca/src/lib.rs +++ b/pallets/dca/src/lib.rs @@ -167,7 +167,10 @@ pub mod pallet { &schedule, &mut randomness_generator, ) { - if e == Error::::PriceUnstable.into() || e == Error::::Bumped.into() { + if e == Error::::PriceUnstable.into() + || e == Error::::Bumped.into() + || e == T::ExtraGasSupport::out_of_gas_error() + { continue; } else { Self::terminate_schedule(schedule_id, &schedule, e); @@ -759,7 +762,19 @@ impl Pallet { return Err(Error::::Bumped.into()); } - Self::take_transaction_fee_from_user(schedule_id, schedule, weight_for_dca_execution)?; + if let Err(e) = Self::take_transaction_fee_from_user(schedule_id, schedule, weight_for_dca_execution) { + if e == T::ExtraGasSupport::out_of_gas_error() { + Self::increment_extra_gas(schedule_id, schedule); + Self::deposit_event(Event::TradeFailed { + id: schedule_id, + who: schedule.owner.clone(), + error: e, + }); + Self::retry_schedule(schedule_id, schedule, current_blocknumber, randomness_generator)?; + return Err(e); + } + return Err(e); + } if Self::is_price_unstable(schedule) { Self::deposit_event(Event::TradeFailed { @@ -780,7 +795,7 @@ impl Pallet { schedule_id: ScheduleId, schedule: &Schedule>, ) -> Result, DispatchError> { - // Set extra gas for dispatcher to use during EVM calls + // Set extra gas for evm execution when previous dca failed with out of gas let extra_gas = ScheduleExtraGas::::get(schedule_id); T::ExtraGasSupport::set_extra_gas(extra_gas); @@ -867,7 +882,6 @@ impl Pallet { pallet_broadcast::Pallet::::remove_from_context()?; - // Clean up extra gas T::ExtraGasSupport::clear_extra_gas(); trade_result @@ -1050,6 +1064,10 @@ impl Pallet { schedule: &Schedule>, weight_to_charge: Weight, ) -> DispatchResult { + // Set extra gas for evm execution when previous dca failed with out of gas + let extra_gas = ScheduleExtraGas::::get(schedule_id); + T::ExtraGasSupport::set_extra_gas(extra_gas); + let fee_currency = schedule.order.get_asset_in(); let fee_amount_in_sold_asset = Self::convert_weight_to_fee(weight_to_charge, fee_currency)?; @@ -1084,6 +1102,8 @@ impl Pallet { )?; } + T::ExtraGasSupport::clear_extra_gas(); + Ok(()) } From 7a05912e09462e240fed6cd97ac2834908ecca78 Mon Sep 17 00:00:00 2001 From: dmoka Date: Tue, 6 Jan 2026 11:08:51 +0100 Subject: [PATCH 16/19] bump versions --- Cargo.lock | 4 ++-- runtime/hydradx/Cargo.toml | 2 +- runtime/hydradx/src/lib.rs | 2 +- traits/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9f6936ab..b02fafdaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,7 +5598,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "377.0.0" +version = "378.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", @@ -5764,7 +5764,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "4.4.3" +version = "4.5.0" dependencies = [ "frame-support", "impl-trait-for-tuples", diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index d950ab61b..45a8b4245 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "377.0.0" +version = "378.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 2a6c63af3..8b1a8f8ad 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -122,7 +122,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 377, + spec_version: 378, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/traits/Cargo.toml b/traits/Cargo.toml index 2749659a9..eba73c31e 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-traits" -version = "4.4.3" +version = "4.5.0" description = "Shared traits" authors = ["GalacticCouncil"] edition = "2021" From 39d33afad840b359fa10b94d72eb2ae902b97f3d Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 14:43:38 +0100 Subject: [PATCH 17/19] bump minor version --- Cargo.lock | 2 +- pallets/dca/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6944b841..d8822e2cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9248,7 +9248,7 @@ dependencies = [ [[package]] name = "pallet-dca" -version = "1.9.6" +version = "1.10.0" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", diff --git a/pallets/dca/Cargo.toml b/pallets/dca/Cargo.toml index 0603aa460..3fbe19232 100644 --- a/pallets/dca/Cargo.toml +++ b/pallets/dca/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-dca' -version = "1.9.6" +version = "1.10.0" description = 'A pallet to manage DCA scheduling' authors = ['GalacticCouncil'] edition = '2021' From b6c8ec865615be8cc6122e167a3667434a221a4d Mon Sep 17 00:00:00 2001 From: dmoka Date: Fri, 9 Jan 2026 13:09:58 +0100 Subject: [PATCH 18/19] update lock --- Cargo.lock | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 093262bc3..7d87eea8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5764,7 +5764,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "4.4.4" +version = "4.5.0" dependencies = [ "frame-support", "impl-trait-for-tuples", @@ -9248,7 +9248,7 @@ dependencies = [ [[package]] name = "pallet-dca" -version = "1.9.5" +version = "1.10.0" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", @@ -9265,7 +9265,9 @@ dependencies = [ "pallet-balances", "pallet-broadcast", "pallet-currencies", + "pallet-dispatcher", "pallet-ema-oracle", + "pallet-evm", "pallet-omnipool", "pallet-route-executor", "pallet-xyk", @@ -9355,7 +9357,7 @@ dependencies = [ [[package]] name = "pallet-dispatcher" -version = "1.3.0" +version = "1.4.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9836,7 +9838,7 @@ dependencies = [ [[package]] name = "pallet-hsm" -version = "1.5.0" +version = "1.5.1" dependencies = [ "ethabi", "evm", @@ -10025,7 +10027,7 @@ dependencies = [ [[package]] name = "pallet-liquidation" -version = "2.1.0" +version = "2.1.1" dependencies = [ "ethabi", "ethereum", @@ -14763,7 +14765,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "runtime-integration-tests" -version = "1.61.2" +version = "1.62.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", From aa5a4c328c050fa5ebcb157e4c9817e8a6b80e71 Mon Sep 17 00:00:00 2001 From: dmoka Date: Fri, 9 Jan 2026 13:11:45 +0100 Subject: [PATCH 19/19] bump runtime --- Cargo.lock | 2 +- runtime/hydradx/Cargo.toml | 2 +- runtime/hydradx/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d87eea8b..7b0c69fc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,7 +5598,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "379.0.0" +version = "380.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index a714dfce1..a152b443f 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "379.0.0" +version = "380.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 36ce94034..744aa0d28 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -122,7 +122,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 379, + spec_version: 380, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1,