Skip to content

[PolkaVM] Dry runs fail (but submission succeed) for contracts that contain consume_all_gas with resolc version 0.6.0 #259

@0xOmarA

Description

@0xOmarA

Summary

Version 0.6.0 of resolc made changes so that if any contract attempts to access out of bounds memory then the consume_all_gas host function is called and the entirety of the gas that the callframe has available to it is consumed. This differs from the behavior of version 0.5.0 of resolc where accessing out of bounds memory was just a trap.

We noticed that PolkaVM contracts that call the consume_all_gas host function fail dry runs with an OutOfGas error but can succeed when being submitted to the network with a sane gas limit.

Minimal Reproducible Example

The following contract is a minimal reproducible example of this error. You can run this contract either through the revive differential testing or you can also run it manually to observe its behavior. In this example, we will run the contract manually.

//! {
//!     "cases": [
//!         {
//!             "name": "Revert with invalid pointer doesn't consume all gas",
//!             "inputs": [
//!                 {
//!                     "method": "test",
//!                     "calldata": [],
//!                     "caller": "0x552cec68aedc3ada9eb9e612f010e884fd22cd18"
//!                 }
//!             ],
//!             "expected": [
//!                 "0"
//!             ]
//!         }
//!     ]
//! }

// SPDX-License-Identifier: MIT

pragma solidity >=0.4.21;

contract Test {
    /**
     * Calls the `main` function with the max offset and length and returns the success code.
     */
    function test() external {
        assembly {
            mstore(0, 0xcc572cf9) // main selector
            mstore(
                32,
                0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
            )
            mstore(
                64,
                0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
            )
            let gas_value := div(mul(gas(), 1), 100)
            let success := call(gas_value, address(), 0, 28, 68, 0, 0)

            mstore(0, success)
            return(0, 32)
        }
    }

    /**
     * A function that takes in the offset and length and reverts with the given offset and length.
     */
    function main(uint256 offset, uint256 len) external pure {
        assembly {
            // nullify memory ptr slot
            mstore(0x40, 0)
            revert(offset, len)
        }
    }
}

The primary function we use in the test is the test function which initializes all of the memory locations needed to perform a call and then calls the main function with 1% of the gas that's available to the parent call-frame. The main function is called with and offset and length equal to $2^{256} - 1$ which is 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF and the test function then returns the status code of whether execution succeeded or failed.

Assuming that you already have a revive-dev-node and eth-rpc up and running you can reproduce this issue with the following command which would publish the contract for you and then perform a dry run and submit the transaction:

