From 668fb4d1bce619ab128d163b6c45a8806aa576bf Mon Sep 17 00:00:00 2001 From: pgherveou Date: Wed, 28 Jan 2026 14:33:48 +0100 Subject: [PATCH 1/2] revive: cap remaining_gas to u64::MAX in substrate_execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When calculating resource limits for nested calls through substrate_execution::new_nested_meter, the ratio-based scaling fails when deposit_left is very large (e.g., u128::MAX default). The ratio becomes near-zero because: - remaining_gas = weight_gas + deposit_gas (huge number from deposit) - ratio = requested_gas / remaining_gas ≈ 0 - nested_weight_limit = ratio × weight_left ≈ 0 This causes proxy contracts using delegatecall to fail with OutOfGas when called through ReviveApi.call, while the same calls succeed through eth_transact. Cap remaining_gas to u64::MAX since Ethereum gas is a u64 value. This ensures the ratio is 1.0 when requesting all gas, giving the nested call the full remaining weight. --- substrate/frame/revive/src/metering/math.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/substrate/frame/revive/src/metering/math.rs b/substrate/frame/revive/src/metering/math.rs index 54f1df1922c83..b8a46e235427e 100644 --- a/substrate/frame/revive/src/metering/math.rs +++ b/substrate/frame/revive/src/metering/math.rs @@ -120,6 +120,12 @@ pub mod substrate_execution { return Err(>::OutOfGas.into()); }; + // Cap remaining_gas to u64::MAX since Ethereum gas is a u64 value. + // Without this cap, when deposit_left is very large (e.g., u128::MAX), + // the ratio calculation would produce a near-zero value, causing the + // nested call to receive almost no weight even when requesting all gas. + let remaining_gas = remaining_gas.min(u64::MAX.saturated_into()); + let gas_limit = remaining_gas.min(*gas); let ratio = if remaining_gas.is_zero() { From c604fd84f5a2bf9796e53874e2d1c6eb86ed1aa6 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Wed, 28 Jan 2026 15:14:47 +0100 Subject: [PATCH 2/2] revive: add tests for substrate_execution nested metering Add regression tests for the gas limit cap fix in substrate_execution. These tests validate that nested calls requesting all gas receive meaningful weight allocation when using large deposit limits. Note: The tests use Balance = u64, so the bug doesn't fully manifest. The fix is critical for u128 production configs where deposit_left can exceed u64::MAX. --- substrate/frame/revive/src/metering/math.rs | 6 +- substrate/frame/revive/src/metering/tests.rs | 81 ++++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/substrate/frame/revive/src/metering/math.rs b/substrate/frame/revive/src/metering/math.rs index b8a46e235427e..dbec63d0622ac 100644 --- a/substrate/frame/revive/src/metering/math.rs +++ b/substrate/frame/revive/src/metering/math.rs @@ -120,10 +120,8 @@ pub mod substrate_execution { return Err(>::OutOfGas.into()); }; - // Cap remaining_gas to u64::MAX since Ethereum gas is a u64 value. - // Without this cap, when deposit_left is very large (e.g., u128::MAX), - // the ratio calculation would produce a near-zero value, causing the - // nested call to receive almost no weight even when requesting all gas. + // Cap to u64::MAX since Ethereum gas is u64. Without this, large deposit_left + // (e.g., u128::MAX) causes ratio ≈ 0, giving nested calls almost no weight. let remaining_gas = remaining_gas.min(u64::MAX.saturated_into()); let gas_limit = remaining_gas.min(*gas); diff --git a/substrate/frame/revive/src/metering/tests.rs b/substrate/frame/revive/src/metering/tests.rs index 23614230b6256..81cc1d87df660 100644 --- a/substrate/frame/revive/src/metering/tests.rs +++ b/substrate/frame/revive/src/metering/tests.rs @@ -753,3 +753,84 @@ fn catch_constructor_test() { assert_eq!("revert: invalid address", gas_trace.calls[0].revert_reason.as_ref().unwrap()); }); } + +/// Regression test for proxy contract delegatecall with large deposit limits. +/// +/// When deposit_left is very large (u128::MAX in production), remaining_gas becomes huge, +/// causing ratio = gas_limit / remaining_gas ≈ 0. This resulted in nested calls receiving +/// almost no weight. The fix caps remaining_gas to u64::MAX since Ethereum gas is u64. +/// +/// Note: This test uses Balance = u64, so the bug doesn't fully manifest here. +/// The fix is a no-op in u64 configs but critical for u128 production configs. +#[test] +fn substrate_nesting_with_large_deposit_and_max_gas_request() { + use super::math::substrate_execution; + + ExtBuilder::default() + .with_next_fee_multiplier(FixedU128::from_rational(1, 5)) + .build() + .execute_with(|| { + let weight_limit = Weight::from_parts(1_000_000_000, 10_000); + let deposit_limit = u64::MAX; + + let mut root_meter = + substrate_execution::new_root::(weight_limit, deposit_limit).unwrap(); + + root_meter.charge_weight_token(TestToken(1000, 100)).unwrap(); + root_meter.charge_deposit(&StorageDeposit::Charge(1000)).unwrap(); + + let weight_left_before = root_meter.weight_left().unwrap(); + + let gas_scale: u64 = ::GasScale::get().into(); + let max_eth_gas = u64::MAX / gas_scale; + + let nested = root_meter + .new_nested(&CallResources::Ethereum { gas: max_eth_gas, add_stipend: false }) + .unwrap(); + + let nested_weight_left = nested.weight_left().unwrap(); + + assert!( + nested_weight_left.ref_time() >= weight_left_before.ref_time() / 2, + "Nested meter should get at least 50% of remaining weight. \ + Got ref_time: {}, expected at least: {}", + nested_weight_left.ref_time(), + weight_left_before.ref_time() / 2 + ); + + assert!(nested.deposit_left().unwrap() > 0); + }); +} + +/// Test ratio-based weight scaling for partial gas requests in substrate execution. +#[test] +fn substrate_nesting_with_partial_gas_request_scales_weight() { + use super::math::substrate_execution; + + ExtBuilder::default() + .with_next_fee_multiplier(FixedU128::from_rational(1, 5)) + .build() + .execute_with(|| { + let weight_limit = Weight::from_parts(1_000_000_000, 10_000); + let deposit_limit = 1_000_000_000u64; + + let mut root_meter = + substrate_execution::new_root::(weight_limit, deposit_limit).unwrap(); + + root_meter.charge_weight_token(TestToken(1000, 100)).unwrap(); + + let weight_left_before = root_meter.weight_left().unwrap(); + + let gas_scale: u64 = ::GasScale::get().into(); + let partial_gas = (u64::MAX / gas_scale) / 10; + + let nested = root_meter + .new_nested(&CallResources::Ethereum { gas: partial_gas, add_stipend: false }) + .unwrap(); + + let nested_weight_left = nested.weight_left().unwrap(); + + assert!(nested_weight_left.ref_time() > 0); + assert!(nested_weight_left.ref_time() <= weight_left_before.ref_time()); + }); +}