Skip to content

Commit c17d91b

Browse files
committed
fix(spec-specs): Check delegation access gas before reading
1 parent 1ce4750 commit c17d91b

File tree

3 files changed

+144
-83
lines changed

3 files changed

+144
-83
lines changed

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

Lines changed: 36 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,64 @@ 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

141145
code = get_account(state, address).code
146+
track_address(evm.state_changes, address)
147+
142148
if not is_valid_delegation(code):
143-
return False, address, address, code, Uint(0)
149+
return False, address, None, Uint(0)
144150

145151
delegated_address = Address(code[EOA_DELEGATION_MARKER_LENGTH:])
146152

153+
# Calculate gas cost for delegation target access
147154
if delegated_address in evm.accessed_addresses:
148-
additional_gas_cost = GAS_WARM_ACCESS
155+
delegation_gas_cost = GAS_WARM_ACCESS
149156
else:
150-
additional_gas_cost = GAS_COLD_ACCOUNT_ACCESS
157+
delegation_gas_cost = GAS_COLD_ACCOUNT_ACCESS
151158

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

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

162-
163-
def apply_delegation_tracking(
164-
evm: Evm, original_address: Address, delegated_address: Address
165-
) -> None:
162+
def read_delegation_target(evm: Evm, delegated_address: Address) -> Bytes:
166163
"""
167-
Apply delegation tracking after gas check passes.
164+
Read the delegation target's code and track the access.
165+
166+
Should ONLY be called AFTER verifying we have gas for the access.
167+
168+
This function:
169+
1. Reads the delegation target's code from state
170+
2. Adds it to accessed_addresses (if not already there)
171+
3. Tracks it in state_changes for BAL
168172
169173
Parameters
170174
----------
171175
evm : `Evm`
172176
The execution frame.
173-
original_address : `Address`
174-
The original address that was called.
175177
delegated_address : `Address`
176-
The address delegated to.
178+
The delegation target address.
179+
180+
Returns
181+
-------
182+
code : `Bytes`
183+
The delegation target's code.
177184
178185
"""
179-
track_address(evm.state_changes, original_address)
186+
state = evm.message.block_env.state
180187

188+
# Add to accessed addresses for warm/cold gas accounting
181189
if delegated_address not in evm.accessed_addresses:
182190
evm.accessed_addresses.add(delegated_address)
183191

192+
# Track the address for BAL
184193
track_address(evm.state_changes, delegated_address)
185194

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
195+
return get_account(state, delegated_address).code
205196

206197

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

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

Lines changed: 85 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,30 @@ def call(evm: Evm) -> None:
397397
if is_cold_access:
398398
evm.accessed_addresses.add(to)
399399

400+
# check gas for base access before reading `to` account
401+
base_gas_cost = extend_memory.cost + access_gas_cost
402+
check_gas(evm, base_gas_cost)
403+
404+
# read `to` account and assess delegation cost
400405
(
401406
is_delegated,
402407
original_address,
403-
final_address,
404-
code,
408+
delegated_address,
405409
delegation_gas_cost,
406-
) = check_delegation(evm, to)
410+
) = calculate_delegation_cost(evm, to)
411+
412+
# check gas again for delegation target access before reading it
413+
if is_delegated and delegation_gas_cost > Uint(0):
414+
check_gas(evm, base_gas_cost + delegation_gas_cost)
415+
416+
if is_delegated:
417+
assert delegated_address is not None
418+
code = read_delegation_target(evm, delegated_address)
419+
final_address = delegated_address
420+
else:
421+
code = get_account(evm.message.block_env.state, to).code
422+
final_address = to
423+
407424
access_gas_cost += delegation_gas_cost
408425

409426
code_address = final_address
@@ -421,12 +438,6 @@ def call(evm: Evm) -> None:
421438
access_gas_cost + create_gas_cost + transfer_gas_cost,
422439
)
423440

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-
430441
charge_gas(evm, message_call_gas.cost + extend_memory.cost)
431442
if evm.message.is_static and value != U256(0):
432443
raise WriteInStaticContext
@@ -497,13 +508,30 @@ def callcode(evm: Evm) -> None:
497508
if is_cold_access:
498509
evm.accessed_addresses.add(code_address)
499510

