From 0de5bac35f7635956ccfad9356ed10e83f80561d Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Fri, 5 Dec 2025 10:12:35 -0300 Subject: [PATCH 1/8] feat(tests): Refactor xcall test to use deploy_max_contract_factory for contract deployment --- .../compute/instruction/test_system.py | 147 ++++++++++-------- 1 file changed, 79 insertions(+), 68 deletions(-) diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index 78fde39b9a..4bfb5fd551 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -18,6 +18,7 @@ import pytest from execution_testing import ( Account, + Address, Alloc, BenchmarkTestFiller, Block, @@ -108,74 +109,7 @@ def test_xcall( "during the setup phase of this test." ) - # The initcode will take its address as a starting point to the input to - # the keccak hash function. It will reuse the output of the hash function - # in a loop to create a large amount of seemingly random code, until it - # reaches the maximum contract size. - initcode = ( - Op.MSTORE(0, Op.ADDRESS) - + While( - body=( - Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) - # Use a xor table to avoid having to call the "expensive" sha3 - # opcode as much - + sum( - ( - Op.PUSH32[xor_value] - + Op.XOR - + Op.DUP1 - + Op.MSIZE - + Op.MSTORE - ) - for xor_value in XOR_TABLE - ) - + Op.POP - ), - condition=Op.LT(Op.MSIZE, max_contract_size), - ) - # Despite the whole contract has random bytecode, we make the first - # opcode be a STOP so CALL-like attacks return as soon as possible, - # while EXTCODE(HASH|SIZE) work as intended. - + Op.MSTORE8(0, 0x00) - + Op.RETURN(0, max_contract_size) - ) - initcode_address = pre.deploy_contract(code=initcode) - - # The factory contract will simply use the initcode that is already - # deployed, and create a new contract and return its address if successful. - factory_code = ( - Op.EXTCODECOPY( - address=initcode_address, - dest_offset=0, - offset=0, - size=Op.EXTCODESIZE(initcode_address), - ) - + Op.MSTORE( - 0, - Op.CREATE2( - value=0, - offset=0, - size=Op.EXTCODESIZE(initcode_address), - salt=Op.SLOAD(0), - ), - ) - + Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)) - + Op.RETURN(0, 32) - ) - factory_address = pre.deploy_contract(code=factory_code) - - # The factory caller will call the factory contract N times, creating N new - # contracts. Calldata should contain the N value. - factory_caller_code = Op.CALLDATALOAD(0) + While( - body=Op.POP(Op.CALL(address=factory_address)), - condition=Op.PUSH1(1) - + Op.SWAP1 - + Op.SUB - + Op.DUP1 - + Op.ISZERO - + Op.ISZERO, - ) - factory_caller_address = pre.deploy_contract(code=factory_caller_code) + initcode, factory_address, factory_caller_address = deploy_max_contract_factory(pre, fork) with TestPhaseManager.setup(): contracts_deployment_tx = Transaction( @@ -246,6 +180,83 @@ def test_xcall( exclude_full_post_state_in_output=True, ) +def deploy_max_contract_factory( + pre: Alloc, + fork: Fork, +) -> tuple[Bytecode, Address, Address]: + max_contract_size = fork.max_code_size() + + # The initcode will take its address as a starting point to the input to + # the keccak hash function. It will reuse the output of the hash function + # in a loop to create a large amount of seemingly random code, until it + # reaches the maximum contract size. + initcode = ( + Op.MSTORE(0, Op.ADDRESS) + + While( + body=( + Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) + # Use a xor table to avoid having to call the "expensive" sha3 + # opcode as much + + sum( + ( + Op.PUSH32[xor_value] + + Op.XOR + + Op.DUP1 + + Op.MSIZE + + Op.MSTORE + ) + for xor_value in XOR_TABLE + ) + + Op.POP + ), + condition=Op.LT(Op.MSIZE, max_contract_size), + ) + # Despite the whole contract has random bytecode, we make the first + # opcode be a STOP so CALL-like attacks return as soon as possible, + # while EXTCODE(HASH|SIZE) work as intended. + + Op.MSTORE8(0, 0x00) + + Op.RETURN(0, max_contract_size) + ) + initcode_address = pre.deploy_contract(code=initcode) + + # The factory contract will simply use the initcode that is already + # deployed, and create a new contract and return its address if successful. + factory_code = ( + Op.EXTCODECOPY( + address=initcode_address, + dest_offset=0, + offset=0, + size=Op.EXTCODESIZE(initcode_address), + ) + + Op.MSTORE( + 0, + Op.CREATE2( + value=0, + offset=0, + size=Op.EXTCODESIZE(initcode_address), + salt=Op.SLOAD(0), + ), + ) + + Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)) + + Op.RETURN(0, 32) + ) + factory_address = pre.deploy_contract(code=factory_code) + + # The factory caller will call the factory contract N times, creating N new + # contracts. Calldata should contain the N value. + factory_caller_code = Op.CALLDATALOAD(0) + While( + body=Op.POP(Op.CALL(address=factory_address)), + condition=Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.ISZERO + + Op.ISZERO, + ) + factory_caller_address = pre.deploy_contract(code=factory_caller_code) + + return initcode, factory_address, factory_caller_address + @pytest.mark.parametrize( "opcode", From d7ff98f072050dc4ab3211a08b01d3e894ff0377 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Fri, 5 Dec 2025 10:12:35 -0300 Subject: [PATCH 2/8] feat(tests): change xcall test to deploy multiple contracts in separate transactions --- .../compute/instruction/test_system.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index 4bfb5fd551..d15ec3448b 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -109,16 +109,33 @@ def test_xcall( "during the setup phase of this test." ) - initcode, factory_address, factory_caller_address = deploy_max_contract_factory(pre, fork) + initcode, factory_address, factory_caller_address = ( + deploy_max_contract_factory(pre, fork) + ) with TestPhaseManager.setup(): - contracts_deployment_tx = Transaction( - to=factory_caller_address, - gas_limit=env.gas_limit, - gas_price=10**6, - data=Hash(num_contracts), - sender=pre.fund_eoa(), + # We require deploying num_contracts by calling the factory contract. Each call can only + # use up to the transaction gas limit cap, thus we need to split the deployment into + # multiple transactions. The factory contract uses a storage slot to keep track of the last + # seed used to generate bytecodes, so it is safe to call it multiple times in different txs. + num_contracts_per_tx = ( + 3 # TODO: Try generalizing for any tx gas limit cap. For 17M is 3. ) + num_contract_creation_txs = math.ceil( + num_contracts / num_contracts_per_tx + ) + + contracts_deployment_txs = [] + for _ in range(num_contract_creation_txs): + contracts_deployment_txs.append( + Transaction( + to=factory_caller_address, + gas_limit=fork.transaction_gas_limit_cap(), + gas_price=10**6, + data=Hash(num_contracts_per_tx), + sender=pre.fund_eoa(), + ) + ) post = {} deployed_contract_addresses = [] @@ -174,16 +191,17 @@ def test_xcall( pre=pre, post=post, blocks=[ - Block(txs=[contracts_deployment_tx]), - Block(txs=[opcode_tx]), + Block(txs=contracts_deployment_txs), + # Block(txs=[opcode_tx]), ], exclude_full_post_state_in_output=True, ) + def deploy_max_contract_factory( pre: Alloc, fork: Fork, -) -> tuple[Bytecode, Address, Address]: +) -> tuple[Bytecode, Address, Address]: max_contract_size = fork.max_code_size() # The initcode will take its address as a starting point to the input to From 90d15e19b1f09bd1c9290957b2382e47d50f3f5c Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Fri, 5 Dec 2025 10:12:36 -0300 Subject: [PATCH 3/8] feat(tests): update test_xcall to partition attack in tx_max_gas_limit transactions with proper offseting to avoid overlap --- .../compute/instruction/test_system.py | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index d15ec3448b..91a0893802 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -91,7 +91,7 @@ def test_xcall( + gas_costs.G_COLD_ACCOUNT_ACCESS # Opcode cost + 30 # ~Gluing opcodes ) - # Calculate the number of contracts to be targeted + # Calculate an upper bound of the number of contracts to be targeted num_contracts = ( # Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs) attack_gas_limit - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4 @@ -121,12 +121,10 @@ def test_xcall( num_contracts_per_tx = ( 3 # TODO: Try generalizing for any tx gas limit cap. For 17M is 3. ) - num_contract_creation_txs = math.ceil( - num_contracts / num_contracts_per_tx - ) + attack_txs = math.ceil(num_contracts / num_contracts_per_tx) contracts_deployment_txs = [] - for _ in range(num_contract_creation_txs): + for _ in range(attack_txs): contracts_deployment_txs.append( Transaction( to=factory_caller_address, @@ -138,7 +136,6 @@ def test_xcall( ) post = {} - deployed_contract_addresses = [] for i in range(num_contracts): deployed_contract_address = compute_create2_address( address=factory_address, @@ -146,7 +143,6 @@ def test_xcall( initcode=initcode, ) post[deployed_contract_address] = Account(nonce=1) - deployed_contract_addresses.append(deployed_contract_address) attack_call = Bytecode() if opcode == Op.EXTCODECOPY: @@ -162,7 +158,7 @@ def test_xcall( # 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)] Op.MSTORE(0, factory_address) + Op.MSTORE8(32 - 20 - 1, 0xFF) - + Op.MSTORE(32, 0) + + Op.MSTORE(32, Op.CALLDATALOAD(0)) + Op.MSTORE(64, initcode.keccak256()) # Main loop + While( @@ -177,22 +173,47 @@ def test_xcall( f"Code size {len(attack_code)} exceeds maximum " f"code size {max_contract_size}" ) - opcode_address = pre.deploy_contract(code=attack_code) + attack_address = pre.deploy_contract(code=attack_code) with TestPhaseManager.execution(): - opcode_tx = Transaction( - to=opcode_address, - gas_limit=attack_gas_limit, - gas_price=10**9, - sender=pre.fund_eoa(), - ) + tx_gas_cap = fork.transaction_gas_limit_cap() + full_txs = attack_gas_limit // tx_gas_cap + remainder = attack_gas_limit % tx_gas_cap + + num_targeted_contracts_per_full_tx = ( + # Base available gas = TX_GAS_LIMIT - intrinsic - (out of loop MSTOREs) + tx_gas_cap - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4 + ) // loop_cost + contract_start_index = 0 + opcode_txs = [] + for _ in range(full_txs): + opcode_txs.append( + Transaction( + to=attack_address, + gas_limit=tx_gas_cap, + gas_price=10**9, + data=Hash(contract_start_index), + sender=pre.fund_eoa(), + ) + ) + contract_start_index += num_targeted_contracts_per_full_tx + if remainder > 0: + opcode_txs.append( + Transaction( + to=attack_address, + gas_limit=remainder, + gas_price=10**9, + data=Hash(contract_start_index), + sender=pre.fund_eoa(), + ) + ) blockchain_test( pre=pre, post=post, blocks=[ Block(txs=contracts_deployment_txs), - # Block(txs=[opcode_tx]), + Block(txs=opcode_txs), ], exclude_full_post_state_in_output=True, ) From 3aa28b001eddc34db4906ed0724815a40140000a Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Fri, 5 Dec 2025 11:10:08 -0300 Subject: [PATCH 4/8] refactor(tests): improvements --- .../compute/instruction/test_system.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index 91a0893802..6bef382091 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -110,16 +110,20 @@ def test_xcall( ) initcode, factory_address, factory_caller_address = ( - deploy_max_contract_factory(pre, fork) + _deploy_max_contract_factory(pre, fork) ) + # Deploy num_contracts via multiple txs (each capped by tx gas limit). with TestPhaseManager.setup(): - # We require deploying num_contracts by calling the factory contract. Each call can only - # use up to the transaction gas limit cap, thus we need to split the deployment into - # multiple transactions. The factory contract uses a storage slot to keep track of the last - # seed used to generate bytecodes, so it is safe to call it multiple times in different txs. - num_contracts_per_tx = ( - 3 # TODO: Try generalizing for any tx gas limit cap. For 17M is 3. + # Rough estimate (rounded down) of contracts per tx based on dominant + # cost factor only. E.g., 17M gas limit + 24KiB contracts = ~3 per tx. + # The goal is to involve the minimum amount of gas pricing to avoid + # complexity and potential brittleness. + # If this estimation is incorrect in the future (i.e. tx gas limit cap) + # is increased or cost per byte, the post-state check will detect it + # and can be adjusted with a more complex formula. + num_contracts_per_tx = fork.transaction_gas_limit_cap() // ( + gas_costs.G_CODE_DEPOSIT_BYTE * max_contract_size ) attack_txs = math.ceil(num_contracts / num_contracts_per_tx) @@ -181,7 +185,8 @@ def test_xcall( remainder = attack_gas_limit % tx_gas_cap num_targeted_contracts_per_full_tx = ( - # Base available gas = TX_GAS_LIMIT - intrinsic - (out of loop MSTOREs) + # Base available gas: + # TX_GAS_LIMIT - intrinsic - (out of loop MSTOREs) tx_gas_cap - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4 ) // loop_cost contract_start_index = 0 @@ -219,7 +224,7 @@ def test_xcall( ) -def deploy_max_contract_factory( +def _deploy_max_contract_factory( pre: Alloc, fork: Fork, ) -> tuple[Bytecode, Address, Address]: From d8c8b05d62133482756da817ca3af88c27686865 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Fri, 5 Dec 2025 12:58:02 -0300 Subject: [PATCH 5/8] feat(benchmarks): lints --- .../benchmark/compute/instruction/test_system.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index 6bef382091..fdd490d92e 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -68,6 +68,7 @@ def test_xcall( max_contract_size = fork.max_code_size() gas_costs = fork.gas_costs() + tx_gas_limit_cap = fork.transaction_gas_limit_cap() or attack_gas_limit # Calculate the absolute minimum gas costs to deploy the contract This does # not take into account setting up the actual memory (using KECCAK256 and @@ -122,7 +123,7 @@ def test_xcall( # If this estimation is incorrect in the future (i.e. tx gas limit cap) # is increased or cost per byte, the post-state check will detect it # and can be adjusted with a more complex formula. - num_contracts_per_tx = fork.transaction_gas_limit_cap() // ( + num_contracts_per_tx = tx_gas_limit_cap // ( gas_costs.G_CODE_DEPOSIT_BYTE * max_contract_size ) attack_txs = math.ceil(num_contracts / num_contracts_per_tx) @@ -132,7 +133,7 @@ def test_xcall( contracts_deployment_txs.append( Transaction( to=factory_caller_address, - gas_limit=fork.transaction_gas_limit_cap(), + gas_limit=tx_gas_limit_cap, gas_price=10**6, data=Hash(num_contracts_per_tx), sender=pre.fund_eoa(), @@ -180,14 +181,15 @@ def test_xcall( attack_address = pre.deploy_contract(code=attack_code) with TestPhaseManager.execution(): - tx_gas_cap = fork.transaction_gas_limit_cap() - full_txs = attack_gas_limit // tx_gas_cap - remainder = attack_gas_limit % tx_gas_cap + full_txs = attack_gas_limit // tx_gas_limit_cap + remainder = attack_gas_limit % tx_gas_limit_cap num_targeted_contracts_per_full_tx = ( # Base available gas: # TX_GAS_LIMIT - intrinsic - (out of loop MSTOREs) - tx_gas_cap - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4 + tx_gas_limit_cap + - intrinsic_gas_cost_calc() + - gas_costs.G_VERY_LOW * 4 ) // loop_cost contract_start_index = 0 opcode_txs = [] @@ -195,7 +197,7 @@ def test_xcall( opcode_txs.append( Transaction( to=attack_address, - gas_limit=tx_gas_cap, + gas_limit=tx_gas_limit_cap, gas_price=10**9, data=Hash(contract_start_index), sender=pre.fund_eoa(), From 5dee3072a78ce03c817ef7e6f8e579475bba63e0 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Fri, 5 Dec 2025 13:15:00 -0300 Subject: [PATCH 6/8] feat(bench): add assertion for transaction gas limit cap in test_xcall --- tests/benchmark/compute/instruction/test_system.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index fdd490d92e..b6713dcf2d 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -68,7 +68,10 @@ def test_xcall( max_contract_size = fork.max_code_size() gas_costs = fork.gas_costs() - tx_gas_limit_cap = fork.transaction_gas_limit_cap() or attack_gas_limit + tx_gas_limit_cap = fork.transaction_gas_limit_cap() + assert tx_gas_limit_cap is not None, ( + "This benchmark requires a tx gas limit cap" + ) # Calculate the absolute minimum gas costs to deploy the contract This does # not take into account setting up the actual memory (using KECCAK256 and From 29315dec2c1dfb8ead207119b68fac3617f25f1b Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Mon, 8 Dec 2025 08:04:31 -0300 Subject: [PATCH 7/8] feedback review Signed-off-by: Ignacio Hagopian --- tests/benchmark/compute/instruction/test_system.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index b6713dcf2d..03d7ad803e 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -137,7 +137,6 @@ def test_xcall( Transaction( to=factory_caller_address, gas_limit=tx_gas_limit_cap, - gas_price=10**6, data=Hash(num_contracts_per_tx), sender=pre.fund_eoa(), ) @@ -174,13 +173,6 @@ def test_xcall( ) ) - if len(attack_code) > max_contract_size: - # TODO: A workaround could be to split the opcode code into multiple - # contracts and call them in sequence. - raise ValueError( - f"Code size {len(attack_code)} exceeds maximum " - f"code size {max_contract_size}" - ) attack_address = pre.deploy_contract(code=attack_code) with TestPhaseManager.execution(): @@ -201,7 +193,6 @@ def test_xcall( Transaction( to=attack_address, gas_limit=tx_gas_limit_cap, - gas_price=10**9, data=Hash(contract_start_index), sender=pre.fund_eoa(), ) @@ -212,7 +203,6 @@ def test_xcall( Transaction( to=attack_address, gas_limit=remainder, - gas_price=10**9, data=Hash(contract_start_index), sender=pre.fund_eoa(), ) From 92bd0954e995af74035479220a101c9e54ab734a Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Mon, 8 Dec 2025 08:35:39 -0300 Subject: [PATCH 8/8] test_xcall: generalize to any fork Signed-off-by: Ignacio Hagopian --- .../compute/instruction/test_system.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index 03d7ad803e..61b5f2b89d 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -59,19 +59,16 @@ def test_xcall( gas_benchmark_value: int, ) -> None: """Benchmark a system execution where a single opcode execution.""" - # The attack gas limit is the gas limit which the target tx will use The - # test will scale the block gas limit to setup the contracts accordingly to - # be able to pay for the contract deposit. This has to take into account - # the 200 gas per byte, but also the quadratic memory expansion costs which - # have to be paid each time the memory is being setup + # The attack gas limit represents the transaction gas limit cap or + # the block gas limit. If eip-7825 is applied, the test will create + # multiple transactions for contract deployment. It should account + # for the 200 gas per byte cost and the quadratic memory-expansion + # costs, which must be paid each time memory is initialized. attack_gas_limit = gas_benchmark_value max_contract_size = fork.max_code_size() gas_costs = fork.gas_costs() tx_gas_limit_cap = fork.transaction_gas_limit_cap() - assert tx_gas_limit_cap is not None, ( - "This benchmark requires a tx gas limit cap" - ) # Calculate the absolute minimum gas costs to deploy the contract This does # not take into account setting up the actual memory (using KECCAK256 and @@ -126,17 +123,24 @@ def test_xcall( # If this estimation is incorrect in the future (i.e. tx gas limit cap) # is increased or cost per byte, the post-state check will detect it # and can be adjusted with a more complex formula. - num_contracts_per_tx = tx_gas_limit_cap // ( - gas_costs.G_CODE_DEPOSIT_BYTE * max_contract_size + num_contracts_per_tx = ( + tx_gas_limit_cap + // (gas_costs.G_CODE_DEPOSIT_BYTE * max_contract_size) + if tx_gas_limit_cap + else num_contracts + ) + attack_txs = ( + math.ceil(num_contracts / num_contracts_per_tx) + if num_contracts_per_tx + else 1 ) - attack_txs = math.ceil(num_contracts / num_contracts_per_tx) contracts_deployment_txs = [] for _ in range(attack_txs): contracts_deployment_txs.append( Transaction( to=factory_caller_address, - gas_limit=tx_gas_limit_cap, + gas_limit=tx_gas_limit_cap or env.gas_limit, data=Hash(num_contracts_per_tx), sender=pre.fund_eoa(), ) @@ -176,13 +180,17 @@ def test_xcall( attack_address = pre.deploy_contract(code=attack_code) with TestPhaseManager.execution(): - full_txs = attack_gas_limit // tx_gas_limit_cap - remainder = attack_gas_limit % tx_gas_limit_cap + full_txs = ( + attack_gas_limit // tx_gas_limit_cap if tx_gas_limit_cap else 1 + ) + remainder = ( + attack_gas_limit % tx_gas_limit_cap if tx_gas_limit_cap else 0 + ) num_targeted_contracts_per_full_tx = ( # Base available gas: # TX_GAS_LIMIT - intrinsic - (out of loop MSTOREs) - tx_gas_limit_cap + (tx_gas_limit_cap or attack_gas_limit) - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4 ) // loop_cost @@ -192,7 +200,7 @@ def test_xcall( opcode_txs.append( Transaction( to=attack_address, - gas_limit=tx_gas_limit_cap, + gas_limit=tx_gas_limit_cap or attack_gas_limit, data=Hash(contract_start_index), sender=pre.fund_eoa(), )