Skip to content

Test Selfdestruct Balance Bug

Documentation for test cases from tests/merge/security/test_selfdestruct_balance_bug.py.

Generate fixtures for these test cases with:

fill -v tests/merge/security/test_selfdestruct_balance_bug.py
Tests the Consensus Flaw During Block Processing related to SELFDESTRUCT

Tests the consensus-vulnerability reported in go-ethereum/security/advisories/GHSA-xw37-57qp-9mm4.

To reproduce the issue with this test case:

  1. Fill the test with the most recent geth evm version.
  2. Run the fixture output within a vulnerable geth version: v1.9.20 > geth >= v1.9.4.

test_tx_selfdestruct_balance_bug(blockchain_test, yul)

Test that the vulnerability is not present by checking the balance of the 0xaa contract after executing specific transactions:

  1. Start with contract 0xaa which has initial balance of 3 wei. 0xaa contract code simply performs a self-destruct to itself.

  2. Send a transaction (tx 1) to invoke caller contract 0xcc (which has a balance of 1 wei), which in turn invokes 0xaa with a 1 wei call.

  3. Store the balance of 0xaa after the first transaction is processed. 0xaa self-destructed. Expected outcome: 0 wei.

  4. Send another transaction (tx 2) to call 0xaa with 5 wei.

  5. Store the balance of 0xaa after the second transaction is processed. No self-destruct. Expected outcome: 5 wei.

  6. Verify that:

    • Call within tx 1 is successful, i.e 0xaa self-destructed.
    • The balances of 0xaa after each tx are correct.
    • During tx 2, code in 0xaa does not execute, hence self-destruct mechanism does not trigger.
Source code in tests/merge/security/test_selfdestruct_balance_bug.py
 27
 28
 29
 30
 31
 32
 33
 34
 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
@pytest.mark.compile_yul_with("Merge")  # Shanghai refuses to compile SELFDESTRUCT
@pytest.mark.valid_from("Constantinople")
def test_tx_selfdestruct_balance_bug(blockchain_test: BlockchainTestFiller, yul: YulCompiler):
    """
    Test that the vulnerability is not present by checking the balance of the
    `0xaa` contract after executing specific transactions:

    1. Start with contract `0xaa` which has initial balance of 3 wei.
        `0xaa` contract code simply performs a self-destruct to itself.

    2. Send a transaction (tx 1) to invoke caller contract `0xcc` (which
        has a balance of 1 wei), which in turn invokes `0xaa` with a 1 wei call.

    3. Store the balance of `0xaa` after the first transaction
        is processed. `0xaa` self-destructed. Expected outcome: 0 wei.

    4. Send another transaction (tx 2) to call 0xaa with 5 wei.

    5. Store the balance of `0xaa` after the second transaction
        is processed. No self-destruct. Expected outcome: 5 wei.

    6. Verify that:
        - Call within tx 1 is successful, i.e `0xaa` self-destructed.
        - The balances of `0xaa` after each tx are correct.
        - During tx 2, code in `0xaa` does not execute,
            hence self-destruct mechanism does not trigger.
    """
    aa_code = yul(
        """
        {
            /* 1st entrance is self-destruct */
            if eq(0, callvalue()) {
                selfdestruct(0x00000000000000000000000000000000000000AA)
            }

            /* 2nd entrance is other rnd code execution */
            if eq(1, callvalue()) {
                let x := selfbalance()
                sstore(0, x)
            }
        }
        """
    )

    cc_code = Op.SSTORE(0xCA1101, Op.CALL(100000, 0xAA, 0, 0, 0, 0, 0)) + Op.CALL(
        100000, 0xAA, 1, 0, 0, 0, 0
    )

    balance_code = Op.SSTORE(0xBA1AA, Op.BALANCE(0xAA))

    pre = {
        # sender
        TestAddress: Account(balance=1000000000),
        # caller
        to_address(0xCC): Account(balance=1000000000, code=cc_code),
        # initial balance of 3 wei
        to_address(0xAA): Account(balance=3, code=aa_code),
        # stores balance of 0xaa after each tx 1
        to_address(0xBA11): Account(code=balance_code),
        # stores balance of 0xaa after each tx 2
        to_address(0xBA12): Account(code=balance_code),
    }

    blocks = [
        Block(
            txs=[
                # Sender invokes caller, caller invokes 0xaa:
                # calling with 1 wei call
                Transaction(
                    nonce=0,
                    to=to_address(0xCC),
                    gas_limit=1000000,
                    gas_price=10,
                ),
                # Dummy tx to store balance of 0xaa after first TX.
                Transaction(
                    nonce=1,
                    to=to_address(0xBA11),
                    gas_limit=100000,
                    gas_price=10,
                ),
                # Sender calls 0xaa with 5 wei.
                Transaction(
                    nonce=2,
                    to=to_address(0xAA),
                    gas_limit=100000,
                    gas_price=10,
                    value=5,
                ),
                # Dummy tx to store balance of 0xaa after second TX.
                Transaction(
                    nonce=3,
                    to=to_address(0xBA12),
                    gas_limit=100000,
                    gas_price=10,
                ),
            ],
        ),
    ]

    post = {
        # Check call from caller has succeeded.
        to_address(0xCC): Account(storage={0xCA1101: 1}),
        # Check balance of 0xaa after tx 1 is 0 wei, i.e self-destructed.
        # Vulnerable versions should return 1 wei.
        to_address(0xBA11): Account(storage={0xBA1AA: 0}),
        # Check that 0xaa exists and balance after tx 2 is 5 wei.
        # Vulnerable versions should return 6 wei.
        to_address(0xBA12): Account(storage={0xBA1AA: 5}),
        to_address(0xAA): Account(storage={0: 0}),
    }

    blockchain_test(pre=pre, post=post, blocks=blocks)