diff --git a/tests/cancun/eip1153_tstore/test_tstorage.py b/tests/cancun/eip1153_tstore/test_tstorage.py index 6edd1e989d..de8efe4c2e 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage.py +++ b/tests/cancun/eip1153_tstore/test_tstorage.py @@ -6,15 +6,16 @@ [ethereum/tests/src/EIPTestsFiller/StateTests/stEIP1153-transientStorage/](https://github.com/ethereum/tests/blob/9b00b68593f5869eb51a6659e1cc983e875e616b/src/EIPTestsFiller/StateTests/stEIP1153-transientStorage) """ # noqa: E501 -# from typing import Mapping +from enum import unique import pytest -from ethereum_test_tools import Account, Environment +from ethereum_test_tools import Account, Code, CodeGasMeasure, Environment from ethereum_test_tools import Opcodes as Op from ethereum_test_tools import StateTestFiller, TestAddress, Transaction -from .spec import ref_spec_1153 +from . import PytestParameterEnum +from .spec import Spec, ref_spec_1153 REFERENCE_SPEC_GIT_PATH = ref_spec_1153.git_path REFERENCE_SPEC_VERSION = ref_spec_1153.version @@ -96,6 +97,52 @@ def test_tload_after_tstore(state_test: StateTestFiller): ) +def test_tload_after_sstore(state_test: StateTestFiller): + """ + Loading after storing returns the stored value: TSTORE(x, y), TLOAD(x) + returns y. + + Based on [ethereum/tests/.../18_tloadAfterStoreFiller.yml](https://github.com/ethereum/tests/blob/9b00b68593f5869eb51a6659e1cc983e875e616b/src/EIPTestsFiller/StateTests/stEIP1153-transientStorage/18_tloadAfterStoreFiller.yml)", # noqa: E501 + """ + env = Environment() + + slots_under_test = [1, 3, 2**128, 2**256 - 1] + code = b"".join( + [ + Op.SSTORE(slot - 1, 0xFF) + Op.SSTORE(slot, Op.TLOAD(slot - 1)) + for slot in slots_under_test + ] + ) + + pre = { + TestAddress: Account(balance=10_000_000), + code_address: Account(code=code, storage={slot: 1 for slot in slots_under_test}), + } + + txs = [ + Transaction( + to=code_address, + data=b"", + gas_limit=1_000_000, + ) + ] + + post = { + code_address: Account( + code=code, + storage={slot - 1: 0xFF for slot in slots_under_test} + | {slot: 0 for slot in slots_under_test}, + ) + } + + state_test( + env=env, + pre=pre, + post=post, + txs=txs, + ) + + def test_tload_after_tstore_is_zero(state_test: StateTestFiller): """ Test that tload returns zero after tstore is called with zero. @@ -139,3 +186,73 @@ def test_tload_after_tstore_is_zero(state_test: StateTestFiller): post=post, txs=txs, ) + + +@unique +class GasMeasureTestCases(PytestParameterEnum): + """ + Test cases for gas measurement. + """ + + TLOAD = { + "description": "Test that tload() of an empty slot consumes the expected gas.", + "bytecode": Op.TLOAD(10), + "overhead_cost": 3, # 1 x PUSH1 + "extra_stack_items": 1, + "expected_gas": Spec.TLOAD_GAS_COST, + } + TSTORE_TLOAD = { + "description": "Test that tload() of a used slot consumes the expected gas.", + "bytecode": Op.TSTORE(10, 10) + Op.TLOAD(10), + "overhead_cost": 3 * 3, # 3 x PUSH1 + "extra_stack_items": 1, + "expected_gas": Spec.TSTORE_GAS_COST + Spec.TLOAD_GAS_COST, + } + TSTORE_COLD = { + "description": "Test that tstore() of a previously unused slot consumes the expected gas.", + "bytecode": Op.TSTORE(10, 10), + "overhead_cost": 2 * 3, # 2 x PUSH1 + "extra_stack_items": 0, + "expected_gas": Spec.TSTORE_GAS_COST, + } + TSTORE_WARM = { + "description": "Test that tstore() of a previously used slot consumes the expected gas.", + "bytecode": Op.TSTORE(10, 10) + Op.TSTORE(10, 11), + "overhead_cost": 4 * 3, # 4 x PUSH1 + "extra_stack_items": 0, + "expected_gas": 2 * Spec.TSTORE_GAS_COST, + } + + +@GasMeasureTestCases.parametrize() +def test_gas_usage( + state_test: StateTestFiller, + bytecode: Code, + expected_gas: int, + overhead_cost: int, + extra_stack_items: int, +): + """ + Test that tstore and tload consume the expected gas. + """ + gas_measure_bytecode = CodeGasMeasure( + code=bytecode, overhead_cost=overhead_cost, extra_stack_items=extra_stack_items + ) + + env = Environment() + pre = { + TestAddress: Account(balance=10_000_000, nonce=0), + code_address: Account(code=gas_measure_bytecode), + } + txs = [ + Transaction( + to=code_address, + data=b"", + gas_limit=1_000_000, + ) + ] + post = { + code_address: Account(code=gas_measure_bytecode, storage={0: expected_gas}), + TestAddress: Account(nonce=1), + } + state_test(env=env, pre=pre, txs=txs, post=post) diff --git a/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py index f0ef98bfd5..5f22bb8dc2 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py @@ -39,6 +39,12 @@ class DynamicCallContextTestCases(EnumMeta): def __new__(cls, name, bases, classdict): # noqa: D102 for opcode in [Op.CALLCODE, Op.DELEGATECALL]: + if opcode == Op.DELEGATECALL: + contract_call = opcode(Op.GAS(), callee_address, 0, 0, 0, 0) + elif opcode == Op.CALLCODE: + contract_call = opcode(Op.GAS(), callee_address, 0, 0, 0, 0, 0) + else: + raise ValueError("Unexpected opcode.") classdict[opcode._name_] = { "description": ( "Caller and callee contracts share transient storage when callee is " @@ -46,7 +52,7 @@ def __new__(cls, name, bases, classdict): # noqa: D102 ), "caller_bytecode": ( Op.TSTORE(0, 420) - + Op.SSTORE(0, opcode(Op.GAS(), callee_address, 0, 0, 0, 0, 0)) + + Op.SSTORE(0, contract_call) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(4, Op.TLOAD(1)) ), @@ -56,16 +62,23 @@ def __new__(cls, name, bases, classdict): # noqa: D102 "expected_caller_storage": {0: 1, 1: 420, 2: 420, 3: 69, 4: 69}, "expected_callee_storage": {}, } + + for opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL]: + if opcode == Op.DELEGATECALL: + contract_call = opcode(Op.GAS(), callee_address, 0, 0, 0, 0) + elif opcode in [Op.CALL, Op.CALLCODE]: + contract_call = opcode(Op.GAS(), callee_address, 0, 0, 0, 0, 0) + else: + raise ValueError("Unexpected opcode.") classdict[f"{opcode._name_}_WITH_REVERT"] = { "description": ( - "Caller and callee contracts share transient storage when callee is " - f"called via {opcode._name_} but transient storage usage is discarded " - "from sub-call upon REVERT." + f"Transient storage usage is discarded from sub-call with {opcode._name_} " + "upon REVERT." ), "caller_bytecode": ( Op.TSTORE(0, 420) + Op.TSTORE(1, 420) - + Op.SSTORE(0, opcode(Op.GAS(), callee_address, 0, 0, 0, 0, 0)) + + Op.SSTORE(0, contract_call) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(2, Op.TLOAD(1)) ), @@ -73,16 +86,22 @@ def __new__(cls, name, bases, classdict): # noqa: D102 "expected_caller_storage": {0: 0, 1: 420, 2: 420}, "expected_callee_storage": {}, } + + if opcode == Op.DELEGATECALL: + contract_call = opcode(0xFF, callee_address, 0, 0, 0, 0) + elif opcode in [Op.CALL, Op.CALLCODE]: + contract_call = opcode(0xFF, callee_address, 0, 0, 0, 0, 0) + else: + raise ValueError("Unexpected opcode.") classdict[f"{opcode._name_}_WITH_INVALID"] = { "description": ( - "Caller and callee contracts share transient storage when callee is " - f"called via {opcode._name_} but transient storage usage is discarded " - "from sub-call upon INVALID. Note: Gas passed to sub-call is capped." + f"Transient storage usage is discarded from sub-call with {opcode._name_} " + "upon REVERT. Note: Gas passed to sub-call is capped." ), "caller_bytecode": ( Op.TSTORE(0, 420) + Op.TSTORE(1, 420) - + Op.SSTORE(0, opcode(0xFF, callee_address, 0, 0, 0, 0, 0)) + + Op.SSTORE(0, contract_call) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(2, Op.TLOAD(1)) ), @@ -90,16 +109,22 @@ def __new__(cls, name, bases, classdict): # noqa: D102 "expected_caller_storage": {0: 0, 1: 420, 2: 420}, "expected_callee_storage": {}, } + + if opcode == Op.DELEGATECALL: + contract_call = opcode(0xFFFF, callee_address, 0, 0, 0, 0) + elif opcode in [Op.CALL, Op.CALLCODE]: + contract_call = opcode(0xFFFF, callee_address, 0, 0, 0, 0, 0) + else: + raise ValueError("Unexpected opcode.") classdict[f"{opcode._name_}_WITH_STACK_OVERFLOW"] = { "description": ( - "Caller and callee contracts share transient storage when callee is " - f"called via {opcode._name_} but transient storage usage is discarded " - "from sub-call upon stack overflow." + f"Transient storage usage is discarded from sub-call with {opcode._name_} " + "upon stack overflow." ), "caller_bytecode": ( Op.TSTORE(0, 420) + Op.TSTORE(1, 420) - + Op.SSTORE(0, opcode(0xFFFF, callee_address, 0, 0, 0, 0, 0)) + + Op.SSTORE(0, contract_call) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(2, Op.TLOAD(1)) ), @@ -109,14 +134,13 @@ def __new__(cls, name, bases, classdict): # noqa: D102 } classdict[f"{opcode._name_}_WITH_TSTORE_STACK_UNDERFLOW"] = { "description": ( - "Caller and callee contracts share transient storage when callee is " - f"called via {opcode._name_} but transient storage usage is discarded " - "from sub-call upon stack underflow because of TSTORE parameters (1)." + f"Transient storage usage is discarded from sub-call with {opcode._name_} " + "upon stack underflow because of TSTORE parameters (1)." ), "caller_bytecode": ( Op.TSTORE(0, 420) + Op.TSTORE(1, 420) - + Op.SSTORE(0, opcode(0xFFFF, callee_address, 0, 0, 0, 0, 0)) + + Op.SSTORE(0, contract_call) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(2, Op.TLOAD(1)) ), @@ -126,14 +150,13 @@ def __new__(cls, name, bases, classdict): # noqa: D102 } classdict[f"{opcode._name_}_WITH_TSTORE_STACK_UNDERFLOW_2"] = { "description": ( - "Caller and callee contracts share transient storage when callee is " - f"called via {opcode._name_} but transient storage usage is discarded " - "from sub-call upon stack underflow because of TSTORE parameters (0)." + f"Transient storage usage is discarded from sub-call with {opcode._name_} " + " upon stack underflow because of TSTORE parameters (0)." ), "caller_bytecode": ( Op.TSTORE(0, 420) + Op.TSTORE(1, 420) - + Op.SSTORE(0, opcode(0xFFFF, callee_address, 0, 0, 0, 0, 0)) + + Op.SSTORE(0, contract_call) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(2, Op.TLOAD(1)) ), @@ -143,14 +166,13 @@ def __new__(cls, name, bases, classdict): # noqa: D102 } classdict[f"{opcode._name_}_WITH_TLOAD_STACK_UNDERFLOW"] = { "description": ( - "Caller and callee contracts share transient storage when callee is " - f"called via {opcode._name_} but transient storage usage is discarded " - "from sub-call upon stack underflow because of TLOAD parameters (0)." + f"Transient storage usage is discarded from sub-call with {opcode._name_} " + "upon stack underflow because of TLOAD parameters (0)." ), "caller_bytecode": ( Op.TSTORE(0, 420) + Op.TSTORE(1, 420) - + Op.SSTORE(0, opcode(0xFFFF, callee_address, 0, 0, 0, 0, 0)) + + Op.SSTORE(0, contract_call) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(2, Op.TLOAD(1)) ), @@ -158,12 +180,22 @@ def __new__(cls, name, bases, classdict): # noqa: D102 "expected_caller_storage": {0: 0, 1: 420, 2: 420}, "expected_callee_storage": {}, } + + gas_limit = Spec.TSTORE_GAS_COST + (PUSH_OPCODE_COST * 2) - 1 + if opcode == Op.DELEGATECALL: + contract_call = opcode( + opcode(gas_limit, callee_address, 0, 0, 0, 0), + ) + elif opcode in [Op.CALL, Op.CALLCODE]: + contract_call = opcode( + opcode(gas_limit, callee_address, 0, 0, 0, 0, 0), + ) + else: + raise ValueError("Unexpected opcode.") classdict[f"{opcode._name_}_WITH_OUT_OF_GAS"] = { "description": ( - "Caller and callee contracts share transient storage when callee is " - f"called via {opcode._name_} but transient storage usage is discarded " - "from sub-call upon out of gas during TSTORE. Note: Gas passed to sub-call is " - "capped." + f"Transient storage usage is discarded from sub-call with {opcode._name_} " + "upon out of gas during TSTORE. Note: Gas passed to sub-call is capped." ), "caller_bytecode": ( Op.TSTORE(0, 420) @@ -187,17 +219,22 @@ def __new__(cls, name, bases, classdict): # noqa: D102 "expected_caller_storage": {0: 0, 1: 420, 2: 420}, "expected_callee_storage": {}, } + + if opcode == Op.DELEGATECALL: + contract_call = opcode(0xFF, callee_address, 0, 0, 0, 0) + elif opcode in [Op.CALL, Op.CALLCODE]: + contract_call = opcode(0xFF, callee_address, 0, 0, 0, 0, 0) + else: + raise ValueError("Unexpected opcode.") classdict[f"{opcode._name_}_WITH_OUT_OF_GAS_2"] = { "description": ( - "Caller and callee contracts share transient storage when callee is " - f"called via {opcode._name_} but transient storage usage is discarded " - "from sub-call upon out of gas after TSTORE. Note: Gas passed to sub-call is " - "capped." + f"Transient storage usage is discarded from sub-call with {opcode._name_} " + "upon out of gas after TSTORE. Note: Gas passed to sub-call is capped." ), "caller_bytecode": ( Op.TSTORE(0, 420) + Op.TSTORE(1, 420) - + Op.SSTORE(0, opcode(0xFF, callee_address, 0, 0, 0, 0, 0)) + + Op.SSTORE(0, contract_call) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(2, Op.TLOAD(1)) ), @@ -264,7 +301,7 @@ class CallContextTestCases(PytestParameterEnum, metaclass=DynamicCallContextTest + Op.SSTORE(0, Op.STATICCALL(Op.GAS(), callee_address, 0, 0, 0, 0)) + Op.SSTORE(1, Op.TLOAD(0)) ), - "callee_bytecode": Op.TLOAD(0), # calling tload does fail the call + "callee_bytecode": Op.TLOAD(0), # calling tload does not cause the call to fail "expected_caller_storage": {0: 1, 1: 420}, "expected_callee_storage": {}, } diff --git a/tests/cancun/eip1153_tstore/test_tstorage_reentrancy_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_reentrancy_contexts.py index 0f2e70615a..5ad4c3c244 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_reentrancy_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_reentrancy_contexts.py @@ -42,7 +42,7 @@ def __new__(cls, name, bases, classdict): # noqa: D102 subcall_gas = Op.GAS() elif opcode == Op.INVALID: opcode_call = Op.INVALID() - subcall_gas = 0xFF + subcall_gas = 0xFFFF else: raise ValueError(f"Unknown opcode: {opcode}.") @@ -112,15 +112,14 @@ def __new__(cls, name, bases, classdict): # noqa: D102 if opcode == Op.REVERT: opcode_call = Op.REVERT(0, 32) - pytest_marks = [] + second_call_return_value = 1 elif opcode == Op.INVALID: opcode_call = Op.INVALID() - pytest_marks = pytest.mark.xfail + second_call_return_value = 0 else: raise ValueError(f"Unknown opcode: {opcode}.") classdict[f"{opcode._name_}_UNDOES_TSTORAGE_AFTER_SUCCESSFUL_CALL"] = { - "pytest_marks": pytest_marks, "description": ( f"{opcode._name_} undoes transient storage writes from inner calls that " "successfully returned. TSTORE(x, y), CALL(self, ...), CALL(self, ...), " @@ -133,8 +132,8 @@ def __new__(cls, name, bases, classdict): # noqa: D102 Op.TSTORE(0xFF, 0x100) + Op.SSTORE(2, Op.TLOAD(0xFF)) + Op.MSTORE(0, 2) - + Op.SSTORE(0, Op.CALL(subcall_gas, callee_address, 0, 0, 32, 0, 32)) - + Op.SSTORE(1, Op.MLOAD(0)) # should be 1 (successful call) + + Op.SSTORE(0, Op.CALL(subcall_gas, callee_address, 0, 0, 32, 32, 32)) + + Op.SSTORE(1, Op.MLOAD(32)) # should be 1 (successful call) + Op.SSTORE(3, Op.TLOAD(0xFF)) ), cases=[ @@ -148,10 +147,13 @@ def __new__(cls, name, bases, classdict): # noqa: D102 ), ), # the second, reentrant call, which returns successfully - CalldataCase(value=3, action=Op.TSTORE(0xFF, 0x101)), + CalldataCase( + value=3, + action=Op.TSTORE(0xFF, 0x101), + ), ], ), - "expected_storage": {0: 0x00, 1: 0x01, 2: 0x100, 3: 0x100}, + "expected_storage": {0: 0x00, 1: second_call_return_value, 2: 0x100, 3: 0x100}, } return super().__new__(cls, name, bases, classdict) @@ -221,6 +223,40 @@ class ReentrancyTestCases(PytestParameterEnum, metaclass=DynamicReentrancyTestCa ), "expected_storage": {0: 0x01, 1: 0x100, 2: 0x101, 3: 0x101}, } + TSTORE_IN_CALL_THEN_TLOAD_RETURN_IN_STATICCALL = { + "description": ( + "A reentrant call followed by a reentrant subcall can call tload correctly: " + "TSTORE(x, y), CALL(self, ...), STATICCALL(self, ...), TLOAD(x), RETURN returns y." + "Based on [ethereum/tests/.../10_revertUndoesStoreAfterReturnFiller.yml](https://github.com/ethereum/tests/blob/9b00b68593f5869eb51a6659e1cc983e875e616b/src/EIPTestsFiller/StateTests/stEIP1153-transientStorage/10_revertUndoesStoreAfterReturnFiller.yml).", # noqa: E501 + ), + "bytecode": Switch( + default_action=( # setup; make first reentrant sub-call + Op.TSTORE(0xFF, 0x100) + + Op.SSTORE(2, Op.TLOAD(0xFF)) + + Op.MSTORE(0, 2) + + Op.SSTORE(0, Op.CALL(Op.GAS(), callee_address, 0, 0, 32, 0, 0)) + + Op.SSTORE(4, Op.TLOAD(0xFE)) + ), + cases=[ + # the first, reentrant call which calls tstore and a further reentrant staticcall + CalldataCase( + value=2, + action=( + Op.TSTORE(0xFE, 0x101) + + Op.MSTORE(0, 3) + + Op.SSTORE(1, Op.STATICCALL(Op.GAS(), callee_address, 0, 32, 0, 32)) + + Op.SSTORE(3, Op.MLOAD(0)) + ), + ), + # the second, reentrant call, which calls tload and return returns successfully + CalldataCase( + value=3, + action=Op.MSTORE(0, Op.TLOAD(0xFE)) + Op.RETURN(0, 32), + ), + ], + ), + "expected_storage": {0: 0x01, 1: 0x01, 2: 0x100, 3: 0x101, 4: 0x101}, + } @ReentrancyTestCases.parametrize() @@ -238,7 +274,7 @@ def test_reentrant_call(state_test: StateTestFiller, bytecode, expected_storage) tx = Transaction( to=callee_address, data=to_hash_bytes(1), - gas_limit=1_000_000, + gas_limit=10_000_000, ) post = {callee_address: Account(code=bytecode, storage=expected_storage)} diff --git a/tests/cancun/eip1153_tstore/test_tstorage_selfdestruct.py b/tests/cancun/eip1153_tstore/test_tstorage_selfdestruct.py new file mode 100644 index 0000000000..8225c67f0e --- /dev/null +++ b/tests/cancun/eip1153_tstore/test_tstorage_selfdestruct.py @@ -0,0 +1,252 @@ +""" +abstract: Tests for [EIP-1153: Transient Storage](https://eips.ethereum.org/EIPS/eip-1153) + + Test cases for `TSTORE` and `TLOAD` opcode calls in reentrancy after self-destruct, taking into + account the changes in EIP-6780. +""" # noqa: E501 + +from enum import unique +from typing import Dict + +import pytest + +from ethereum_test_tools import Account, CalldataCase, Environment, Initcode +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import ( + StateTestFiller, + Switch, + TestAddress, + Transaction, + compute_create_address, +) + +from . import PytestParameterEnum +from .spec import ref_spec_1153 + +REFERENCE_SPEC_GIT_PATH = ref_spec_1153.git_path +REFERENCE_SPEC_VERSION = ref_spec_1153.version + +pytestmark = [pytest.mark.valid_from("Cancun")] + +# Addresses +caller_address = 0x100 +copy_from_initcode_address = 0x200 +callee_address = compute_create_address(caller_address, 1) + +CREATE_CODE = Op.EXTCODECOPY( + copy_from_initcode_address, 0, 0, Op.EXTCODESIZE(copy_from_initcode_address) +) + Op.CREATE(0, 0, Op.EXTCODESIZE(copy_from_initcode_address)) + + +def call_option(option_number: int) -> bytes: + """ + Return the bytecode for a call to the callee contract with the given option number. + """ + return Op.MSTORE(0, option_number) + Op.CALL( + Op.GAS, Op.PUSH20(callee_address), 0, 0, 32, 0, 32 + ) + + +@unique +class SelfDestructCases(PytestParameterEnum): + """ + Transient storage test cases for different reentrancy calls which involve the contract + self-destructing. + """ + + TLOAD_AFTER_SELFDESTRUCT_PRE_EXISTING_CONTRACT = { + "description": ( + "Use TSTORE to store a transient value and self-destruct in a contract that was" + "deployed in a transaction prior to the one currently executing." + "Then re-enter the contract and attempt to TLOAD the transient value.", + ), + "pre_existing_contract": True, + "caller_bytecode": Op.SSTORE(0, call_option(1)) + + Op.SSTORE(1, call_option(2)) + + Op.SSTORE(2, Op.MLOAD(0)), + "callee_bytecode": Switch( + default_action=b"", + cases=[ + CalldataCase(value=1, action=Op.TSTORE(0xFF, 0x100) + Op.SELFDESTRUCT(0)), + CalldataCase(value=2, action=Op.MSTORE(0, Op.TLOAD(0xFF)) + Op.RETURN(0, 32)), + ], + ), + "expected_storage": { + 0: 0x01, + 1: 0x01, + 2: 0x100, + }, + } + + TLOAD_AFTER_SELFDESTRUCT_NEW_CONTRACT = { + "description": ( + "Use TSTORE to store a transient value and self-destruct in a contract that was" + "deployed in the current transaction." + "Then re-enter the contract and attempt to TLOAD the transient value.", + ), + "pre_existing_contract": False, + "caller_bytecode": Op.SSTORE(0, CREATE_CODE) + + Op.SSTORE(1, call_option(1)) + + Op.SSTORE(2, call_option(2)) + + Op.SSTORE(3, Op.MLOAD(0)), + "callee_bytecode": Switch( + default_action=b"", + cases=[ + CalldataCase(value=1, action=Op.TSTORE(0xFF, 0x100) + Op.SELFDESTRUCT(0)), + CalldataCase(value=2, action=Op.MSTORE(0, Op.TLOAD(0xFF)) + Op.RETURN(0, 32)), + ], + ), + "expected_storage": { + 0: callee_address, + 1: 0x01, + 2: 0x01, + 3: 0x100, + }, + } + + TLOAD_AFTER_INNER_SELFDESTRUCT_PRE_EXISTING_CONTRACT = { + "description": ( + "Use TSTORE to store a transient value and then call for re-entry and self-destruct," + "and use TLOAD upon return from the inner self-destructing call.", + ), + "pre_existing_contract": True, + "caller_bytecode": Op.SSTORE(0, call_option(1)) + Op.SSTORE(1, Op.MLOAD(0)), + "callee_bytecode": Switch( + default_action=b"", + cases=[ + CalldataCase( + value=1, + action=Op.TSTORE(0xFF, 0x100) + + call_option(2) + + Op.MSTORE(0, Op.TLOAD(0xFF)) + + Op.RETURN(0, 32), + ), + CalldataCase(value=2, action=Op.SELFDESTRUCT(0)), + ], + ), + "expected_storage": { + 0: 0x01, + 1: 0x100, + }, + } + + TLOAD_AFTER_INNER_SELFDESTRUCT_NEW_CONTRACT = { + "description": ( + "In a newly created contract, use TSTORE to store a transient value and then call " + "for re-entry and self-destruct, and use TLOAD upon return from the inner " + "self-destructing call.", + ), + "pre_existing_contract": False, + "caller_bytecode": ( + Op.SSTORE(0, CREATE_CODE) + Op.SSTORE(1, call_option(1)) + Op.SSTORE(2, Op.MLOAD(0)) + ), + "callee_bytecode": Switch( + default_action=b"", + cases=[ + CalldataCase( + value=1, + action=Op.TSTORE(0xFF, 0x100) + + call_option(2) + + Op.MSTORE(0, Op.TLOAD(0xFF)) + + Op.RETURN(0, 32), + ), + CalldataCase(value=2, action=Op.SELFDESTRUCT(0)), + ], + ), + "expected_storage": { + 0: callee_address, + 1: 0x01, + 2: 0x100, + }, + } + + TSTORE_AFTER_SELFDESTRUCT_PRE_EXISTING_CONTRACT = { + "description": ( + "Use self-destruct in a pre-existing contract and then use TSTORE upon a re-entry." + "Lastly use TLOAD on another re-entry", + ), + "pre_existing_contract": True, + "caller_bytecode": Op.SSTORE(0, call_option(1)) + + Op.SSTORE(1, call_option(2)) + + Op.SSTORE(2, call_option(3)) + + Op.SSTORE(3, Op.MLOAD(0)), + "callee_bytecode": Switch( + default_action=b"", + cases=[ + CalldataCase(value=1, action=Op.SELFDESTRUCT(0)), + CalldataCase(value=2, action=Op.TSTORE(0xFF, 0x100)), + CalldataCase(value=3, action=Op.MSTORE(0, Op.TLOAD(0xFF)) + Op.RETURN(0, 32)), + ], + ), + "expected_storage": { + 0: 0x01, + 1: 0x01, + 2: 0x01, + 3: 0x100, + }, + } + + TSTORE_AFTER_SELFDESTRUCT_NEW_CONTRACT = { + "description": ( + "Use self-destruct in a newly created contract and then use TSTORE upon a re-entry." + "Lastly use TLOAD on another re-entry", + ), + "pre_existing_contract": False, + "caller_bytecode": Op.SSTORE(0, CREATE_CODE) + + Op.SSTORE(1, call_option(1)) + + Op.SSTORE(2, call_option(2)) + + Op.SSTORE(3, call_option(3)) + + Op.SSTORE(4, Op.MLOAD(0)), + "callee_bytecode": Switch( + default_action=b"", + cases=[ + CalldataCase(value=1, action=Op.SELFDESTRUCT(0)), + CalldataCase(value=2, action=Op.TSTORE(0xFF, 0x100)), + CalldataCase(value=3, action=Op.MSTORE(0, Op.TLOAD(0xFF)) + Op.RETURN(0, 32)), + ], + ), + "expected_storage": { + 0: callee_address, + 1: 0x01, + 2: 0x01, + 3: 0x01, + 4: 0x100, + }, + } + + +@SelfDestructCases.parametrize() +def test_reentrant_selfdestructing_call( + state_test: StateTestFiller, + pre_existing_contract, + caller_bytecode, + callee_bytecode, + expected_storage, +): + """ + Test transient storage in different reentrancy contexts after selfdestructing. + """ + env = Environment() + + pre = { + TestAddress: Account(balance=10**40), + caller_address: Account(code=caller_bytecode, nonce=1), + copy_from_initcode_address: Account(code=Initcode(deploy_code=callee_bytecode)), + } + + if pre_existing_contract: + pre[callee_address] = Account(code=callee_bytecode) + + tx = Transaction( + to=caller_address, + gas_limit=1_000_000, + ) + + post: Dict = {caller_address: Account(storage=expected_storage)} + + if pre_existing_contract: + post[callee_address] = Account(code=callee_bytecode) + else: + post[callee_address] = Account.NONEXISTENT + + state_test(env=env, pre=pre, post=post, txs=[tx]) diff --git a/whitelist.txt b/whitelist.txt index 8bcabd2b06..637cc77350 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -476,6 +476,7 @@ create2 staticcall revert selfdestruct +selfdestructing sendall blobhash mcopy