echo "🔮 Deploying contract"
CONTRACT_ADDRESS=$(cast send --private-key 0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133 --create 50564d00009a06000000000000010700c23000c200000480ac0a000000000700000015000000230000003100000039000000480000005500000060000000720000006164647265737363616c6c5f646174615f636f707963616c6c5f646174615f6c6f616463616c6c5f646174615f73697a6563616c6c5f65766d636f6e73756d655f616c6c5f6761737265665f74696d655f6c6566747365616c5f72657475726e7365745f696d6d757461626c655f6461746176616c75655f7472616e73666572726564051102837a0463616c6c8386066465706c6f790685be140284f31a001f0045009b00a700de00ff00180154015901c301e30103029a02140363037a0386039303e8039511f07b10087b158475010a03013d07000002510507501002095010048d009511a07b10587b15509515608411e049111849111049018000330740641849110850100684034911384911304911284911208317200a0901821730821838821928821a20d49808d4a707d487075107123308100002838833070133090a0701330710000283778000330833090a01282203330780003308501008200483783307330933000a0a07019511c0fe7b1038017b1530017b162801951540018411e064164926f8004926f0004926e00080003307409568e0004926e80050100ceb027b67383907000002531704569517e08477e07b673064718377330833000e0a020182673081771c5147f92c57cc4f52476dfda8f83050121080020a0901826a3082a71082a81882a90882aad49808d4a707d4870751074b33081000022808330810000201838833070133093300120a073302142842025012143f020a0901826a3082a71082a81882a90882aad49808d4a707d4870751071a330810000228c739070000025617031c330810000228b739070000025617432301330810000228a64926d8004926d0004926c8001407f92c57cc000000009568c0007b67c000330750101606024926b800ff4926b000ff4926a800ff3307209568a0004926a000ff501018e60149269800ff49269000ff49268800ff3307409568800049268000ff50101ac6010a06330864cb87077b6730330710000483777b67280a019519e08499e064914919183907040002491910491908490956175f0b200304000260019517f88477e064714807330c10000295c81c9788209888207b6a38140a0000000044000000d4a80b7b6c2097c820988820977720d4870c8399330708826828826a300a048877014916784916704916689568607b6760330750101c2f01390704000256171f0b2003040002200181682033092033070a0701836700013308040a02826718017b6730826710017b6728826708017b6720826700017b6718836700013308240a02956800018287187b67088287107b67108287087b6782877b6738491658491650491648330740c8760849164050101eb500826718826830826920d48909826a28d4a708d49808988820d4a909979920d4980852083f826838826a10d4a809826808826bd4b808d48909989920d4a808978820d4980852081c826838501020580183788169383307010a073300220a0528083300220a05019511f87b1033075010247ffc9511f87b1033070150102672fc9517e08477e07b67306471330849171849171049170849078377320239080800025108dcfc330730000483770a0828cffc9511d87b10207b15187b161082897b19088289087b19828510828618330820501028d3006f686f59821a6faa821b086fbb787b18787a10787908787898bc38787c1f98bc30787c1e98bc28787c1d98bc20787c1c98bc18787c1b98bc10787c1a98bb08787b1998ab38787b1798ab30787b1698ab28787b1598ab20787b1498ab18787b1398ab10787b1298aa08787a11989a38787a0f989a30787a0e989a28787a0d989a20787a0c989a18787a0b989a10787a0a9899087879099889387879079889307879069889287879059889207879049889187879039889107879029888087878018210208215188216109511283200838951092e8b7a11520a34330b010002aeb92cc8780883881f8488e0563800000220390a040002ae8a093d080400020183773308100002c8870732000049694884244912292149d2244992506a882a51a8d2888848114122424224a9d22410084d929224149652129a24254928442884504404105108212442082124422849282725244530a9902401244952929224298408263552241289448a244925499290244992244952922449282995a6424a485249aa10a192242525095529494992244992244992244992244992244992244992244952494292042161210500 --json | jq -r '.contractAddress')
echo "🔮 Deployed Contract at Address: $CONTRACT_ADDRESS"
echo "🔮 Running a dry run of calling the \`test\` function (Should Fail ❌)"
cast call \
    --rpc-url http://127.0.0.1:8545 \
    --from 0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac \
    $CONTRACT_ADDRESS "test()"
echo "🔮 Submitting a transaction calling the \`test\` function (Should Succeed ✅)"
cast send \
    --rpc-url http://127.0.0.1:8545 \
    --private-key 0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133 \
    --gas-limit 10000000 \
    --gas-price 100000000001 \
    --priority-gas-price 1 \
    $CONTRACT_ADDRESS "test()"

Node Logs

The revive-dev-node shows the following logs for the dry run:

