|
16 | 16 | Alloc, |
17 | 17 | Block, |
18 | 18 | BlockchainTestFiller, |
| 19 | + Bytecode, |
19 | 20 | Environment, |
20 | 21 | Fork, |
| 22 | + Header, |
| 23 | + Initcode, |
21 | 24 | Op, |
22 | 25 | StateTestFiller, |
23 | 26 | Storage, |
24 | 27 | Transaction, |
| 28 | + compute_create_address, |
25 | 29 | ) |
26 | 30 |
|
27 | | -from .spec import ref_spec_8037 |
| 31 | +from .spec import init_code_at_high_bytes, ref_spec_8037 |
28 | 32 |
|
29 | 33 | REFERENCE_SPEC_GIT_PATH = ref_spec_8037.git_path |
30 | 34 | REFERENCE_SPEC_VERSION = ref_spec_8037.version |
@@ -248,3 +252,279 @@ def test_selfdestruct_new_beneficiary_header_gas_used( |
248 | 252 | ], |
249 | 253 | post={caller: Account(storage=storage)}, |
250 | 254 | ) |
| 255 | + |
| 256 | + |
| 257 | +@pytest.mark.parametrize( |
| 258 | + "num_slots", |
| 259 | + [ |
| 260 | + pytest.param(0, id="no_storage"), |
| 261 | + pytest.param(1, id="one_slot"), |
| 262 | + pytest.param(5, id="five_slots"), |
| 263 | + ], |
| 264 | +) |
| 265 | +@pytest.mark.with_all_create_opcodes() |
| 266 | +@pytest.mark.valid_from("EIP8037") |
| 267 | +def test_create_selfdestruct_refunds_account_and_storage( |
| 268 | + blockchain_test: BlockchainTestFiller, |
| 269 | + pre: Alloc, |
| 270 | + fork: Fork, |
| 271 | + create_opcode: Op, |
| 272 | + num_slots: int, |
| 273 | +) -> None: |
| 274 | + """ |
| 275 | + Verify same tx CREATE+SELFDESTRUCT refunds account and storage. |
| 276 | +
|
| 277 | + Factory CREATE/CREATE2 initcode does N cold SSTOREs then |
| 278 | + SELFDESTRUCTs. Refund covers `GAS_NEW_ACCOUNT` plus each |
| 279 | + created slot's state gas. Under OLD behavior the state charges |
| 280 | + remain in `block_state_gas_used`. Under NEW they are refunded. |
| 281 | + """ |
| 282 | + gas_limit_cap = fork.transaction_gas_limit_cap() |
| 283 | + assert gas_limit_cap is not None |
| 284 | + new_account_state_gas = fork.gas_costs().GAS_NEW_ACCOUNT |
| 285 | + sstore_state_gas = fork.sstore_state_gas() |
| 286 | + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() |
| 287 | + |
| 288 | + init_code = Bytecode() |
| 289 | + for i in range(num_slots): |
| 290 | + init_code += Op.SSTORE.with_metadata( |
| 291 | + key_warm=False, |
| 292 | + original_value=0, |
| 293 | + current_value=0, |
| 294 | + new_value=1, |
| 295 | + )(i, 1) |
| 296 | + init_code += Op.SELFDESTRUCT.with_metadata(address_warm=True)(Op.ADDRESS) |
| 297 | + mstore_value, size = init_code_at_high_bytes(init_code) |
| 298 | + |
| 299 | + # Metadata so `.gas_cost(fork)` matches runtime charges. |
| 300 | + mstore = Op.MSTORE.with_metadata(new_memory_size=32, old_memory_size=0)( |
| 301 | + 0, mstore_value |
| 302 | + ) |
| 303 | + create_metadata = create_opcode.with_metadata(init_code_size=size) |
| 304 | + create_call = ( |
| 305 | + create_metadata(value=0, offset=0, size=size, salt=0) |
| 306 | + if create_opcode == Op.CREATE2 |
| 307 | + else create_metadata(value=0, offset=0, size=size) |
| 308 | + ) |
| 309 | + factory_code = mstore + Op.POP(create_call) |
| 310 | + factory = pre.deploy_contract(code=factory_code) |
| 311 | + |
| 312 | + total_state_refund = new_account_state_gas + num_slots * sstore_state_gas |
| 313 | + # Subtract the state portion so tx_regular matches the header. |
| 314 | + tx_regular = ( |
| 315 | + intrinsic_gas |
| 316 | + + factory_code.gas_cost(fork) |
| 317 | + + init_code.gas_cost(fork) |
| 318 | + - total_state_refund |
| 319 | + ) |
| 320 | + |
| 321 | + tx = Transaction( |
| 322 | + to=factory, |
| 323 | + gas_limit=gas_limit_cap + total_state_refund, |
| 324 | + sender=pre.fund_eoa(), |
| 325 | + ) |
| 326 | + |
| 327 | + blockchain_test( |
| 328 | + pre=pre, |
| 329 | + blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_regular))], |
| 330 | + post={}, |
| 331 | + ) |
| 332 | + |
| 333 | + |
| 334 | +@pytest.mark.parametrize( |
| 335 | + "beneficiary_type,code_size", |
| 336 | + [ |
| 337 | + pytest.param("self", 2, id="self_tiny"), |
| 338 | + pytest.param("self", 100, id="self_medium"), |
| 339 | + pytest.param("external", 100, id="external_medium"), |
| 340 | + ], |
| 341 | +) |
| 342 | +@pytest.mark.valid_from("EIP8037") |
| 343 | +def test_create_selfdestruct_refunds_code_deposit_state_gas( |
| 344 | + blockchain_test: BlockchainTestFiller, |
| 345 | + pre: Alloc, |
| 346 | + fork: Fork, |
| 347 | + code_size: int, |
| 348 | + beneficiary_type: str, |
| 349 | +) -> None: |
| 350 | + """ |
| 351 | + Verify same tx CREATE+SELFDESTRUCT refunds code deposit state gas. |
| 352 | +
|
| 353 | + Factory CREATEs a contract deploying `code_size` bytes of code |
| 354 | + then CALLs it to trigger SELFDESTRUCT. Refund is account plus |
| 355 | + `code_size * cost_per_state_byte`. `external` beneficiary tests |
| 356 | + that the refund applies to the created account, not the |
| 357 | + destination of the ETH transfer. |
| 358 | + """ |
| 359 | + assert code_size >= 2 |
| 360 | + gas_limit_cap = fork.transaction_gas_limit_cap() |
| 361 | + assert gas_limit_cap is not None |
| 362 | + new_account_state_gas = fork.gas_costs().GAS_NEW_ACCOUNT |
| 363 | + code_deposit_state_gas = fork.code_deposit_state_gas(code_size=code_size) |
| 364 | + |
| 365 | + if beneficiary_type == "self": |
| 366 | + selfdestruct = Op.SELFDESTRUCT(Op.ADDRESS) |
| 367 | + else: |
| 368 | + beneficiary = pre.deploy_contract(code=Op.STOP) |
| 369 | + selfdestruct = Op.SELFDESTRUCT(beneficiary) |
| 370 | + sd_len = len(bytes(selfdestruct)) |
| 371 | + assert code_size >= sd_len |
| 372 | + deployed = bytes(selfdestruct) + b"\x00" * (code_size - sd_len) |
| 373 | + initcode = Initcode(deploy_code=deployed) |
| 374 | + initcode_len = len(initcode) |
| 375 | + |
| 376 | + # Nest CREATE directly as the address argument to CALL so the |
| 377 | + # deployed contract's address flows via the stack, avoiding a |
| 378 | + # magic memory slot for address storage and an arbitrary gas |
| 379 | + # budget. |
| 380 | + factory_code = Op.CALLDATACOPY( |
| 381 | + 0, |
| 382 | + 0, |
| 383 | + Op.CALLDATASIZE, |
| 384 | + data_size=initcode_len, |
| 385 | + new_memory_size=initcode_len, |
| 386 | + ) + Op.POP( |
| 387 | + Op.CALL( |
| 388 | + gas=Op.GAS, |
| 389 | + address=Op.CREATE( |
| 390 | + value=0, |
| 391 | + offset=0, |
| 392 | + size=Op.CALLDATASIZE, |
| 393 | + init_code_size=initcode_len, |
| 394 | + ), |
| 395 | + ) |
| 396 | + ) |
| 397 | + factory = pre.deploy_contract(code=factory_code) |
| 398 | + created_address = compute_create_address(address=factory, nonce=1) |
| 399 | + |
| 400 | + total_state_refund = new_account_state_gas + code_deposit_state_gas |
| 401 | + tx = Transaction( |
| 402 | + to=factory, |
| 403 | + data=bytes(initcode), |
| 404 | + gas_limit=gas_limit_cap + total_state_refund, |
| 405 | + sender=pre.fund_eoa(), |
| 406 | + ) |
| 407 | + |
| 408 | + blockchain_test( |
| 409 | + pre=pre, |
| 410 | + blocks=[Block(txs=[tx])], |
| 411 | + post={created_address: Account.NONEXISTENT}, |
| 412 | + ) |
| 413 | + |
| 414 | + |
| 415 | +@pytest.mark.valid_from("EIP8037") |
| 416 | +def test_create_selfdestruct_no_double_refund_with_sstore_restoration( |
| 417 | + blockchain_test: BlockchainTestFiller, |
| 418 | + pre: Alloc, |
| 419 | + fork: Fork, |
| 420 | +) -> None: |
| 421 | + """ |
| 422 | + Verify SSTORE restoration and SELFDESTRUCT refunds do not stack. |
| 423 | +
|
| 424 | + Initcode does SSTORE(0, 1) then SSTORE(0, 0) then SELFDESTRUCT. |
| 425 | + The 0 to x to 0 restoration refunds the slot inline. The end of |
| 426 | + tx selfdestruct refund scans `storage_writes[B]` and only counts |
| 427 | + non zero final values, so the restored slot is excluded and the |
| 428 | + end of tx refund is account only. |
| 429 | + """ |
| 430 | + gas_limit_cap = fork.transaction_gas_limit_cap() |
| 431 | + assert gas_limit_cap is not None |
| 432 | + new_account_state_gas = fork.gas_costs().GAS_NEW_ACCOUNT |
| 433 | + sstore_state_gas = fork.sstore_state_gas() |
| 434 | + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() |
| 435 | + |
| 436 | + init_code = ( |
| 437 | + Op.SSTORE.with_metadata( |
| 438 | + key_warm=False, |
| 439 | + original_value=0, |
| 440 | + current_value=0, |
| 441 | + new_value=1, |
| 442 | + )(0, 1) |
| 443 | + + Op.SSTORE.with_metadata( |
| 444 | + key_warm=True, |
| 445 | + original_value=0, |
| 446 | + current_value=1, |
| 447 | + new_value=0, |
| 448 | + )(0, 0) |
| 449 | + + Op.SELFDESTRUCT.with_metadata(address_warm=True)(Op.ADDRESS) |
| 450 | + ) |
| 451 | + mstore_value, size = init_code_at_high_bytes(init_code) |
| 452 | + |
| 453 | + mstore = Op.MSTORE.with_metadata(new_memory_size=32, old_memory_size=0)( |
| 454 | + 0, mstore_value |
| 455 | + ) |
| 456 | + create_call = Op.CREATE.with_metadata(init_code_size=size)(0, 0, size) |
| 457 | + factory_code = mstore + Op.POP(create_call) |
| 458 | + factory = pre.deploy_contract(code=factory_code) |
| 459 | + |
| 460 | + # Subtract both state charges (CREATE account + cold SSTORE) to |
| 461 | + # isolate the regular total. |
| 462 | + tx_regular = ( |
| 463 | + intrinsic_gas |
| 464 | + + factory_code.gas_cost(fork) |
| 465 | + + init_code.gas_cost(fork) |
| 466 | + - new_account_state_gas |
| 467 | + - sstore_state_gas |
| 468 | + ) |
| 469 | + |
| 470 | + tx = Transaction( |
| 471 | + to=factory, |
| 472 | + gas_limit=gas_limit_cap + new_account_state_gas + sstore_state_gas, |
| 473 | + sender=pre.fund_eoa(), |
| 474 | + ) |
| 475 | + |
| 476 | + blockchain_test( |
| 477 | + pre=pre, |
| 478 | + blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_regular))], |
| 479 | + post={}, |
| 480 | + ) |
| 481 | + |
| 482 | + |
| 483 | +@pytest.mark.valid_from("EIP8037") |
| 484 | +def test_selfdestruct_pre_existing_account_no_refund( |
| 485 | + blockchain_test: BlockchainTestFiller, |
| 486 | + pre: Alloc, |
| 487 | + fork: Fork, |
| 488 | +) -> None: |
| 489 | + """ |
| 490 | + Verify SELFDESTRUCT of a pre-existing account earns no refund. |
| 491 | +
|
| 492 | + The same-tx-create guard (`address in tx_state.created_accounts`) |
| 493 | + is load-bearing: without it, destroying any account would leak |
| 494 | + state gas back into the reservoir. A contract deployed in `pre` |
| 495 | + is destroyed by the tx; `accounts_to_delete` contains it but |
| 496 | + `created_accounts` does not, so no refund is applied. The block |
| 497 | + header `gas_used` reflects the full regular-gas tx cost (no |
| 498 | + state-gas refund offset). |
| 499 | + """ |
| 500 | + gas_limit_cap = fork.transaction_gas_limit_cap() |
| 501 | + assert gas_limit_cap is not None |
| 502 | + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() |
| 503 | + |
| 504 | + # Victim deployed in `pre` (NOT same-tx-created). SELFDESTRUCTs |
| 505 | + # to self so no new-account state gas is charged to the tx. |
| 506 | + victim_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)(Op.ADDRESS) |
| 507 | + victim = pre.deploy_contract(code=victim_code) |
| 508 | + |
| 509 | + caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=victim)) |
| 510 | + caller = pre.deploy_contract(code=caller_code) |
| 511 | + |
| 512 | + # No refund offset: both caller_code and victim_code are pure |
| 513 | + # regular gas (SELFDESTRUCT to self, no value-to-new-account). |
| 514 | + tx_regular = ( |
| 515 | + intrinsic_gas + caller_code.gas_cost(fork) + victim_code.gas_cost(fork) |
| 516 | + ) |
| 517 | + |
| 518 | + tx = Transaction( |
| 519 | + to=caller, |
| 520 | + gas_limit=gas_limit_cap, |
| 521 | + sender=pre.fund_eoa(), |
| 522 | + ) |
| 523 | + |
| 524 | + # Per EIP-6780, SELFDESTRUCT on a not-same-tx-created account |
| 525 | + # does not delete it — the account still exists after the tx. |
| 526 | + blockchain_test( |
| 527 | + pre=pre, |
| 528 | + blocks=[Block(txs=[tx], header_verify=Header(gas_used=tx_regular))], |
| 529 | + post={victim: Account(code=victim_code)}, |
| 530 | + ) |
0 commit comments