Skip to main content

ethrex_levm/opcode_handlers/
exchange.rs

1//! # Stack exchange operations
2//!
3//! Includes the following opcodes:
4//!   - `SWAP1` to `SWAP16`
5
6use crate::{
7    constants::STACK_LIMIT,
8    errors::{ExceptionalHalt, OpcodeResult, VMError},
9    gas_cost,
10    opcode_handlers::OpcodeHandler,
11    vm::VM,
12};
13use std::mem;
14
15/// Implementation for the `SWAPn` opcodes.
16pub struct OpSwapHandler<const N: usize>;
17impl<const N: usize> OpcodeHandler for OpSwapHandler<N> {
18    #[inline(always)]
19    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
20        vm.current_call_frame
21            .increase_consumed_gas(gas_cost::SWAPN)?;
22
23        vm.current_call_frame.stack.swap::<N>()?;
24
25        Ok(OpcodeResult::Continue)
26    }
27}
28
29/// Implementation for the `SWAPN` opcode.
30pub struct OpSwapNHandler;
31impl OpcodeHandler for OpSwapNHandler {
32    #[inline(always)]
33    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
34        vm.current_call_frame
35            .increase_consumed_gas(gas_cost::SWAPN)?;
36
37        let relative_offset = vm
38            .current_call_frame
39            .bytecode
40            .dispatch_buf()
41            .get(vm.current_call_frame.pc)
42            .copied()
43            .unwrap_or_default();
44
45        // Remove offsets that break backwards compatibility, which are
46        //   - 0x5B, which corresponds to a JUMPDEST opcode.
47        //   - 0x5F to 0x7F, which corresponds to PUSHx opcodes.
48        //   - The extra 3 values (0x5C, 0x5D and 0x5E) are probably included to simplify decoding.
49        if (0x5B..0x80).contains(&relative_offset) {
50            return Err(ExceptionalHalt::InvalidOpcode.into());
51        }
52        let relative_offset = relative_offset.wrapping_add(145);
53
54        // Stack grows downwards, so we add the offset to get deeper elements
55        // SWAPN swaps top with the (n+1)th element where n = decoded relative_offset
56        // The (n+1)th element (1-indexed) is at array index offset + n
57        let absolute_offset = vm
58            .current_call_frame
59            .stack
60            .offset
61            .checked_add(usize::from(relative_offset))
62            .ok_or(ExceptionalHalt::StackUnderflow)?;
63
64        // Verify the offset is within stack bounds
65        if absolute_offset >= STACK_LIMIT {
66            return Err(ExceptionalHalt::StackUnderflow.into());
67        }
68
69        let top_offset = vm.current_call_frame.stack.offset;
70
71        #[expect(unsafe_code, reason = "bound already checked")]
72        unsafe {
73            let [x, y] = vm
74                .current_call_frame
75                .stack
76                .values
77                .get_disjoint_unchecked_mut([top_offset, absolute_offset]);
78            mem::swap(x, y);
79        }
80
81        vm.current_call_frame.pc = vm.current_call_frame.pc.wrapping_add(1);
82        Ok(OpcodeResult::Continue)
83    }
84}
85
86/// Implementation for the `EXCHANGE` opcode.
87pub struct OpExchangeHandler;
88impl OpcodeHandler for OpExchangeHandler {
89    #[inline(always)]
90    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
91        vm.current_call_frame
92            .increase_consumed_gas(gas_cost::EXCHANGE)?;
93
94        let relative_offset = vm
95            .current_call_frame
96            .bytecode
97            .dispatch_buf()
98            .get(vm.current_call_frame.pc)
99            .copied()
100            .unwrap_or_default();
101
102        // Remove offsets that break backwards compatibility, which are
103        //   - 0x5B, which corresponds to a JUMPDEST opcode.
104        //   - 0x5F to 0x7F, which corresponds to PUSHx opcodes.
105        //   - The extra 3 values (0x5C, 0x5D and 0x5E) are probably included to simplify decoding.
106        //
107        // This range is more restricted than the one in DUPN and SWAPN because this payload
108        // contains two values, and the decoded offsets would overlap. In other words, it avoids
109        // having two different EXCHANGE encodings for the exact same offsets.
110        if (0x52..0x80).contains(&relative_offset) {
111            return Err(ExceptionalHalt::InvalidOpcode.into());
112        }
113
114        let relative_offset = {
115            let byte = relative_offset ^ 0x8F;
116
117            let q = byte >> 4;
118            let r = byte & 0x0F;
119
120            #[expect(
121                clippy::arithmetic_side_effects,
122                reason = "ranges are limited, cannot overflow or underflow"
123            )]
124            if q < r {
125                (q + 1, r + 1)
126            } else {
127                (r + 1, 29 - q)
128            }
129        };
130
131        // Stack grows downwards, so we add the offsets to get deeper elements
132        let absolute_offset = {
133            let stack_offset = vm.current_call_frame.stack.offset;
134
135            let q = stack_offset
136                .checked_add(usize::from(relative_offset.0))
137                .ok_or(ExceptionalHalt::StackUnderflow)?;
138            let r = stack_offset
139                .checked_add(usize::from(relative_offset.1))
140                .ok_or(ExceptionalHalt::StackUnderflow)?;
141
142            // Verify both offsets are within stack bounds
143            if q >= STACK_LIMIT || r >= STACK_LIMIT {
144                return Err(ExceptionalHalt::StackUnderflow.into());
145            }
146
147            (q, r)
148        };
149
150        #[expect(unsafe_code, reason = "bound already checked")]
151        unsafe {
152            let [x, y] = vm
153                .current_call_frame
154                .stack
155                .values
156                .get_disjoint_unchecked_mut([absolute_offset.0, absolute_offset.1]);
157            mem::swap(x, y);
158        }
159
160        vm.current_call_frame.pc = vm.current_call_frame.pc.wrapping_add(1);
161        Ok(OpcodeResult::Continue)
162    }
163}