511+
# check gas for base access before reading `code_address` account
512+
base_gas_cost = extend_memory.cost + access_gas_cost
513+
check_gas(evm, base_gas_cost)
514+
515+
# read code_address account and assess delegation cost
500516
(
501517
is_delegated,
502518
original_address,
503-
final_address,
504-
code,
519+
delegated_address,
505520
delegation_gas_cost,
506-
) = check_delegation(evm, code_address)
521+
) = calculate_delegation_cost(evm, code_address)
522+
523+
# check gas again for delegation target access before reading it
524+
if is_delegated and delegation_gas_cost > Uint(0):
525+
check_gas(evm, base_gas_cost + delegation_gas_cost)
526+
527+
if is_delegated:
528+
assert delegated_address is not None
529+
code = read_delegation_target(evm, delegated_address)
530+
final_address = delegated_address
531+
else:
532+
code = get_account(evm.message.block_env.state, code_address).code
533+
final_address = code_address
534+
507535
access_gas_cost += delegation_gas_cost
508536

509537
code_address = final_address
@@ -518,12 +546,6 @@ def callcode(evm: Evm) -> None:
518546
access_gas_cost + transfer_gas_cost,
519547
)
520548

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-
527549
charge_gas(evm, message_call_gas.cost + extend_memory.cost)
528550

529551
# OPERATION
@@ -665,16 +687,35 @@ def delegatecall(evm: Evm) -> None:
665687
access_gas_cost = (
666688
GAS_COLD_ACCOUNT_ACCESS if is_cold_access else GAS_WARM_ACCESS
667689
)
690+
691+
# check gas for base access before reading `code_address` account
692+
base_gas_cost = extend_memory.cost + access_gas_cost
693+
check_gas(evm, base_gas_cost)
694+
668695
if is_cold_access:
669696
evm.accessed_addresses.add(code_address)
670697

698+
# read `code_address` account and assess delegation cost
671699
(
672700
is_delegated,
673701
original_address,
674-
final_address,
675-
code,
702+
delegated_address,
676703
delegation_gas_cost,
677-
) = check_delegation(evm, code_address)
704+
) = calculate_delegation_cost(evm, code_address)
705+
706+
# check gas again for delegation target access before reading it
707+
if is_delegated and delegation_gas_cost > Uint(0):
708+
check_gas(evm, base_gas_cost + delegation_gas_cost)
709+
710+
# Now safe to read delegation target since we verified gas
711+
if is_delegated:
712+
assert delegated_address is not None
713+
code = read_delegation_target(evm, delegated_address)
714+
final_address = delegated_address
715+
else:
716+
code = get_account(evm.message.block_env.state, code_address).code
717+
final_address = code_address
718+
678719
access_gas_cost += delegation_gas_cost
679720

680721
code_address = final_address
@@ -684,12 +725,6 @@ def delegatecall(evm: Evm) -> None:
684725
U256(0), gas, Uint(evm.gas_left), extend_memory.cost, access_gas_cost
685726
)
686727

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-
693728
charge_gas(evm, message_call_gas.cost + extend_memory.cost)
694729

695730
# OPERATION
@@ -749,13 +784,31 @@ def staticcall(evm: Evm) -> None:
749784
if is_cold_access:
750785
evm.accessed_addresses.add(to)
751786

787+
# check gas for base access before reading `to` account
788+
base_gas_cost = extend_memory.cost + access_gas_cost
789+
check_gas(evm, base_gas_cost)
790+
791+
# read `to` account and assess delegation cost
752792
(
753793
is_delegated,
754794
original_address,
755-
final_address,
756-
code,
795+
delegated_address,
757796
delegation_gas_cost,
758-
) = check_delegation(evm, to)
797+
) = calculate_delegation_cost(evm, to)
798+
799+
# check gas again for delegation target access before reading it
800+
if is_delegated and delegation_gas_cost > Uint(0):
801+
check_gas(evm, base_gas_cost + delegation_gas_cost)
802+
803+
# Now safe to read delegation target since we verified gas
804+
if is_delegated:
805+
assert delegated_address is not None
806+
code = read_delegation_target(evm, delegated_address)
807+
final_address = delegated_address
808+
else:
809+
code = get_account(evm.message.block_env.state, to).code
810+
final_address = to
811+
759812
access_gas_cost += delegation_gas_cost
760813

761814
code_address = final_address
@@ -769,12 +822,6 @@ def staticcall(evm: Evm) -> None:
769822
access_gas_cost,
770823
)
771824

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-
778825
charge_gas(evm, message_call_gas.cost + extend_memory.cost)
779826

780827
# 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)