Skip to content

Test Reentrancy Selfdestruct Revert

Documentation for tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py.

Generate fixtures for these test cases with:

fill -v tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py

Suicide scenario requested test tests#1325

test_reentrancy_selfdestruct_revert(env, fork, first_suicide, second_suicide, state_test)

Suicide reentrancy scenario:

Call|Callcode|Delegatecall the contract S. S self destructs. Call the revert proxy contract R. R Calls|Callcode|Delegatecall S. S self destructs (for the second time). R reverts (including the effects of the second selfdestruct). It is expected the S is self destructed after the transaction.

Source code in tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
@pytest.mark.valid_from("Paris")
@pytest.mark.parametrize("first_suicide", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL])
@pytest.mark.parametrize("second_suicide", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL])
def test_reentrancy_selfdestruct_revert(
    env: Environment,
    fork: Fork,
    first_suicide: Op,
    second_suicide: Op,
    state_test: StateTestFiller,
):
    """
    Suicide reentrancy scenario:

    Call|Callcode|Delegatecall the contract S.
    S self destructs.
    Call the revert proxy contract R.
    R Calls|Callcode|Delegatecall S.
    S self destructs (for the second time).
    R reverts (including the effects of the second selfdestruct).
    It is expected the S is self destructed after the transaction.
    """
    address_to = TestAddress2
    address_s = Address(0x1000000000000000000000000000000000000001)
    address_r = Address(0x1000000000000000000000000000000000000002)
    suicide_d = Address(0x03E8)

    def construct_call_s(call_type: Op, money: int):
        if call_type in [Op.CALLCODE, Op.CALL]:
            return call_type(Op.GAS, address_s, money, 0, 0, 0, 0)
        else:
            return call_type(Op.GAS, address_s, money, 0, 0, 0)

    pre = {
        address_to: Account(
            balance=1000000000000000000,
            nonce=0,
            code=Op.SSTORE(1, construct_call_s(first_suicide, 0))
            + Op.SSTORE(2, Op.CALL(Op.GAS, address_r, 0, 0, 0, 0, 0))
            + Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE())
            + Op.SSTORE(3, Op.MLOAD(0)),
            storage={0x01: 0x0100, 0x02: 0x0100, 0x03: 0x0100},
        ),
        address_s: Account(
            balance=3000000000000000000,
            nonce=0,
            code=Op.SELFDESTRUCT(1000),
            storage={},
        ),
        address_r: Account(
            balance=5000000000000000000,
            nonce=0,
            # Send money when calling it suicide second time to make sure the funds not transferred
            code=Op.MSTORE(0, Op.ADD(15, construct_call_s(second_suicide, 100)))
            + Op.REVERT(0, 32),
            storage={},
        ),
        TestAddress: Account(
            balance=7000000000000000000,
            nonce=0,
            code="0x",
            storage={},
        ),
    }

    post = {
        # Second caller unchanged as call gets reverted
        address_r: Account(balance=5000000000000000000, storage={}),
    }

    if first_suicide in [Op.CALLCODE, Op.DELEGATECALL]:
        if fork >= Cancun:
            # On Cancun even callcode/delegatecall does not remove the account, so the value remain
            post[address_to] = Account(
                storage={
                    0x01: 0x01,  # First call to contract S->suicide success
                    0x02: 0x00,  # Second call to contract S->suicide reverted
                    0x03: 16,  # Reverted value to check that revert really worked
                },
            )
        else:
            # Callcode executed first suicide from sender. sender is deleted
            post[address_to] = Account.NONEXISTENT  # type: ignore

        # Original suicide account remains in state
        post[address_s] = Account(balance=3000000000000000000, storage={})
        # Suicide destination
        post[suicide_d] = Account(
            balance=1000000000000000000,
        )

    # On Cancun suicide no longer destroys the account from state, just cleans the balance
    if first_suicide in [Op.CALL]:
        post[address_to] = Account(
            storage={
                0x01: 0x01,  # First call to contract S->suicide success
                0x02: 0x00,  # Second call to contract S->suicide reverted
                0x03: 16,  # Reverted value to check that revert really worked
            },
        )
        if fork >= Cancun:
            # On Cancun suicide does not remove the account, just sends the balance
            post[address_s] = Account(balance=0, code="0x6103e8ff", storage={})
        else:
            post[address_s] = Account.NONEXISTENT  # type: ignore

        # Suicide destination
        post[suicide_d] = Account(
            balance=3000000000000000000,
        )

    tx = Transaction(
        ty=0x0,
        chain_id=0x0,
        nonce=0,
        to=address_to,
        gas_price=10,
        protected=False,
        data="",
        gas_limit=500000,
        value=0,
    )

    state_test(env=env, pre=pre, post=post, tx=tx)