Skip to content

Commit fff84d0

Browse files
committed
fix(spec-specs): Check delegation access gas before reading
1 parent 44171cc commit fff84d0

File tree

3 files changed

+158
-83
lines changed

3 files changed

+158
-83
lines changed

src/ethereum/forks/amsterdam/vm/eoa_delegation.py

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,15 @@ def recover_authority(authorization: Authorization) -> Address:
116116
return Address(keccak256(public_key)[12:32])
117117

118118

119-
def check_delegation(
119+
def calculate_delegation_cost(
120120
evm: Evm, address: Address
121-
) -> Tuple[bool, Address, Address, Bytes, Uint]:
121+
) -> Tuple[bool, Address, Optional[Address], Uint]:
122122
"""
123-
Check delegation info without modifying state or tracking.
123+
Check if address has delegation and calculate delegation target gas cost.
124+
125+
This function reads the original account's code to check for delegation
126+
and tracks it in state_changes. It calculates the delegation target's
127+
gas cost but does NOT read the delegation target yet.
124128
125129
Parameters
126130
----------
@@ -131,77 +135,65 @@ def check_delegation(
131135
132136
Returns
133137
-------
134-
delegation : `Tuple[bool, Address, Address, Bytes, Uint]`
135-
(is_delegated, original_address, final_address, code,
136-
additional_gas_cost)
138+
delegation_info : `Tuple[bool, Address, Optional[Address], Uint]`
139+
(is_delegated, original_address, delegated_address_or_none,
140+
delegation_gas_cost)
137141
138142
"""
139143
state = evm.message.block_env.state
140144

145+
# Read original account's code, already checked gas for this
141146
code = get_account(state, address).code
147+
track_address(evm.state_changes, address)
148+
142149
if not is_valid_delegation(code):
143-
return False, address, address, code, Uint(0)
150+
return False, address, None, Uint(0)
144151

145152
delegated_address = Address(code[EOA_DELEGATION_MARKER_LENGTH:])
146153

154+
# Calculate gas cost for delegation target access
147155
if delegated_address in evm.accessed_addresses:
148-
additional_gas_cost = GAS_WARM_ACCESS
156+
delegation_gas_cost = GAS_WARM_ACCESS
149157
else:
150-
additional_gas_cost = GAS_COLD_ACCOUNT_ACCESS
158+
delegation_gas_cost = GAS_COLD_ACCOUNT_ACCESS
151159

152-
delegated_code = get_account(state, delegated_address).code
160+
return True, address, delegated_address, delegation_gas_cost
153161

154-
return (
155-
True,
156-
address,
157-
delegated_address,
158-
delegated_code,
159-
additional_gas_cost,
160-
)
161162

162-
163-
def apply_delegation_tracking(
164-
evm: Evm, original_address: Address, delegated_address: Address
165-
) -> None:
163+
def read_delegation_target(evm: Evm, delegated_address: Address) -> Bytes:
166164
"""
167-
Apply delegation tracking after gas check passes.
165+
Read the delegation target's code and track the access.
166+
167+
Should ONLY be called AFTER verifying we have gas for the access.
168+
169+
This function:
170+
1. Reads the delegation target's code from state
171+
2. Adds it to accessed_addresses (if not already there)
172+
3. Tracks it in state_changes for BAL
168173
169174
Parameters
170175
----------
171176
evm : `Evm`
172177
The execution frame.
173-
original_address : `Address`
174-
The original address that was called.
175178
delegated_address : `Address`
176-
The address delegated to.
179+
The delegation target address.
180+
181+
Returns
182+
-------
183+
code : `Bytes`
184+
The delegation target's code.
177185
178186
"""
179-
track_address(evm.state_changes, original_address)
187+
state = evm.message.block_env.state
180188

189+
# Add to accessed addresses for warm/cold gas accounting
181190
if delegated_address not in evm.accessed_addresses:
182191
evm.accessed_addresses.add(delegated_address)
183192

193+
# Track the address for BAL
184194
track_address(evm.state_changes, delegated_address)
185195

186-
187-
def access_delegation(
188-
evm: Evm, address: Address
189-
) -> Tuple[bool, Address, Bytes, Uint]:
190-
"""
191-
Access delegation info and track state changes.
192-
193-
DEPRECATED: Use check_delegation and apply_delegation_tracking
194-
for proper gas check ordering.
195-
196-
"""
197-
is_delegated, orig_addr, final_addr, code, gas_cost = check_delegation(
198-
evm, address
199-
)
200-
201-
if is_delegated:
202-
apply_delegation_tracking(evm, orig_addr, final_addr)
203-
204-
return is_delegated, final_addr, code, gas_cost
196+
return get_account(state, delegated_address).code
205197

206198

207199
def set_delegation(message: Message) -> U256:

src/ethereum/forks/amsterdam/vm/instructions/system.py

Lines changed: 98 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
to_address_masked,
3535
)
3636
from ...vm.eoa_delegation import (
37-
apply_delegation_tracking,
38-
check_delegation,
37+
calculate_delegation_cost,
38+
read_delegation_target,
3939
)
4040
from .. import (
4141
Evm,
@@ -397,13 +397,36 @@ def call(evm: Evm) -> None:
397397
if is_cold_access:
398398
evm.accessed_addresses.add(to)
399399

400+
# Calculate base gas cost for accessing 'to' account
401+
base_gas_cost = extend_memory.cost + access_gas_cost
402+
403+
# Check gas for base access BEFORE reading 'to' account
404+
check_gas(evm, base_gas_cost)
405+
406+
# Now safe to read 'to' account's code to check for delegation
407+
# This reads 'to' account's code and tracks it, but does NOT
408+
# read the delegation target yet
400409
(
401410
is_delegated,
402411
original_address,
403-
final_address,
404-
code,
412+
delegated_address,
405413
delegation_gas_cost,
406-
) = check_delegation(evm, to)
414+
) = calculate_delegation_cost(evm, to)
415+
416+
# Check gas for delegation target access before reading it
417+
# Note: We haven't charged base_gas_cost yet (only checked it),
418+
# so we need to verify we have TOTAL gas (base + delegation)
419+
if is_delegated and delegation_gas_cost > Uint(0):
420+
check_gas(evm, base_gas_cost + delegation_gas_cost)
421+
422+
if is_delegated:
423+
assert delegated_address is not None
424+
code = read_delegation_target(evm, delegated_address)
425+
final_address = delegated_address
426+
else:
427+
code = get_account(evm.message.block_env.state, to).code
428+
final_address = to
429+
407430
access_gas_cost += delegation_gas_cost
408431

409432
code_address = final_address
@@ -421,12 +444,6 @@ def call(evm: Evm) -> None:
421444
access_gas_cost + create_gas_cost + transfer_gas_cost,
422445
)
423446

424-
check_gas(evm, message_call_gas.cost + extend_memory.cost)
425-
426-
track_address(evm.state_changes, to)
427-
if is_delegated:
428-
apply_delegation_tracking(evm, original_address, final_address)
429-
430447
charge_gas(evm, message_call_gas.cost + extend_memory.cost)
431448
if evm.message.is_static and value != U256(0):
432449
raise WriteInStaticContext
@@ -497,13 +514,33 @@ def callcode(evm: Evm) -> None:
497514
if is_cold_access:
498515
evm.accessed_addresses.add(code_address)
499516

517+
# Check we have enough gas for base access before reading state
518+
base_gas_cost = extend_memory.cost + access_gas_cost
519+
check_gas(evm, base_gas_cost)
520+
521+
# Check delegation cost without reading delegation target yet
500522
(
501523
is_delegated,
502524
original_address,
503-
final_address,
504-
code,
525+
delegated_address,
505526
delegation_gas_cost,
506-
) = check_delegation(evm, code_address)
527+
) = calculate_delegation_cost(evm, code_address)
528+
529+
# Check gas for delegation target access BEFORE reading it
530+
# Note: We haven't charged base_gas_cost yet (only checked it),
531+
# so we need to verify we have TOTAL gas (base + delegation)
532+
if is_delegated and delegation_gas_cost > Uint(0):
533+
check_gas(evm, base_gas_cost + delegation_gas_cost)
534+
535+
# Now safe to read delegation target since we verified gas
536+
if is_delegated:
537+
assert delegated_address is not None
538+
code = read_delegation_target(evm, delegated_address)
539+
final_address = delegated_address
540+
else:
541+
code = get_account(evm.message.block_env.state, code_address).code
542+
final_address = code_address
543+
507544
access_gas_cost += delegation_gas_cost
508545

509546
code_address = final_address
@@ -518,12 +555,6 @@ def callcode(evm: Evm) -> None:
518555
access_gas_cost + transfer_gas_cost,
519556
)
520557

521-
check_gas(evm, message_call_gas.cost + extend_memory.cost)
522-
523-
track_address(evm.state_changes, original_address)
524-
if is_delegated:
525-
apply_delegation_tracking(evm, original_address, final_address)
526-
527558
charge_gas(evm, message_call_gas.cost + extend_memory.cost)
528559

529560
# OPERATION
@@ -665,16 +696,37 @@ def delegatecall(evm: Evm) -> None:
665696
access_gas_cost = (
666697
GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS
667698
)
699+
700+
# Check we have enough gas for base access before reading state
701+
base_gas_cost = extend_memory.cost + access_gas_cost
702+
check_gas(evm, base_gas_cost)
703+
668704
if is_cold_access:
669705
evm.accessed_addresses.add(code_address)
670706

707+
# Check delegation cost without reading delegation target yet
671708
(
672709
is_delegated,
673710
original_address,
674-
final_address,
675-
code,
711+
delegated_address,
676712
delegation_gas_cost,
677-
) = check_delegation(evm, code_address)
713+
) = calculate_delegation_cost(evm, code_address)
714+
715+
# Check gas for delegation target access BEFORE reading it
716+
# Note: We haven't charged base_gas_cost yet (only checked it),
717+
# so we need to verify we have TOTAL gas (base + delegation)
718+
if is_delegated and delegation_gas_cost > Uint(0):
719+
check_gas(evm, base_gas_cost + delegation_gas_cost)
720+
721+
# Now safe to read delegation target since we verified gas
722+
if is_delegated:
723+
assert delegated_address is not None
724+
code = read_delegation_target(evm, delegated_address)
725+
final_address = delegated_address
726+
else:
727+
code = get_account(evm.message.block_env.state, code_address).code
728+
final_address = code_address
729+
678730
access_gas_cost += delegation_gas_cost
679731

680732
code_address = final_address
@@ -684,12 +736,6 @@ def delegatecall(evm: Evm) -> None:
684736
U256(0), gas, Uint(evm.gas_left), extend_memory.cost, access_gas_cost
685737
)
686738

687-
check_gas(evm, message_call_gas.cost + extend_memory.cost)
688-
689-
track_address(evm.state_changes, original_address)
690-
if is_delegated:
691-
apply_delegation_tracking(evm, original_address, final_address)
692-
693739
charge_gas(evm, message_call_gas.cost + extend_memory.cost)
694740

695741
# OPERATION
@@ -749,13 +795,33 @@ def staticcall(evm: Evm) -> None:
749795
if is_cold_access:
750796
evm.accessed_addresses.add(to)
751797

798+
# Check we have enough gas for base access before reading state
799+
base_gas_cost = extend_memory.cost + access_gas_cost
800+
check_gas(evm, base_gas_cost)
801+
802+
# Check delegation cost without reading delegation target yet
752803
(
753804
is_delegated,
754805
original_address,
755-
final_address,
756-
code,
806+
delegated_address,
757807
delegation_gas_cost,
758-
) = check_delegation(evm, to)
808+
) = calculate_delegation_cost(evm, to)
809+
810+
# Check gas for delegation target access BEFORE reading it
811+
# Note: We haven't charged base_gas_cost yet (only checked it),
812+
# so we need to verify we have TOTAL gas (base + delegation)
813+
if is_delegated and delegation_gas_cost > Uint(0):
814+
check_gas(evm, base_gas_cost + delegation_gas_cost)
815+
816+
# Now safe to read delegation target since we verified gas
817+
if is_delegated:
818+
assert delegated_address is not None
819+
code = read_delegation_target(evm, delegated_address)
820+
final_address = delegated_address
821+
else:
822+
code = get_account(evm.message.block_env.state, to).code
823+
final_address = to
824+
759825
access_gas_cost += delegation_gas_cost
760826

761827
code_address = final_address
@@ -769,12 +835,6 @@ def staticcall(evm: Evm) -> None:
769835
access_gas_cost,
770836
)
771837

772-
check_gas(evm, message_call_gas.cost + extend_memory.cost)
773-
774-
track_address(evm.state_changes, to)
775-
if is_delegated:
776-
apply_delegation_tracking(evm, original_address, final_address)
777-
778838
charge_gas(evm, message_call_gas.cost + extend_memory.cost)
779839

780840
# OPERATION

tests/prague/eip7702_set_code_tx/test_gas.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
Address,
1919
Alloc,
2020
AuthorizationTuple,
21+
BalAccountExpectation,
22+
BalNonceChange,
23+
BlockAccessListExpectation,
2124
Bytecode,
2225
Bytes,
2326
ChainConfig,
@@ -1269,6 +1272,25 @@ def test_call_to_pre_authorized_oog(
12691272
sender=pre.fund_eoa(),
12701273
)
12711274

1275+
expected_block_access_list = None
1276+
if fork.header_bal_hash_required():
1277+
# Sender nonce changes, callee is accessed but storage unchanged (OOG)
1278+
# auth_signer is tracked (we read its code to check delegation)
1279+
# delegation is NOT tracked (OOG before reading it)
1280+
account_expectations = {
1281+
tx.sender: BalAccountExpectation(
1282+
nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)],
1283+
),
1284+
callee_address: BalAccountExpectation.empty(),
1285+
# read for calculating delegation access cost:
1286+
auth_signer: BalAccountExpectation.empty(),
1287+
# OOG - not enough gas for delegation access:
1288+
delegation: None,
1289+
}
1290+
expected_block_access_list = BlockAccessListExpectation(
1291+
account_expectations=account_expectations
1292+
)
1293+
12721294
state_test(
12731295
pre=pre,
12741296
tx=tx,
@@ -1277,4 +1299,5 @@ def test_call_to_pre_authorized_oog(
12771299
auth_signer: Account(code=Spec.delegation_designation(delegation)),
12781300
delegation: Account(storage=Storage()),
12791301
},
1302+
expected_block_access_list=expected_block_access_list,
12801303
)

0 commit comments

Comments
 (0)