2026-01-26 21:57:16.051 DEBUG tokio-runtime-worker runtime::revive: dry_run_eth_transact: GenericTransaction { access_list: None, authorization_list: [], blob_versioned_hashes: [], blobs: [], chain_id: Some(420420420), from: Some(0xf24ff3a9cf04c71dbc94d0b566f7a27b94566cac), gas: None, gas_price: None, input: InputOrData { input: Some(Bytes(0xf8a8fd6d)), data: Some(Bytes(0xf8a8fd6d)) }, max_fee_per_blob_gas: None, max_fee_per_gas: None, max_priority_fee_per_gas: None, nonce: None, to: Some(0x21cb3940e6ba5284e1750f1109131a8e8062b9f1), type: None, value: None }
2026-01-26 21:57:16.051 DEBUG tokio-runtime-worker runtime::revive: Start new meter: transaction_limits=TransactionLimits::EthereumGas { eth_gas_limit: 18446744073709551615, maybe_weight_limit: None, eth_tx_info: EthTxInfo { encoded_len: 133, extra_weight: Weight { ref_time: 1403745664, proof_size: 17044 }, _phantom: PhantomData<revive_dev_runtime::Runtime> } }
2026-01-26 21:57:16.051 TRACE tokio-runtime-worker runtime::revive: New meter done: weight_left=Some(Weight { ref_time: 18446744072305805951, proof_size: 18446744073709534571 }), deposit_left=Some(922337203685462768847336), weight_consumed=Weight { ref_time: 0, proof_size: 0 }, deposit_consumed=Charge(0)
2026-01-26 21:57:16.051 TRACE tokio-runtime-worker runtime::revive: Creating nested meter from parent: limit=CallResources::NoLimits, weight_left=Some(Weight { ref_time: 18446744072301621545, proof_size: 18446744073709532881 }), deposit_left=Some(922337203685462764662930), weight_consumed=Weight { ref_time: 4184406, proof_size: 1690 }, deposit_consumed=Charge(0)
2026-01-26 21:57:16.051 TRACE tokio-runtime-worker runtime::revive: Creating nested meter done: weight_left=Some(Weight { ref_time: 18446744072301621545, proof_size: 18446744073709532881 }), deposit_left=Some(922337203685462764662930), weight_consumed=Weight { ref_time: 0, proof_size: 0 }, deposit_consumed=Charge(0)
2026-01-26 21:57:16.051 TRACE tokio-runtime-worker runtime::revive::strace: call_data_size() = Ok(4) weight_consumed: Weight { ref_time: 1008744, proof_size: 0 }
2026-01-26 21:57:16.051 TRACE tokio-runtime-worker runtime::revive::strace: call_data_load(out_ptr: 4294835840, offset: 0) = Ok(()) weight_consumed: Weight { ref_time: 7938088, proof_size: 0 }
2026-01-26 21:57:16.051 TRACE tokio-runtime-worker runtime::revive::strace: value_transferred(out_ptr: 4294835808) = Ok(()) weight_consumed: Weight { ref_time: 9329672, proof_size: 0 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: ref_time_left() = Ok(18446744073709254731) weight_consumed: Weight { ref_time: 28134086, proof_size: 0 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: address(out_ptr: 262160) = Ok(()) weight_consumed: Weight { ref_time: 28669530, proof_size: 0 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: Creating nested meter from parent: limit=CallResources::Ethereum { gas: 184467440737092547, add_stipend: false }, weight_left=Some(Weight { ref_time: 18446744071957908437, proof_size: 18446744073709525597 }), deposit_left=Some(922337203685462420949822), weight_consumed=Weight { ref_time: 343713108, proof_size: 7284 }, deposit_consumed=Charge(0)
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: Creating nested meter done: weight_left=Some(Weight { ref_time: 18446744071957908437, proof_size: 18446744073709525597 }), deposit_left=Some(9223372036854627350000), weight_consumed=Weight { ref_time: 0, proof_size: 0 }, deposit_consumed=Charge(0)
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: call_data_size() = Ok(68) weight_consumed: Weight { ref_time: 1008744, proof_size: 0 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: call_data_load(out_ptr: 4294835840, offset: 0) = Ok(()) weight_consumed: Weight { ref_time: 7938088, proof_size: 0 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: value_transferred(out_ptr: 4294835808) = Ok(()) weight_consumed: Weight { ref_time: 9280442, proof_size: 0 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: call_data_load(out_ptr: 4294836128, offset: 4) = Ok(()) weight_consumed: Weight { ref_time: 11828316, proof_size: 0 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: call_data_load(out_ptr: 4294836128, offset: 36) = Ok(()) weight_consumed: Weight { ref_time: 12357760, proof_size: 0 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: consume_all_gas() = Err(Return(ReturnData { flags: 1, data: [] })) weight_consumed: Weight { ref_time: 18446744071957908437, proof_size: 18446744073709525597 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: frame finished with: Ok(ExecReturnValue { flags: REVERT, data: [] })
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: Absorb weight meter only: parent_weight_left=Some(Weight { ref_time: 18446744071957908437, proof_size: 18446744073709525597 }), parent_deposit_left=Some(922337203685462420949822), parent_weight_consumed=Weight { ref_time: 343713108, proof_size: 7284 }, parent_deposit_consumed=Charge(0), child_weight_left=Some(Weight { ref_time: 0, proof_size: 0 }), child_deposit_left=Some(9204925292782669441563), child_weight_consumed=Weight { ref_time: 18446744071957908437, proof_size: 18446744073709525597 }, child_deposit_consumed=Charge(0)
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: Absorb weight meter done: parent_weight_left=Some(Weight { ref_time: 0, proof_size: 0 }), parent_deposit_left=Some(922318756941390463041385), parent_weight_consumed=Weight { ref_time: 18446744072301621545, proof_size: 18446744073709532881 }, parent_deposit_consumed=Charge(0)
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive::strace: call_evm(flags: 8, callee: 262160, value_ptr: 4294835776, gas: 184467440737092547, input_data: 292057907244, output_data: 18446179062171959312) = Err(SupervisorError(Module(ModuleError { index: 5, error: [3, 0, 0, 0], message: Some("OutOfGas") }))) weight_consumed: Weight { ref_time: 18446744072301621545, proof_size: 18446744073709532881 }
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: frame finished with: Err(ExecError { error: Module(ModuleError { index: 5, error: [3, 0, 0, 0], message: Some("OutOfGas") }), origin: Callee })
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: Absorb weight meter only: parent_weight_left=Some(Weight { ref_time: 18446744072301621545, proof_size: 18446744073709532881 }), parent_deposit_left=Some(922337203685462764662930), parent_weight_consumed=Weight { ref_time: 4184406, proof_size: 1690 }, parent_deposit_consumed=Charge(0), child_weight_left=Some(Weight { ref_time: 0, proof_size: 0 }), child_deposit_left=Some(922318756941390463041385), child_weight_consumed=Weight { ref_time: 18446744072301621545, proof_size: 18446744073709532881 }, child_deposit_consumed=Charge(0)
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: Absorb weight meter done: parent_weight_left=Some(Weight { ref_time: 0, proof_size: 0 }), parent_deposit_left=Some(922318756941390463041385), parent_weight_consumed=Weight { ref_time: 18446744072305805951, proof_size: 18446744073709534571 }, parent_deposit_consumed=Charge(0)
2026-01-26 21:57:16.052 TRACE tokio-runtime-worker runtime::revive: Bare call ends: result=Err(ExecError { error: Module(ModuleError { index: 5, error: [3, 0, 0, 0], message: Some("OutOfGas") }), origin: Callee }), weight_consumed=Weight { ref_time: 18446744072305805951, proof_size: 18446744073709534571 }, weight_required=Weight { ref_time: 18446744072305805951, proof_size: 18446744073709534571 }, storage_deposit=Charge(0), gas_consumed=368934881742355, max_storage_deposit=Charge(0)
2026-01-26 21:57:16.052 DEBUG tokio-runtime-worker runtime::revive: Failed to execute call: Module(ModuleError { index: 5, error: [3, 0, 0, 0], message: Some("OutOfGas") })

Logs Analysis

There are a few things that we can see in the logs:

  1. The gas limit used for the dry run is 18446744073709551615 which is $2^{64} - 1$ which is equivalent to 18446744072301621545 ref time and 18446744073709532881 proof size weights.
  2. The nested meter created for the call to the main function is given 184467440737092547 which 1% of the gas that's available to the parent which is expected. However, the ref time given to it is 18446744071957908437 which almost equal to that of the parent and the proof size is 18446744073709525597 which is almost to that of the parent. This is unexpected since we only wanted to pass in 1% of the gas (and weights) available to the call.
  3. Because the call to main is being allocated more ref_time and proof_size than it should it ends up finishing up the entire ref_time and proof_size available to the entire transaction and not just this call.

Note

  1. We're able to submit this transaction if we provide sane a sane gas limit, but we can't dry run it.
  2. It seems like whatever issue we have here only happens with large values of gas so I'm assuming that it has something to do with overflows.
  3. This is related to Transaction fails with OutOfGas error #244

Metadata

Metadata

Assignees

No one assigned

    Labels

    PVMProblem occurs at the execution stage for PVMerror: CompilerProblem occurs at the compiling stage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions