Skip to main content

klend_interface/
errors.rs

1//! Klend program error codes.
2//!
3//! Anchor custom errors start at offset 6000. Each variant's error code is `6000 + variant index`.
4//! Use [`LendingError::from_error_code`] to convert an on-chain error code back to a variant,
5//! or `TryFrom<u32>` for the same purpose.
6//!
7//! # Example
8//!
9//! ```rust
10//! use klend_interface::LendingError;
11//!
12//! // Decode an error code from a failed transaction
13//! let error = LendingError::from_error_code(6008);
14//! assert_eq!(error, Some(LendingError::InsufficientLiquidity));
15//! assert_eq!(error.unwrap().error_code(), 6008);
16//! assert_eq!(
17//!     error.unwrap().to_string(),
18//!     "Insufficient liquidity available"
19//! );
20//! ```
21
22/// Anchor error-code base for custom program errors.
23const ANCHOR_ERROR_BASE: u32 = 6000;
24
25macro_rules! define_lending_errors {
26    (
27        $(
28            $(#[doc = $doc:expr])*
29            $variant:ident = $offset:literal => $msg:expr
30        ),*
31        $(,)?
32    ) => {
33        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34        #[repr(u32)]
35        pub enum LendingError {
36            $(
37                $(#[doc = $doc])*
38                $variant = ANCHOR_ERROR_BASE + $offset,
39            )*
40        }
41
42        impl LendingError {
43            /// Returns the Anchor error code for this variant (`6000 + offset`).
44            pub const fn error_code(self) -> u32 {
45                self as u32
46            }
47
48            /// Returns the human-readable error message.
49            pub const fn message(self) -> &'static str {
50                match self {
51                    $(Self::$variant => $msg,)*
52                }
53            }
54
55            /// Converts an Anchor error code to a `LendingError`, if it matches a known variant.
56            pub const fn from_error_code(code: u32) -> Option<Self> {
57                match code {
58                    $(x if x == ANCHOR_ERROR_BASE + $offset => Some(Self::$variant),)*
59                    _ => None,
60                }
61            }
62        }
63
64        impl core::fmt::Display for LendingError {
65            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66                write!(f, "{}", self.message())
67            }
68        }
69
70        impl TryFrom<u32> for LendingError {
71            type Error = u32;
72
73            /// Converts an error code to a `LendingError`.
74            /// Returns `Err(code)` if the code doesn't match any known variant.
75            fn try_from(code: u32) -> Result<Self, Self::Error> {
76                Self::from_error_code(code).ok_or(code)
77            }
78        }
79    };
80}
81
82define_lending_errors! {
83    InvalidMarketAuthority = 0 => "Market authority is invalid",
84    InvalidMarketOwner = 1 => "Market owner is invalid",
85    InvalidAccountOwner = 2 => "Input account owner is not the program address",
86    InvalidAmount = 3 => "Input amount is invalid",
87    InvalidConfig = 4 => "Input config value is invalid",
88    InvalidSigner = 5 => "Signer is not allowed to perform this action",
89    InvalidAccountInput = 6 => "Invalid account input",
90    MathOverflow = 7 => "Math operation overflow",
91    InsufficientLiquidity = 8 => "Insufficient liquidity available",
92    ReserveStale = 9 => "Reserve state needs to be refreshed",
93    WithdrawTooSmall = 10 => "Withdraw amount too small",
94    WithdrawTooLarge = 11 => "Withdraw amount too large",
95    BorrowTooSmall = 12 => "Borrow amount too small to receive liquidity after fees",
96    BorrowTooLarge = 13 => "Borrow amount too large for deposited collateral",
97    RepayTooSmall = 14 => "Repay amount too small to transfer liquidity",
98    LiquidationTooSmall = 15 => "Liquidation amount too small to receive collateral",
99    ObligationHealthy = 16 => "Cannot liquidate healthy obligations",
100    ObligationStale = 17 => "Obligation state needs to be refreshed",
101    ObligationReserveLimit = 18 => "Obligation reserve limit exceeded",
102    InvalidObligationOwner = 19 => "Obligation owner is invalid",
103    ObligationDepositsEmpty = 20 => "Obligation deposits are empty",
104    ObligationBorrowsEmpty = 21 => "Obligation borrows are empty",
105    ObligationDepositsZero = 22 => "Obligation deposits have zero value",
106    ObligationBorrowsZero = 23 => "Obligation borrows have zero value",
107    InvalidObligationCollateral = 24 => "Invalid obligation collateral",
108    InvalidObligationLiquidity = 25 => "Invalid obligation liquidity",
109    ObligationCollateralEmpty = 26 => "Obligation collateral is empty",
110    ObligationLiquidityEmpty = 27 => "Obligation liquidity is empty",
111    NegativeInterestRate = 28 => "Interest rate is negative",
112    InvalidOracleConfig = 29 => "Input oracle config is invalid",
113    InsufficientProtocolFeesToRedeem = 30 => "Insufficient protocol fees to claim or no liquidity available",
114    FlashBorrowCpi = 31 => "No cpi flash borrows allowed",
115    NoFlashRepayFound = 32 => "No corresponding repay found for flash borrow",
116    InvalidFlashRepay = 33 => "Invalid repay found",
117    FlashRepayCpi = 34 => "No cpi flash repays allowed",
118    MultipleFlashBorrows = 35 => "Multiple flash borrows not allowed in the same transaction",
119    FlashLoansDisabled = 36 => "Flash loans are disabled for this reserve",
120    SwitchboardV2Error = 37 => "Switchboard error",
121    CouldNotDeserializeScope = 38 => "Cannot deserialize the scope price account",
122    PriceTooOld = 39 => "Price too old",
123    PriceTooDivergentFromTwap = 40 => "Price too divergent from twap",
124    InvalidTwapPrice = 41 => "Invalid twap price",
125    GlobalEmergencyMode = 42 => "Emergency mode is enabled",
126    InvalidFlag = 43 => "Invalid lending market config",
127    PriceNotValid = 44 => "Price is not valid",
128    PriceIsBiggerThanHeuristic = 45 => "Price is bigger than allowed by heuristic",
129    PriceIsLowerThanHeuristic = 46 => "Price lower than allowed by heuristic",
130    PriceIsZero = 47 => "Price is zero",
131    PriceConfidenceTooWide = 48 => "Price confidence too wide",
132    IntegerOverflow = 49 => "Conversion between integers failed",
133    NoFarmForReserve = 50 => "This reserve does not have a farm",
134    IncorrectInstructionInPosition = 51 => "Wrong instruction at expected position",
135    NoPriceFound = 52 => "No price found",
136    InvalidTwapConfig = 53 => "Invalid Twap configuration: Twap is enabled but one of the enabled price doesn't have a twap",
137    InvalidPythPriceAccount = 54 => "Pyth price account does not match configuration",
138    InvalidSwitchboardAccount = 55 => "Switchboard account(s) do not match configuration",
139    InvalidScopePriceAccount = 56 => "Scope price account does not match configuration",
140    ObligationCollateralLtvZero = 57 => "The obligation has one collateral with an LTV set to 0. Withdraw it before withdrawing other collaterals",
141    InvalidObligationSeedsValue = 58 => "Seeds must be default pubkeys for tag 0, and mint addresses for tag 1 or 2",
142    DeprecatedInvalidObligationId = 59 => "[DEPRECATED] Obligation id must be 0",
143    InvalidBorrowRateCurvePoint = 60 => "Invalid borrow rate curve point",
144    InvalidUtilizationRate = 61 => "Invalid utilization rate",
145    CannotSocializeObligationWithCollateral = 62 => "Obligation hasn't been fully liquidated and debt cannot be socialized.",
146    ObligationEmpty = 63 => "Obligation has no borrows or deposits.",
147    WithdrawalCapReached = 64 => "Withdrawal cap is reached",
148    LastTimestampGreaterThanCurrent = 65 => "The last interval start timestamp is greater than the current timestamp",
149    LiquidationRewardTooSmall = 66 => "The reward amount is less than the minimum acceptable received liquidity",
150    IsolatedAssetTierViolation = 67 => "Isolated Asset Tier Violation",
151    InconsistentElevationGroup = 68 => "The obligation's elevation group and the reserve's are not the same",
152    InvalidElevationGroup = 69 => "The elevation group chosen for the reserve does not exist in the lending market",
153    InvalidElevationGroupConfig = 70 => "The elevation group updated has wrong parameters set",
154    UnhealthyElevationGroupLtv = 71 => "The current obligation must have most or all its debt repaid before changing the elevation group",
155    ElevationGroupNewLoansDisabled = 72 => "Elevation group does not accept any new loans or any new borrows/withdrawals",
156    ReserveDeprecated = 73 => "Reserve was deprecated, no longer usable",
157    ReferrerAccountNotInitialized = 74 => "Referrer account not initialized",
158    ReferrerAccountMintMissmatch = 75 => "Referrer account mint does not match the operation reserve mint",
159    ReferrerAccountWrongAddress = 76 => "Referrer account address is not a valid program address",
160    ReferrerAccountReferrerMissmatch = 77 => "Referrer account referrer does not match the owner referrer",
161    ReferrerAccountMissing = 78 => "Referrer account missing for obligation with referrer",
162    InsufficientReferralFeesToRedeem = 79 => "Insufficient referral fees to claim or no liquidity available",
163    CpiDisabled = 80 => "CPI disabled for this instruction",
164    ShortUrlNotAsciiAlphanumeric = 81 => "Referrer short_url is not ascii alphanumeric",
165    ReserveObsolete = 82 => "Reserve is marked as obsolete",
166    ElevationGroupAlreadyActivated = 83 => "Obligation already part of the same elevation group",
167    ObligationInObsoleteReserve = 84 => "Obligation has a deposit or borrow in an obsolete reserve",
168    ReferrerStateOwnerMismatch = 85 => "Referrer state owner does not match the given signer",
169    UserMetadataOwnerAlreadySet = 86 => "User metadata owner is already set",
170    CollateralNonLiquidatable = 87 => "This collateral cannot be liquidated (LTV set to 0)",
171    BorrowingDisabled = 88 => "Borrowing is disabled",
172    BorrowLimitExceeded = 89 => "Cannot borrow above borrow limit",
173    DepositLimitExceeded = 90 => "Cannot deposit above deposit limit",
174    BorrowingDisabledOutsideElevationGroup = 91 => "Reserve does not accept any new borrows outside elevation group",
175    NetValueRemainingTooSmall = 92 => "Net value remaining too small",
176    WorseLtvBlocked = 93 => "Cannot get the obligation in a worse position",
177    LiabilitiesBiggerThanAssets = 94 => "Cannot have more liabilities than assets in a position",
178    ReserveTokenBalanceMismatch = 95 => "Reserve state and token account cannot drift",
179    ReserveVaultBalanceMismatch = 96 => "Reserve token account has been unexpectedly modified",
180    ReserveAccountingMismatch = 97 => "Reserve internal state accounting has been unexpectedly modified",
181    BorrowingAboveUtilizationRateDisabled = 98 => "Borrowing above set utilization rate is disabled",
182    LiquidationBorrowFactorPriority = 99 => "Liquidation must prioritize the debt with the highest borrow factor",
183    LiquidationLowestLiquidationLtvPriority = 100 => "Liquidation must prioritize the collateral with the lowest liquidation LTV",
184    ElevationGroupBorrowLimitExceeded = 101 => "Elevation group borrow limit exceeded",
185    ElevationGroupWithoutDebtReserve = 102 => "The elevation group does not have a debt reserve defined",
186    ElevationGroupMaxCollateralReserveZero = 103 => "The elevation group does not allow any collateral reserves",
187    ElevationGroupHasAnotherDebtReserve = 104 => "In elevation group attempt to borrow from a reserve that is not the debt reserve",
188    ElevationGroupDebtReserveAsCollateral = 105 => "The elevation group's debt reserve cannot be used as a collateral reserve",
189    ObligationCollateralExceedsElevationGroupLimit = 106 => "Obligation have more collateral than the maximum allowed by the elevation group",
190    ObligationElevationGroupMultipleDebtReserve = 107 => "Obligation is an elevation group but have more than one debt reserve",
191    UnsupportedTokenExtension = 108 => "Mint has a token (2022) extension that is not supported",
192    InvalidTokenAccount = 109 => "Can't have an spl token mint with a t22 account",
193    DepositDisabledOutsideElevationGroup = 110 => "Can't deposit into this reserve outside elevation group",
194    CannotCalculateReferralAmountDueToSlotsMismatch = 111 => "Cannot calculate referral amount due to slots mismatch",
195    ObligationOwnersMustMatch = 112 => "Obligation owners must match",
196    ObligationsMustMatch = 113 => "Obligations must match",
197    LendingMarketsMustMatch = 114 => "Lending markets must match",
198    ObligationCurrentlyMarkedForDeleveraging = 115 => "Obligation is already marked for deleveraging",
199    MaximumWithdrawValueZero = 116 => "Maximum withdrawable value of this collateral is zero, LTV needs improved",
200    ZeroMaxLtvAssetsInDeposits = 117 => "No max LTV 0 assets allowed in deposits for repay and withdraw",
201    LowestLtvAssetsPriority = 118 => "Withdrawing must prioritize the collateral with the lowest reserve max-LTV",
202    WorseLtvThanUnhealthyLtv = 119 => "Cannot get the obligation liquidatable",
203    FarmAccountsMissing = 120 => "Farm accounts to refresh are missing",
204    RepayTooSmallForFullLiquidation = 121 => "Repay amount is too small to satisfy the mandatory full liquidation",
205    InsufficientRepayAmount = 122 => "Liquidator provided repay amount lower than required by liquidation rules",
206    OrderIndexOutOfBounds = 123 => "Obligation order of the given index cannot exist",
207    InvalidOrderConfiguration = 124 => "Given order configuration has wrong parameters",
208    OrderConfigurationNotSupportedByObligation = 125 => "Given order configuration cannot be used with the current state of the obligation",
209    OperationNotPermittedWithCurrentObligationOrders = 126 => "Single debt, single collateral obligation orders have to be cancelled before changing the deposit/borrow count",
210    OperationNotPermittedMarketImmutable = 127 => "Cannot update lending market because it is set as immutable",
211    OrderCreationDisabled = 128 => "Creation of new orders is disabled",
212    NoUpgradeAuthority = 129 => "Cannot initialize global config because there is no upgrade authority to the program",
213    InitialAdminDepositExecuted = 130 => "Initial admin deposit in reserve already executed",
214    ReserveHasNotReceivedInitialDeposit = 131 => "Reserve has not received the initial deposit, cannot update config",
215    CTokenUsageBlocked = 132 => "CToken minting/redeeming is blocked for this reserve",
216    CannotUseSameReserve = 133 => "Cannot call ix with same reserve",
217    TransactionIncludesRestrictedPrograms = 134 => "Transaction includes restricted programs",
218    BorrowOrderDebtLiquidityMintMismatch = 135 => "There is no borrow order requesting debt in the given asset",
219    BorrowOrderMaxBorrowRateExceeded = 136 => "Reserve used for fill exceeds the maximum borrow rate specified by the order",
220    BorrowOrderMinDebtTermInsufficient = 137 => "Reserve used for fill defines a debt term shorter than specified by the order",
221    BorrowOrderFillTimeLimitExceeded = 138 => "Borrow order can no longer be filled",
222    ReserveDebtMaturityReached = 139 => "Cannot borrow from a reserve that reached its debt maturity timestamp",
223    NonUpdatableOrderConfiguration = 140 => "Some piece of the order's configuration cannot be updated (the order should be cancelled and placed again)",
224    BorrowOrderExecutionDisabled = 141 => "Execution of borrow orders is disabled",
225    DebtReachedReserveDebtTerm = 142 => "Cannot increase the debt that has reached its end of term configured by the reserve",
226    ExpectationNotMet = 143 => "The on-chain state does not meet expectation specified by the caller, so the operation must be aborted (to avoid race conditions)",
227    BorrowOrderFillValueTooSmall = 144 => "Available liquidity could not satisfy the minimum required borrow order fill value",
228    WithdrawTicketIssuanceDisabled = 145 => "Issuing new withdraw tickets is disabled by the market",
229    WithdrawTicketRedemptionDisabled = 146 => "Redeeming withdraw tickets is disabled by the market",
230    WithdrawTicketStillValid = 147 => "Recovering collateral is only available after the withdraw ticket has been marked invalid",
231    WithdrawTicketRequiresFullRedemption = 148 => "The withdraw ticket's current state requires that it is fully redeemed (e.g. due to owner ATA creation), but there is not enough liquidity",
232    UserTokenBalanceMismatch = 149 => "The user's token account has changed its balance in an unexpected way",
233    WithdrawQueuedLiquidityValueTooSmall = 150 => "Available liquidity could not satisfy the minimum required ticketed withdrawal value",
234    InvalidTokenAccountState = 151 => "Token account is in a state preventing the handler's operation (e.g. frozen or delegate)",
235    WithdrawTicketInvalid = 152 => "Cannot use ticket that was already marked invalid",
236    BorrowOrderValueTooSmall = 153 => "Borrow order's value would be below the market-configured minimum",
237    WithdrawTicketValueTooSmall = 154 => "Withdraw ticket's value would be below the market-configured minimum",
238    InvalidWithdrawTicketProgressCallbackConfig = 155 => "Invalid configuration or required custom accounts for the requested withdraw ticket callback type",
239    WithdrawTicketProgressCallbackAccountsMissing = 156 => "One or more accounts required by the ticket's configured progress callback are missing",
240    BorrowRolloverConfigurationDisabled = 157 => "Configuring auto-rollover on loans is disabled by market owner",
241    InvalidObligationConfigUpdateSubject = 158 => "Invalid specification of the Obligation's part to be configured",
242    BorrowRolloverLiquidityMintMismatch = 159 => "Auto-rollover must use a target reserve of the same token",
243    ObligationBorrowRolloverNotApplicable = 160 => "The given borrow is not fixed-term and does not require rolling over",
244    ObligationBorrowOutsideRolloverWindow = 161 => "The given borrow is outside the corresponding market-configured rollover window",
245    ObligationBorrowRolloverNotEnabledByOwner = 162 => "Obligation's owner did not opt-in for auto-rollover of the given borrow",
246    ObligationBorrowRolloverTargetReserveMismatch = 163 => "Obligation's owner did not allow to roll over into terms offered by the given reserve",
247    BorrowRolloverExecutionDisabled = 164 => "Executing auto-rollover is disabled by market owner",
248    ObligationAccountingMismatch = 165 => "Obligation internal state accounting has been unexpectedly modified",
249    PartialRolloverValueTooSmall = 166 => "Partial rollover amount is below the market-configured minimum value",
250    ObligationBorrowRolloverConfigMismatch = 167 => "Pre-existing rollover configuration of the loan cannot be overwritten by the operation",
251    ObligationBorrowRolloverMustProlongDebtTerm = 168 => "Rollover into existing borrow must prolong the remaining debt term",
252    RolloverNotSupportedInElevationGroup = 169 => "Rollover is not supported for obligations in an elevation group",
253    WithdrawTicketCancellationDisabled = 170 => "Cancelling withdraw tickets is disabled by the market",
254    WithdrawTicketFullyCancelled = 171 => "Cannot use ticket that was already fully-cancelled",
255    CloneSourceReserveDisabled = 172 => "Cannot clone config from a reserve that is disabled",
256    CloneTargetReserveAlreadyInUse = 173 => "Cannot clone config into a reserve that has been in use",
257    ClonedReserveLiquidityMintMismatch = 174 => "Cannot clone config between reserves of different mints",
258    ReserveEmergencyMode = 175 => "Reserve emergency mode is enabled",
259    ObligationOwnershipTransferInProgress = 176 => "Obligation ownership transfer is in progress",
260    ObligationOwnershipTransferNotInInitiatedState = 177 => "Obligation ownership transfer is not in initiated state",
261    ObligationPendingOwnerNotSet = 178 => "Obligation pending owner not set",
262    ObligationInvalidPendingOwner = 179 => "Invalid pending owner address",
263    ObligationOwnershipTransferNotApproved = 180 => "Obligation ownership transfer not approved by admin",
264    ObligationHasActiveBorrowOrders = 181 => "Obligation has active borrow orders",
265    OnlyComputeBudgetCompanionIxsAllowed = 182 => "Only ComputeBudget instructions may accompany this instruction",
266    MissingPermissioner = 183 => "Required permissioning account is missing",
267    ReserveRewardsDisabled = 184 => "Reserve rewards are disabled on this market (reserve_rewards_max_apr_bps is 0)",
268}
269
270impl std::error::Error for LendingError {}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_error_codes() {
278        assert_eq!(LendingError::InvalidMarketAuthority.error_code(), 6000);
279        assert_eq!(LendingError::InsufficientLiquidity.error_code(), 6008);
280        assert_eq!(
281            LendingError::ClonedReserveLiquidityMintMismatch.error_code(),
282            6174
283        );
284        assert_eq!(LendingError::MissingPermissioner.error_code(), 6183);
285        assert_eq!(
286            LendingError::OnlyComputeBudgetCompanionIxsAllowed.error_code(),
287            6182
288        );
289    }
290
291    #[test]
292    fn test_from_error_code() {
293        assert_eq!(
294            LendingError::from_error_code(6000),
295            Some(LendingError::InvalidMarketAuthority)
296        );
297        assert_eq!(
298            LendingError::from_error_code(6008),
299            Some(LendingError::InsufficientLiquidity)
300        );
301        assert_eq!(LendingError::from_error_code(5999), None);
302        assert_eq!(
303            LendingError::from_error_code(6175),
304            Some(LendingError::ReserveEmergencyMode)
305        );
306        assert_eq!(
307            LendingError::from_error_code(6182),
308            Some(LendingError::OnlyComputeBudgetCompanionIxsAllowed)
309        );
310        assert_eq!(
311            LendingError::from_error_code(6183),
312            Some(LendingError::MissingPermissioner)
313        );
314        assert_eq!(
315            LendingError::from_error_code(6184),
316            Some(LendingError::ReserveRewardsDisabled)
317        );
318        assert_eq!(LendingError::from_error_code(6185), None);
319    }
320
321    #[test]
322    fn test_try_from() {
323        assert_eq!(
324            LendingError::try_from(6042),
325            Ok(LendingError::GlobalEmergencyMode)
326        );
327        assert_eq!(LendingError::try_from(9999), Err(9999));
328    }
329
330    #[test]
331    fn test_display() {
332        assert_eq!(
333            LendingError::MathOverflow.to_string(),
334            "Math operation overflow"
335        );
336    }
337
338    /// Verify variant count matches the on-chain program (183 variants, codes 6000..=6182).
339    #[test]
340    fn test_all_codes_roundtrip() {
341        let mut count = 0u32;
342        for code in 6000..=6183 {
343            let err = LendingError::from_error_code(code)
344                .unwrap_or_else(|| panic!("Missing variant for code {code}"));
345            assert_eq!(err.error_code(), code);
346            count += 1;
347        }
348        assert_eq!(count, 184);
349    }
350}