@pytest.mark.parametrize("value_bearing", [True, False])
def test_selfdestruct_existing(
benchmark_test: BenchmarkTestFiller,
pre: Alloc,
value_bearing: bool,
fork: Fork,
gas_benchmark_value: int,
) -> None:
"""Benchmark SELFDESTRUCT instruction for existing contracts."""
selfdestructable_contract = Op.SELFDESTRUCT(Op.CALLER, address_warm=True)
# Initcode
initcode = (
Op.MSTORE8(
0,
Op.CALLER.int(),
# gas accounting
old_memory_size=0,
new_memory_size=2,
)
+ Op.MSTORE8(1, Op.SELFDESTRUCT.int())
+ Op.RETURN(0, 2, code_deposit_size=2)
)
# Factory Contract Setup
# CALLDATA[0:32] = start index
# CALLDATA[32:64] = end index
factory_setup = (
Op.MSTORE(
0,
initcode.hex(),
# Gas accounting
old_memory_size=0,
new_memory_size=32,
)
+ Op.ADD(1, Op.CALLDATALOAD(32))
+ Op.CALLDATALOAD(0)
)
factory_iterating = While(
body=Op.POP(
Op.CREATE2(
value=1 if value_bearing else 0,
offset=32 - len(initcode),
size=len(initcode),
salt=Op.DUP1,
# gas accounting
init_code_size=len(initcode),
)
),
condition=Op.PUSH1(1) + Op.ADD + Op.DUP1 + Op.DUP3 + Op.GT,
)
factory_code = IteratingBytecode(
setup=factory_setup,
iterating=factory_iterating,
iterating_subcall=initcode,
cleanup=Op.STOP,
)
factory_address = pre.deploy_contract(
code=factory_code,
balance=10**18,
)
create2_preimage = Create2PreimageLayout(
factory_address=factory_address,
salt=Op.CALLDATALOAD(0),
init_code_hash=initcode.keccak256(),
)
# Attack Contract Setup
# CALLDATA[0:32] = start index
# CALLDATA[32:64] = end index
attack_setup = (
create2_preimage + Op.ADD(1, Op.CALLDATALOAD(32)) + Op.CALLDATALOAD(0)
)
loop = While(
body=Op.POP(
Op.CALL(
address=create2_preimage.address_op(),
address_warm=False,
)
)
+ create2_preimage.increment_salt_op(),
condition=Op.PUSH1(1) + Op.ADD + Op.DUP1 + Op.DUP3 + Op.GT,
)
attack_code = IteratingBytecode(
setup=attack_setup,
iterating=loop,
iterating_subcall=selfdestructable_contract,
cleanup=Op.STOP,
)
attack_code_address = pre.deploy_contract(code=attack_code)
def calldata(iteration_count: int, start_iteration: int) -> bytes:
index_end = iteration_count + start_iteration - 1
return Hash(start_iteration) + Hash(index_end)
# Compute iteration counts and expected gas from the gas model.
iteration_counts = list(
attack_code.tx_iterations_by_gas_limit(
fork=fork,
gas_limit=gas_benchmark_value,
calldata=calldata,
)
)
num_contracts = sum(iteration_counts)
def factory_calldata(iteration_count: int, start_iteration: int) -> bytes:
index_end = iteration_count + start_iteration - 1
return Hash(start_iteration) + Hash(index_end)
with TestPhaseManager.setup():
setup_sender = pre.fund_eoa()
setup_txs = list(
factory_code.transactions_by_total_iteration_count(
fork=fork,
total_iterations=num_contracts,
sender=setup_sender,
to=factory_address,
calldata=factory_calldata,
)
)
with TestPhaseManager.execution():
attack_sender = pre.fund_eoa()
exec_txs = list(
attack_code.transactions_by_gas_limit(
fork=fork,
gas_limit=gas_benchmark_value,
sender=attack_sender,
to=attack_code_address,
calldata=calldata,
)
)
total_gas_cost = sum(tx.gas_cost for tx in exec_txs)
post = {}
for i in range(num_contracts):
deployed_contract_address = compute_create2_address(
address=factory_address,
salt=i,
initcode=initcode,
)
post[deployed_contract_address] = Account(nonce=1)
post[attack_code_address] = Account(
balance=num_contracts if value_bearing else 0
)
benchmark_test(
post=post,
target_opcode=Op.SELFDESTRUCT,
blocks=[
Block(txs=setup_txs),
Block(txs=exec_txs),
],
expected_benchmark_gas_used=total_gas_cost,
)