Skip to main content

hopper_native/
error.rs

1//! Program error type for Solana on-chain programs.
2//!
3//! Wire-compatible with pinocchio/solana-program ProgramError.
4//! Each variant maps to a fixed u64 error code returned to the runtime.
5
6/// Errors that a Solana program can return.
7///
8/// `#[repr(u64)]` with EXPLICIT discriminants is load-bearing for binary
9/// size: variant `k` (for the fieldless builtins, `k = 1..=24`) encodes to
10/// the runtime code `(k + 1) << 32`, so [`From<ProgramError> for u64`] is
11/// one tag read + one shift instead of a 25-arm match. The DWARF size
12/// attribution measured that match at 880 bytes, 13% of the parity
13/// vault's `.text`, because the error lowering inlines into every
14/// entrypoint. Keep new variants in this scheme: append with the next
15/// sequential discriminant and the conversion stays arm-free (the
16/// exhaustive `u64_conversion_matches_reference_for_every_variant` test
17/// enforces the correspondence).
18#[derive(Clone, Debug, Eq, PartialEq)]
19#[repr(u64)]
20pub enum ProgramError {
21    /// Custom program error with a u32 code.
22    Custom(u32) = 0,
23    InvalidArgument = 1,
24    InvalidInstructionData = 2,
25    InvalidAccountData = 3,
26    AccountDataTooSmall = 4,
27    InsufficientFunds = 5,
28    IncorrectProgramId = 6,
29    MissingRequiredSignature = 7,
30    AccountAlreadyInitialized = 8,
31    UninitializedAccount = 9,
32    NotEnoughAccountKeys = 10,
33    AccountBorrowFailed = 11,
34    MaxSeedLengthExceeded = 12,
35    InvalidSeeds = 13,
36    BorshIoError = 14,
37    AccountNotRentExempt = 15,
38    UnsupportedSysvar = 16,
39    IllegalOwner = 17,
40    MaxAccountsDataAllocationsExceeded = 18,
41    InvalidRealloc = 19,
42    MaxInstructionTraceLengthExceeded = 20,
43    BuiltinProgramsMustConsumeComputeUnits = 21,
44    InvalidAccountOwner = 22,
45    ArithmeticOverflow = 23,
46    Immutable = 24,
47    IncorrectAuthority = 25,
48}
49
50// ── u64 conversion (Solana runtime ABI) ──────────────────────────────
51
52impl From<ProgramError> for u64 {
53    #[inline]
54    fn from(err: ProgramError) -> u64 {
55        match err {
56            ProgramError::Custom(0) => CUSTOM_ZERO,
57            ProgramError::Custom(code) => code as u64,
58            builtin => {
59                // SAFETY: `ProgramError` is `#[repr(u64)]`, which
60                // guarantees the discriminant is stored as a leading
61                // `u64` tag readable through a pointer cast (RFC 2195
62                // primitive-representation layout). `builtin` is one of
63                // the fieldless variants (Custom was matched above), so
64                // its tag is the explicit discriminant `1..=25`.
65                let tag = unsafe { *(&builtin as *const ProgramError as *const u64) };
66                // Variant k encodes to (k + 1) << 32: InvalidArgument
67                // (tag 1) -> 2 << 32, ..., IncorrectAuthority (tag 25)
68                // -> 26 << 32, exactly the old per-arm to_builtin(k-1)
69                // table, without the 25-arm match in every entrypoint.
70                (tag + 1) << BUILTIN_BIT_SHIFT
71            }
72        }
73    }
74}
75
76impl From<u64> for ProgramError {
77    fn from(code: u64) -> Self {
78        if code == CUSTOM_ZERO {
79            return ProgramError::Custom(0);
80        }
81        let builtin = code >> BUILTIN_BIT_SHIFT;
82        if code & BUILTIN_LOW_MASK == 0 && builtin >= 2 {
83            match builtin - 2 {
84                0 => return ProgramError::InvalidArgument,
85                1 => return ProgramError::InvalidInstructionData,
86                2 => return ProgramError::InvalidAccountData,
87                3 => return ProgramError::AccountDataTooSmall,
88                4 => return ProgramError::InsufficientFunds,
89                5 => return ProgramError::IncorrectProgramId,
90                6 => return ProgramError::MissingRequiredSignature,
91                7 => return ProgramError::AccountAlreadyInitialized,
92                8 => return ProgramError::UninitializedAccount,
93                9 => return ProgramError::NotEnoughAccountKeys,
94                10 => return ProgramError::AccountBorrowFailed,
95                11 => return ProgramError::MaxSeedLengthExceeded,
96                12 => return ProgramError::InvalidSeeds,
97                13 => return ProgramError::BorshIoError,
98                14 => return ProgramError::AccountNotRentExempt,
99                15 => return ProgramError::UnsupportedSysvar,
100                16 => return ProgramError::IllegalOwner,
101                17 => return ProgramError::MaxAccountsDataAllocationsExceeded,
102                18 => return ProgramError::InvalidRealloc,
103                19 => return ProgramError::MaxInstructionTraceLengthExceeded,
104                20 => return ProgramError::BuiltinProgramsMustConsumeComputeUnits,
105                21 => return ProgramError::InvalidAccountOwner,
106                22 => return ProgramError::ArithmeticOverflow,
107                23 => return ProgramError::Immutable,
108                24 => return ProgramError::IncorrectAuthority,
109                _ => {}
110            }
111        }
112        ProgramError::Custom(code as u32)
113    }
114}
115
116/// Map a builtin error index to its runtime u64 code.
117///
118/// The Solana runtime uses a specific encoding for builtin errors:
119/// - `Custom(0)` occupies `1 << 32`
120/// - builtin errors start at `2 << 32`
121const BUILTIN_BIT_SHIFT: usize = 32;
122const CUSTOM_ZERO: u64 = 1_u64 << BUILTIN_BIT_SHIFT;
123const BUILTIN_LOW_MASK: u64 = (1_u64 << BUILTIN_BIT_SHIFT) - 1;
124
125/// Reference builtin encoding, kept ONLY as the test oracle: the
126/// shipped conversion reads the enum tag directly, and the golden
127/// tests below re-derive every variant's code through this original
128/// arithmetic to pin the two forever equal.
129#[cfg(test)]
130#[inline(always)]
131const fn to_builtin(index: u64) -> u64 {
132    (index + 2) << BUILTIN_BIT_SHIFT
133}
134
135impl core::fmt::Display for ProgramError {
136    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
137        match self {
138            ProgramError::Custom(code) => write!(f, "Custom({code})"),
139            ProgramError::InvalidArgument => write!(f, "InvalidArgument"),
140            ProgramError::InvalidInstructionData => write!(f, "InvalidInstructionData"),
141            ProgramError::InvalidAccountData => write!(f, "InvalidAccountData"),
142            ProgramError::AccountDataTooSmall => write!(f, "AccountDataTooSmall"),
143            ProgramError::InsufficientFunds => write!(f, "InsufficientFunds"),
144            ProgramError::IncorrectProgramId => write!(f, "IncorrectProgramId"),
145            ProgramError::MissingRequiredSignature => write!(f, "MissingRequiredSignature"),
146            ProgramError::AccountAlreadyInitialized => write!(f, "AccountAlreadyInitialized"),
147            ProgramError::UninitializedAccount => write!(f, "UninitializedAccount"),
148            ProgramError::NotEnoughAccountKeys => write!(f, "NotEnoughAccountKeys"),
149            ProgramError::AccountBorrowFailed => write!(f, "AccountBorrowFailed"),
150            ProgramError::MaxSeedLengthExceeded => write!(f, "MaxSeedLengthExceeded"),
151            ProgramError::InvalidSeeds => write!(f, "InvalidSeeds"),
152            ProgramError::BorshIoError => write!(f, "BorshIoError"),
153            ProgramError::AccountNotRentExempt => write!(f, "AccountNotRentExempt"),
154            ProgramError::UnsupportedSysvar => write!(f, "UnsupportedSysvar"),
155            ProgramError::IllegalOwner => write!(f, "IllegalOwner"),
156            ProgramError::MaxAccountsDataAllocationsExceeded => {
157                write!(f, "MaxAccountsDataAllocationsExceeded")
158            }
159            ProgramError::InvalidRealloc => write!(f, "InvalidRealloc"),
160            ProgramError::MaxInstructionTraceLengthExceeded => {
161                write!(f, "MaxInstructionTraceLengthExceeded")
162            }
163            ProgramError::BuiltinProgramsMustConsumeComputeUnits => {
164                write!(f, "BuiltinProgramsMustConsumeComputeUnits")
165            }
166            ProgramError::InvalidAccountOwner => write!(f, "InvalidAccountOwner"),
167            ProgramError::ArithmeticOverflow => write!(f, "ArithmeticOverflow"),
168            ProgramError::Immutable => write!(f, "Immutable"),
169            ProgramError::IncorrectAuthority => write!(f, "IncorrectAuthority"),
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    /// Every variant's u64 code, pinned against the pre-tag-read
179    /// reference table (the old 25-arm match, reproduced here verbatim).
180    /// The match is EXHAUSTIVE on purpose: adding a variant without
181    /// updating this test, and without giving it the next sequential
182    /// discriminant, must not compile.
183    #[test]
184    fn u64_conversion_matches_reference_for_every_variant() {
185        fn reference(err: &ProgramError) -> u64 {
186            match err {
187                ProgramError::Custom(0) => CUSTOM_ZERO,
188                ProgramError::Custom(code) => *code as u64,
189                ProgramError::InvalidArgument => to_builtin(0),
190                ProgramError::InvalidInstructionData => to_builtin(1),
191                ProgramError::InvalidAccountData => to_builtin(2),
192                ProgramError::AccountDataTooSmall => to_builtin(3),
193                ProgramError::InsufficientFunds => to_builtin(4),
194                ProgramError::IncorrectProgramId => to_builtin(5),
195                ProgramError::MissingRequiredSignature => to_builtin(6),
196                ProgramError::AccountAlreadyInitialized => to_builtin(7),
197                ProgramError::UninitializedAccount => to_builtin(8),
198                ProgramError::NotEnoughAccountKeys => to_builtin(9),
199                ProgramError::AccountBorrowFailed => to_builtin(10),
200                ProgramError::MaxSeedLengthExceeded => to_builtin(11),
201                ProgramError::InvalidSeeds => to_builtin(12),
202                ProgramError::BorshIoError => to_builtin(13),
203                ProgramError::AccountNotRentExempt => to_builtin(14),
204                ProgramError::UnsupportedSysvar => to_builtin(15),
205                ProgramError::IllegalOwner => to_builtin(16),
206                ProgramError::MaxAccountsDataAllocationsExceeded => to_builtin(17),
207                ProgramError::InvalidRealloc => to_builtin(18),
208                ProgramError::MaxInstructionTraceLengthExceeded => to_builtin(19),
209                ProgramError::BuiltinProgramsMustConsumeComputeUnits => to_builtin(20),
210                ProgramError::InvalidAccountOwner => to_builtin(21),
211                ProgramError::ArithmeticOverflow => to_builtin(22),
212                ProgramError::Immutable => to_builtin(23),
213                ProgramError::IncorrectAuthority => to_builtin(24),
214            }
215        }
216
217        let all = [
218            ProgramError::Custom(0),
219            ProgramError::Custom(1),
220            ProgramError::Custom(0xD003),
221            ProgramError::Custom(u32::MAX),
222            ProgramError::InvalidArgument,
223            ProgramError::InvalidInstructionData,
224            ProgramError::InvalidAccountData,
225            ProgramError::AccountDataTooSmall,
226            ProgramError::InsufficientFunds,
227            ProgramError::IncorrectProgramId,
228            ProgramError::MissingRequiredSignature,
229            ProgramError::AccountAlreadyInitialized,
230            ProgramError::UninitializedAccount,
231            ProgramError::NotEnoughAccountKeys,
232            ProgramError::AccountBorrowFailed,
233            ProgramError::MaxSeedLengthExceeded,
234            ProgramError::InvalidSeeds,
235            ProgramError::BorshIoError,
236            ProgramError::AccountNotRentExempt,
237            ProgramError::UnsupportedSysvar,
238            ProgramError::IllegalOwner,
239            ProgramError::MaxAccountsDataAllocationsExceeded,
240            ProgramError::InvalidRealloc,
241            ProgramError::MaxInstructionTraceLengthExceeded,
242            ProgramError::BuiltinProgramsMustConsumeComputeUnits,
243            ProgramError::InvalidAccountOwner,
244            ProgramError::ArithmeticOverflow,
245            ProgramError::Immutable,
246            ProgramError::IncorrectAuthority,
247        ];
248        for err in all {
249            let want = reference(&err);
250            let got: u64 = err.clone().into();
251            assert_eq!(got, want, "u64 code diverged for {err:?}");
252            // And the reverse mapping still round-trips builtins and
253            // the custom sentinel exactly as before.
254            assert_eq!(ProgramError::from(want), err, "roundtrip for {err:?}");
255        }
256    }
257}