neo_solidity/runtime/execution/helpers/arithmetic/
basic_ops.rs

1/// Arithmetic operation helpers with overflow/underflow checking.
2///
3/// All arithmetic operations follow the same pattern:
4/// 1. Determine if unsigned arithmetic should be used
5/// 2. Coerce both operands to appropriate type (i64/u64)
6/// 3. Perform operation with overflow checking (if strict_arithmetic is enabled)
7/// 4. Return result as StackItem
8///
9/// The macros below generate the repetitive boilerplate for each operation.
10/// Macro to generate arithmetic operation functions (ADD, SUB, MUL).
11///
12/// # Syntax
13/// ```ignore
14/// arithmetic_op!(fn_name, op_name, op_sym, checked_fn, wrapping_fn, error_kind);
15/// ```
16///
17/// # Example
18/// ```ignore
19/// arithmetic_op!(add_stack_items, "ADD", "+", checked_add, wrapping_add, "overflow");
20/// ```
21macro_rules! arithmetic_op {
22    ($fn_name:ident, $op_name:literal, $op_sym:literal, $checked:ident, $wrapping:ident, $error_kind:literal) => {
23        fn $fn_name(&self, a: StackItem, b: StackItem) -> Result<StackItem, RuntimeError> {
24            let use_unsigned =
25                matches!(a, StackItem::UnsignedInteger(_)) || matches!(b, StackItem::UnsignedInteger(_));
26
27            if use_unsigned {
28                let x = self.coerce_item_to_u64(&a).ok_or_else(|| RuntimeError::ExecutionError {
29                    message: concat!("Invalid operands for ", $op_name).to_string(),
30                })?;
31                let y = self.coerce_item_to_u64(&b).ok_or_else(|| RuntimeError::ExecutionError {
32                    message: concat!("Invalid operands for ", $op_name).to_string(),
33                })?;
34                if self.strict_arithmetic {
35                    x.$checked(y)
36                        .map(StackItem::UnsignedInteger)
37                        .ok_or_else(|| RuntimeError::ExecutionError {
38                            message: format!(
39                                "Unsigned integer {} in {}: {} {} {}",
40                                $error_kind, $op_name, x, $op_sym, y
41                            ),
42                        })
43                } else {
44                    Ok(StackItem::UnsignedInteger(x.$wrapping(y)))
45                }
46            } else {
47                let x = self.coerce_item_to_i64(&a).ok_or_else(|| RuntimeError::ExecutionError {
48                    message: concat!("Invalid operands for ", $op_name).to_string(),
49                })?;
50                let y = self.coerce_item_to_i64(&b).ok_or_else(|| RuntimeError::ExecutionError {
51                    message: concat!("Invalid operands for ", $op_name).to_string(),
52                })?;
53                if self.strict_arithmetic {
54                    x.$checked(y)
55                        .map(StackItem::Integer)
56                        .ok_or_else(|| RuntimeError::ExecutionError {
57                            message: format!(
58                                "Integer {} in {}: {} {} {}",
59                                $error_kind, $op_name, x, $op_sym, y
60                            ),
61                        })
62                } else {
63                    Ok(StackItem::Integer(x.$wrapping(y)))
64                }
65            }
66        }
67    };
68}
69
70/// Macro to generate division/modulo operation functions (DIV, MOD).
71///
72/// Checks for division by zero AND the signed overflow case `i64::MIN / -1`
73/// (which panics in Rust debug mode and wraps in release mode).
74macro_rules! divmod_op {
75    ($fn_name:ident, $op_name:literal, $checked_fn:ident, $error_msg:literal) => {
76        fn $fn_name(&self, a: StackItem, b: StackItem) -> Result<StackItem, RuntimeError> {
77            let use_unsigned =
78                matches!(a, StackItem::UnsignedInteger(_)) || matches!(b, StackItem::UnsignedInteger(_));
79
80            if use_unsigned {
81                let x = self.coerce_item_to_u64(&a).ok_or_else(|| RuntimeError::ExecutionError {
82                    message: concat!("Invalid operands for ", $op_name).to_string(),
83                })?;
84                let y = self.coerce_item_to_u64(&b).ok_or_else(|| RuntimeError::ExecutionError {
85                    message: concat!("Invalid operands for ", $op_name).to_string(),
86                })?;
87                if y == 0 {
88                    return Err(RuntimeError::ExecutionError {
89                        message: $error_msg.to_string(),
90                    });
91                }
92                Ok(StackItem::UnsignedInteger(x.$checked_fn(y).unwrap()))
93            } else {
94                let x = self.coerce_item_to_i64(&a).ok_or_else(|| RuntimeError::ExecutionError {
95                    message: concat!("Invalid operands for ", $op_name).to_string(),
96                })?;
97                let y = self.coerce_item_to_i64(&b).ok_or_else(|| RuntimeError::ExecutionError {
98                    message: concat!("Invalid operands for ", $op_name).to_string(),
99                })?;
100                if y == 0 {
101                    return Err(RuntimeError::ExecutionError {
102                        message: $error_msg.to_string(),
103                    });
104                }
105                x.$checked_fn(y)
106                    .map(StackItem::Integer)
107                    .ok_or_else(|| RuntimeError::ExecutionError {
108                        message: format!(
109                            "Signed integer overflow in {}: {} / -1",
110                            $op_name, x
111                        ),
112                    })
113            }
114        }
115    };
116}
117
118impl ExecutionContext {
119    fn coerce_item_to_i64(&self, item: &StackItem) -> Option<i64> {
120        match item {
121            StackItem::Integer(value) => Some(*value),
122            StackItem::UnsignedInteger(value) => i64::try_from(*value).ok(),
123            StackItem::Boolean(value) => Some(if *value { 1 } else { 0 }),
124            StackItem::Null => Some(0),
125            StackItem::ByteArray(bytes) => {
126                let bytes = bytes.borrow();
127                if bytes.is_empty() {
128                    return Some(0);
129                }
130                // NeoVM integers are arbitrary-precision little-endian byte arrays.
131                // This runtime uses i64 internally; truncation to the low 8 bytes
132                // is intentional — the compiler emits masking ops (AND, SHL, etc.)
133                // to handle width reduction at the Solidity level.
134                let len = bytes.len().min(8);
135                let mut buf = [0u8; 8];
136                buf[..len].copy_from_slice(&bytes[..len]);
137                Some(i64::from_le_bytes(buf))
138            }
139            _ => None,
140        }
141    }
142
143    fn coerce_item_to_u64(&self, item: &StackItem) -> Option<u64> {
144        match item {
145            StackItem::UnsignedInteger(value) => Some(*value),
146            StackItem::Integer(value) => {
147                if *value < 0 {
148                    None
149                } else {
150                    Some(*value as u64)
151                }
152            }
153            StackItem::Boolean(value) => Some(if *value { 1 } else { 0 }),
154            StackItem::Null => Some(0),
155            StackItem::ByteArray(bytes) => {
156                let bytes = bytes.borrow();
157                if bytes.is_empty() {
158                    return Some(0);
159                }
160                // See coerce_item_to_i64 comment — truncation to low 8 bytes
161                // is the correct NeoVM runtime behavior.
162                let len = bytes.len().min(8);
163                let mut buf = [0u8; 8];
164                buf[..len].copy_from_slice(&bytes[..len]);
165                Some(u64::from_le_bytes(buf))
166            }
167            _ => None,
168        }
169    }
170
171    // Generate all arithmetic operations using the macros
172    arithmetic_op!(add_stack_items, "ADD", "+", checked_add, wrapping_add, "overflow");
173    arithmetic_op!(sub_stack_items, "SUB", "-", checked_sub, wrapping_sub, "underflow");
174    arithmetic_op!(mul_stack_items, "MUL", "*", checked_mul, wrapping_mul, "overflow");
175    divmod_op!(div_stack_items, "DIV", checked_div, "Division by zero");
176    divmod_op!(mod_stack_items, "MOD", checked_rem, "Modulo by zero");
177}