Skip to content

Conversation

@filip-parity
Copy link

TL;DR

This pull request implements the vm.fee cheatcode for polkadot-foundry's PVM mode, following the same pattern as existing cheatcodes like vm.roll and vm.warp.

Changes

  • Added vm.fee handler in revive-strategy to update block.basefee in EVM context
  • Added comprehensive test to verify vm.fee functionality in PVM mode

Closes #318

});
}

fn set_basefee(new_basefee: U256, ecx: Ecx<'_, '_, '_>) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, so i assume it's not possible to modify the value of Weights::seal_base_fee on pallet-revive?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and do we have anyone who's up to date on gas mapping?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really

@filip-parity filip-parity requested a review from pkhry September 30, 2025 06:48
import "./Vm.sol";
import {console} from "./console.sol";
contract Fee is DSTest {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try calling block.basefee from a contract that's deployed on pvm side

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some more tests

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgetest!(fee_revive, |prj, cmd| {
    prj.insert_ds_test();
    prj.insert_vm();
    prj.insert_console();
    prj.add_source("C.sol",
    r#"
    contract C {
      function example() public returns (uint256) {
        return block.basefee;
      }
    }
    "#
    ).unwrap();
    prj.add_source(
        "Fee.t.sol",
        r#"
import "./test.sol";
import "./Vm.sol";
import "./C.sol";
import {console} from "./console.sol";

contract Fee is DSTest {
  Vm constant vm = Vm(HEVM_ADDRESS);

  function test_Fee() public {
      C target = new C();
      uint256 original = target.example();
      vm.fee(25 gwei);
      uint256 newValue = target.example();
      assertEq(newValue, 25 gwei);
      assert(original != newValue);
  }
}
"#,
    )
    .unwrap();

    let res = cmd.args(["test", "--resolc", "--resolc-startup", "-vvv"]).assert_success();
    res.stderr_eq(str![""]).stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!
[COMPILING_FILES] with [RESOLC_VERSION]
[RESOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for src/Fee.t.sol:Fee
[PASS] test_Fee() ([GAS])
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)

"#]]);
});

result:

Traces:
  [83471631] Fee::test_Fee()
    ├─ [16788608] → new <unknown>@0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC
    │   └─ ← [Return] 2366 bytes of code
    ├─ [33317875] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::example()
    │   └─ ← [Return] 1000000000 [1e9]
    ├─ [0] VM::fee(25000000000 [2.5e10])
    │   └─ ← [Return]
    ├─ [33317875] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::example()
    │   └─ ← [Return] 1000000000 [1e9]
    ├─ emit log(val: "Error: a == b not satisfied [uint]")
    ├─ emit log_named_uint(key: "  Expected", val: 25000000000 [2.5e10])
    ├─ emit log_named_uint(key: "    Actual", val: 1000000000 [1e9])

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the pallet-revive's basefee opcode implementation doesn't read from the effective_gas_price, but has a hardcoded default value of 1 gwei.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you fill out an issue on foundry-polkadot to implement the functionality for pallet-revive/foundry-polkadot

Copy link
Author

@filip-parity filip-parity Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the ticket with a comment and created this issue related to the default value of BASEFEE opcode


fn set_basefee(new_basefee: U256, ecx: Ecx<'_, '_, '_>) {
// Set basefee in EVM context.
ecx.block.basefee = new_basefee.try_into().expect("Basefee exceeds u64");
Copy link
Collaborator

@smiasojed smiasojed Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you check how zksync is handling it?
I think they keep it consistent between vm calls.

  1. Single Source of Truth

// vm.fee() cheatcode sets this:
ccx.ecx.block.basefee =
newBasefee.saturating_to(); // Line 459 in
evm.rs

  1. Shared Through EthEvmContext

When zkVM is called, it receives the same
EthEvmContext that EVM uses:

// In zksync_try_call (strategy/zksync):
foundry_zksync_core::vm::call(call,
factory_deps, ecx, ccx)

  1. zkVM Extracts Basefee from Shared Context

// In runner.rs:303 - zkVM reads basefee from
the shared ecx.block:
block_basefee: min(max_fee_per_gas.to_ru256(),
rU256::from(ecx.block.basefee)),

Why do we use different approach?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added shared source of truth ecx.block.basefee, then clamping the value with maximum gas price.

impl Cheatcode for feeCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { newBasefee } = self;
ensure!(*newBasefee <= U256::from(u64::MAX), "base fee must be less than 2^64 - 1");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vm.fee method accepts up to uint256.max value function fee(uint256 newBasefee) external. While testing, had one case where PVM mode was not enabled, so I observed this line throws error while the use case seems perfectly valid for EVM setup. The saturating_to() can handle u256 values and will bound them to the max (no panicking or wrapping) of basefee which is u64. Let me know if my testing was wrong for the EVM use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Forge test] [Cheatcode support] Implement vm.fee

4 participants