|
10 | 10 | Account, |
11 | 11 | Alloc, |
12 | 12 | Bytecode, |
| 13 | + EIPChecklist, |
| 14 | + Fork, |
13 | 15 | Op, |
14 | 16 | StateTestFiller, |
15 | 17 | Transaction, |
16 | 18 | ) |
17 | 19 |
|
18 | | -from .spec import decode_single, ref_spec_8024 |
| 20 | +from .spec import Spec, decode_single, ref_spec_8024 |
19 | 21 |
|
20 | 22 | REFERENCE_SPEC_GIT_PATH = ref_spec_8024.git_path |
21 | 23 | REFERENCE_SPEC_VERSION = ref_spec_8024.version |
@@ -214,6 +216,99 @@ def test_swapn_stack_underflow( |
214 | 216 | state_test(pre=pre, post=post, tx=tx) |
215 | 217 |
|
216 | 218 |
|
| 219 | +@EIPChecklist.Opcode.Test.GasUsage.Normal() |
| 220 | +@EIPChecklist.Opcode.Test.GasUsage.OutOfGasExecution() |
| 221 | +@EIPChecklist.Opcode.Test.GasUsage.ExtraGas() |
| 222 | +@pytest.mark.parametrize("gas_cost_delta", [-2, -1, 0, 1, 2]) |
| 223 | +def test_swapn_gas_cost_boundary( |
| 224 | + gas_cost_delta: int, |
| 225 | + pre: Alloc, |
| 226 | + fork: Fork, |
| 227 | + state_test: StateTestFiller, |
| 228 | +) -> None: |
| 229 | + """ |
| 230 | + Test SWAPN at the gas cost boundary. |
| 231 | +
|
| 232 | + SWAPN is invoked in a callee that receives exactly its execution cost |
| 233 | + plus `gas_cost_delta`. The caller records the CALL result: a negative |
| 234 | + delta starves SWAPN of its base gas (3) and the sub-call runs out of |
| 235 | + gas (result 0); a zero or positive delta succeeds (result 1). |
| 236 | + """ |
| 237 | + # SWAPN with decoded value n swaps position 1 with position (n+1), so |
| 238 | + # it needs stack_index + 1 items on the stack. |
| 239 | + stack_index = Spec.MIN_STACK_INDEX # 17 |
| 240 | + |
| 241 | + code = Bytecode() |
| 242 | + for i in range(stack_index + 1): |
| 243 | + code += Op.PUSH1(i) |
| 244 | + code += Op.SWAPN[stack_index] |
| 245 | + |
| 246 | + contract_address = pre.deploy_contract(code=code) |
| 247 | + |
| 248 | + call_code = Op.SSTORE( |
| 249 | + 0, |
| 250 | + Op.CALL( |
| 251 | + gas=code.gas_cost(fork) + gas_cost_delta, |
| 252 | + address=contract_address, |
| 253 | + ), |
| 254 | + ) |
| 255 | + call_address = pre.deploy_contract( |
| 256 | + code=call_code, |
| 257 | + storage={0: 0xDEADBEEF}, |
| 258 | + ) |
| 259 | + |
| 260 | + tx = Transaction(to=call_address, sender=pre.fund_eoa(), gas_limit=200_000) |
| 261 | + |
| 262 | + post = {call_address: Account(storage={0: 0 if gas_cost_delta < 0 else 1})} |
| 263 | + |
| 264 | + state_test(pre=pre, post=post, tx=tx) |
| 265 | + |
| 266 | + |
| 267 | +@pytest.mark.parametrize( |
| 268 | + "invalid_immediate", |
| 269 | + list(range(91, 128)), # 0x5b to 0x7f (JUMPDEST and PUSH opcodes) |
| 270 | + ids=lambda x: f"swapn_invalid_imm_0x{x:02x}", |
| 271 | +) |
| 272 | +def test_swapn_invalid_immediate_aborts( |
| 273 | + invalid_immediate: int, |
| 274 | + pre: Alloc, |
| 275 | + state_test: StateTestFiller, |
| 276 | +) -> None: |
| 277 | + """ |
| 278 | + Test SWAPN with invalid immediate values (90 < x < 128) aborts. |
| 279 | +
|
| 280 | + Per EIP-8024, immediate values in range [91, 127] (0x5b-0x7f) are |
| 281 | + invalid because they correspond to JUMPDEST (0x5b) and PUSH opcodes |
| 282 | + (0x60-0x7f). Attempting to execute SWAPN with these immediates should |
| 283 | + abort. |
| 284 | + """ |
| 285 | + sender = pre.fund_eoa() |
| 286 | + |
| 287 | + # Build a stack tall enough for any valid immediate (max decoded index |
| 288 | + # is 235, SWAPN needs index + 1 items) so the abort is caused by the |
| 289 | + # invalid immediate, never an underflow. |
| 290 | + code = Bytecode() |
| 291 | + for i in range(Spec.MAX_STACK_INDEX + 1): |
| 292 | + code += Op.PUSH1(i % 256) |
| 293 | + |
| 294 | + # Attempt SWAPN with invalid immediate - should abort. |
| 295 | + # Pass as bytes (raw immediate byte for testing invalid ranges). |
| 296 | + code += Op.SWAPN[invalid_immediate.to_bytes(1, "big")] |
| 297 | + |
| 298 | + # This should never execute. |
| 299 | + code += Op.PUSH1(0x42) + Op.PUSH1(0) + Op.SSTORE |
| 300 | + code += Op.STOP |
| 301 | + |
| 302 | + contract_address = pre.deploy_contract(code=code) |
| 303 | + |
| 304 | + tx = Transaction(to=contract_address, sender=sender, gas_limit=10_000_000) |
| 305 | + |
| 306 | + # Transaction should fail - invalid immediate causes abort. |
| 307 | + post = {contract_address: Account(storage={})} |
| 308 | + |
| 309 | + state_test(pre=pre, post=post, tx=tx) |
| 310 | + |
| 311 | + |
217 | 312 | def test_endofcode_behavior( |
218 | 313 | pre: Alloc, |
219 | 314 | state_test: StateTestFiller, |
|
0 commit comments