Skip to main content

fluentbase_types/
exit_code.rs

1use rwasm::TrapCode;
2use strum_macros::{Display, FromRepr};
3
4/// Exit codes representing various execution outcomes and error conditions.
5///
6/// This enum defines the possible exit codes that can be returned by the execution
7/// environment.
8///
9/// The codes are grouped into several categories:
10/// - Basic status codes (0 to -2)
11/// - Fluentbase-specific error codes (-1000 and below)
12/// - Trap error codes (-2000 and below)
13///
14/// **Note**: Exit codes cannot be positive, as positive values are used to represent
15/// call indices in interrupted executions.
16///
17/// Exit codes are used to represent the outcome of execution across different
18/// environments.
19/// This makes their interpretation somewhat nuanced.
20///
21/// Within applications, developers can use any exit code, but there are conventions:
22/// 1. `Ok` (0) — Indicates successful execution.
23/// 2. `Panic` (-1) — Controlled application reverts (intended error, no gas penalty).
24/// 3. Any other code is treated as an error with a gas penalty and is mapped to `Err` (-2).
25///
26/// Technically, developers *can* return trap codes, but it's generally pointless:
27/// they are replaced by the `Err` (-2) code and still incur gas penalties.
28/// If the error code cannot be determined, it is mapped to `UnknownError` (-1006),
29/// which also results in a gas fine.
30///
31/// The SDK provides helper functions such as `evm_exit` and `evm_panic` to simplify
32/// returning error codes.
33/// These produce Solidity-compatible error outputs.
34///
35/// The `exit` function remains available to all developers.
36/// In some cases, an application may want to simulate trap behavior or explicitly exit with a gas
37/// penalty to comply with gas consumption standards (for instance, EVM runtime).
38///
39/// This behavior is similar to Solidity, where only three exit codes are supported:
40/// - For legacy contracts: `Ok = 1`, `Revert = 0`, `Err = 2`
41/// - For EOF contracts: `Ok = 0`, `Revert = 1`, `Err = 2`
42///
43/// ## Basic Status Codes
44/// * `Ok` (0) - Successful execution
45/// * `Panic` (-1) - Execution panic
46/// * `Err` (-2) - General error
47///
48/// ## Fluentbase Error Codes
49/// * `RootCallOnly` - Operation restricted to root-level calls
50/// * `MalformedBuiltinParams` - Invalid parameters passed to builtin function
51/// * `CallDepthOverflow` - Call stack depth exceeded limit
52/// * `NonNegativeExitCode` - Exit code must be negative
53/// * `UnknownError` - Unspecified error condition
54/// * `InputOutputOutOfBounds` - I/O operation exceeded bounds
55/// * `PrecompileError` - Error in precompiled contract execution
56///
57/// ## Trap Error Codes
58/// * `UnreachableCodeReached` - Execution reached unreachable code
59/// * `MemoryOutOfBounds` - Memory access violation
60/// * `TableOutOfBounds` - Table access violation
61/// * `IndirectCallToNull` - Attempted call to null function pointer
62/// * `IntegerDivisionByZero` - Division by zero
63/// * `IntegerOverflow` - Integer overflow occurred
64/// * `BadConversionToInteger` - Invalid integer conversion
65/// * `StackOverflow` - Stack limit exceeded
66/// * `BadSignature` - Invalid function signature
67/// * `OutOfFuel` - Insufficient gas/fuel for execution
68/// * `GrowthOperationLimited` - Growth operation exceeded limits
69/// * `UnresolvedFunction` - Function not found
70#[derive(Default, Debug, Copy, Clone, Hash, Eq, PartialEq, Display, FromRepr)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72#[repr(i32)]
73pub enum ExitCode {
74    /* Basic Error Codes */
75    /// Execution is finished without errors
76    #[default]
77    Ok = 0,
78    /// Panic is produced by a program (aka revert)
79    Panic = -1,
80    /// An interruption created by runtime (only for system contracts)
81    InterruptionCalled = -3,
82
83    /* Fluentbase Runtime Error Codes */
84    /// Function can only be invoked as the root entry call
85    RootCallOnly = -1002,
86    /// Builtin function received malformed or invalid parameters
87    MalformedBuiltinParams = -1003,
88    /// Exceeded maximum allowed call stack depth
89    CallDepthOverflow = -1004,
90    /// Exit code must be non-negative, but a negative value was used
91    NonNegativeExitCode = -1005,
92    /// Generic catch-all error for unknown failures
93    UnknownError = -1006,
94    /// I/O operation tried to read/write outside allowed buffer bounds
95    InputOutputOutOfBounds = -1007,
96    /// An error happens inside a precompiled contract
97    PrecompileError = -1008,
98    /// Passed bytecode into executor is not supported
99    NotSupportedBytecode = -1009,
100    /// State changed inside immutable call (static=true)
101    StateChangeDuringStaticCall = -1010,
102    /// Create a contract size limit reached (limit depends on the application type)
103    CreateContractSizeLimit = -1011,
104    /// There is a collision on the contract creation (the same address is derived)
105    CreateContractCollision = -1012,
106    /// Created contract starts with invalid bytes (`0xEF`).
107    CreateContractStartingWithEF = -1013,
108    /// A program runs out of memory (max memory pages reached)
109    OutOfMemory = -1014,
110
111    /* Trap Error Codes */
112    /// Execution reached a code path marked as unreachable
113    UnreachableCodeReached = -2001,
114    /// Memory access outside the allocated memory range
115    MemoryOutOfBounds = -2002,
116    /// Table index access outside the allocated table range
117    TableOutOfBounds = -2003,
118    /// Indirect function call attempted with a null function reference
119    IndirectCallToNull = -2004,
120    /// Division or remainder by zero occurred
121    IntegerDivisionByZero = -2005,
122    /// Integer arithmetic operation overflowed the allowed range
123    IntegerOverflow = -2006,
124    /// Invalid conversion to integer (e.g., from NaN or out-of-range value)
125    BadConversionToInteger = -2007,
126    /// Stack reached its limit (overflow or underflow)
127    StackOverflow = -2008,
128    /// Function signature mismatch in a call
129    BadSignature = -2009,
130    /// Execution ran out of allocated fuel/gas
131    OutOfFuel = -2010,
132    /// Call an undefined or unregistered external function
133    UnknownExternalFunction = -2011,
134
135    /* System Error Codes */
136    /// An unexpected fatal execution failure (node should panic or terminate the execution)
137    UnexpectedFatalExecutionFailure = -3001,
138    /// Missing storage slot
139    MissingStorageSlot = -3002,
140}
141
142impl core::error::Error for ExitCode {}
143
144impl ExitCode {
145    /// Check if the exit code represents a trap code
146    pub fn is_trap_code(&self) -> bool {
147        self.into_i32() <= -2000 && self.into_i32() > -3000
148    }
149}
150
151pub trait UnwrapExitCode<T> {
152    fn unwrap_exit_code(self) -> T;
153}
154
155impl<T> UnwrapExitCode<T> for Result<T, ExitCode> {
156    fn unwrap_exit_code(self) -> T {
157        match self {
158            Ok(res) => res,
159            Err(err) => panic!("exit code: {} ({})", err, err.into_i32()),
160        }
161    }
162}
163
164impl From<i32> for ExitCode {
165    fn from(value: i32) -> Self {
166        Self::from_repr(value).unwrap_or(ExitCode::UnknownError)
167    }
168}
169
170impl From<ExitCode> for i32 {
171    fn from(value: ExitCode) -> Self {
172        value as i32
173    }
174}
175
176impl ExitCode {
177    pub fn is_ok(&self) -> bool {
178        self == &Self::Ok
179    }
180
181    /// Returns whether the result is a revert.
182    pub fn is_revert(&self) -> bool {
183        self == &Self::Panic
184    }
185
186    pub fn is_error(&self) -> bool {
187        !self.is_ok() && !self.is_revert()
188    }
189
190    /// Check if the exit code represents a fatal error
191    ///
192    /// Fatal exit code means that the application terminated unexpectedly, for example,
193    /// by having out of fuel or memory out of bounds. It means that we can't handle the output
194    /// state of this contract gracefully.
195    pub fn is_fatal_exit_code(&self) -> bool {
196        self == &ExitCode::UnexpectedFatalExecutionFailure
197    }
198
199    pub const fn into_i32(self) -> i32 {
200        self as i32
201    }
202}
203
204impl From<TrapCode> for ExitCode {
205    fn from(value: TrapCode) -> Self {
206        Self::from(&value)
207    }
208}
209
210impl From<&TrapCode> for ExitCode {
211    fn from(value: &TrapCode) -> Self {
212        match value {
213            TrapCode::UnreachableCodeReached => ExitCode::UnreachableCodeReached,
214            TrapCode::MemoryOutOfBounds => ExitCode::MemoryOutOfBounds,
215            TrapCode::TableOutOfBounds => ExitCode::TableOutOfBounds,
216            TrapCode::IndirectCallToNull => ExitCode::IndirectCallToNull,
217            TrapCode::IntegerDivisionByZero => ExitCode::IntegerDivisionByZero,
218            TrapCode::IntegerOverflow => ExitCode::IntegerOverflow,
219            TrapCode::BadConversionToInteger => ExitCode::BadConversionToInteger,
220            TrapCode::StackOverflow => ExitCode::StackOverflow,
221            TrapCode::BadSignature => ExitCode::BadSignature,
222            TrapCode::OutOfFuel => ExitCode::OutOfFuel,
223            TrapCode::UnknownExternalFunction => ExitCode::UnknownExternalFunction,
224            TrapCode::InterruptionCalled => ExitCode::InterruptionCalled,
225            _ => {
226                #[cfg(feature = "std")]
227                eprintln!("WARN: unknown trap code: {:?}", value);
228                ExitCode::UnknownError
229            }
230        }
231    }
232}
233
234pub type EvmExitCode = u32;