From 390c455e72f2bf3ec4407601320dd89053ab1c68 Mon Sep 17 00:00:00 2001 From: DevSolex <220715997+DevSolex@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:03:34 +0100 Subject: [PATCH] feat: unify public lending contract errors --- .../contracts/hello-world/src/errors.rs | 233 ++++++++++++++++++ stellar-lend/contracts/hello-world/src/lib.rs | 93 +++---- .../src/tests/cross_contract_test.rs | 2 +- .../hello-world/src/treasury_test.rs | 6 +- 4 files changed, 287 insertions(+), 47 deletions(-) diff --git a/stellar-lend/contracts/hello-world/src/errors.rs b/stellar-lend/contracts/hello-world/src/errors.rs index f7b4a710..0f3b6d10 100644 --- a/stellar-lend/contracts/hello-world/src/errors.rs +++ b/stellar-lend/contracts/hello-world/src/errors.rs @@ -1,5 +1,18 @@ use soroban_sdk::contracterror; +use crate::admin::AdminError; +use crate::analytics::AnalyticsError; +use crate::borrow::BorrowError; +use crate::deposit::DepositError; +use crate::flash_loan::FlashLoanError; +use crate::interest_rate::InterestRateError; +use crate::liquidate::LiquidationError; +use crate::repay::RepayError; +use crate::risk_management::RiskManagementError; +use crate::risk_params::RiskParamsError; +use crate::treasury::TreasuryError; +use crate::withdraw::WithdrawError; + #[contracterror] #[derive(Copy, Clone, Debug, PartialEq)] pub enum GovernanceError { @@ -39,3 +52,223 @@ pub enum GovernanceError { NotInitialized = 133, InvalidProposal = 134, } + +/// Unified public contract error type for the lending interface. +/// +/// Internal module error enums keep their existing numeric values. Public entrypoints +/// convert them into this compact, stable interface codebook. +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum LendingError { + /// Caller is not authorized to perform the requested action. + Unauthorized = 1, + /// Amount input is zero, negative, or otherwise invalid. + InvalidAmount = 2, + /// Asset reference is missing, malformed, or unsupported for the operation. + InvalidAsset = 3, + /// Generic invalid parameter/configuration input. + InvalidParameter = 4, + /// User or contract balance is too low to complete the operation. + InsufficientBalance = 5, + /// User collateral is too low for the requested action. + InsufficientCollateral = 6, + /// Action would violate the required collateral ratio. + InsufficientCollateralRatio = 7, + /// Arithmetic overflow or underflow occurred. + Overflow = 8, + /// Protocol or operation-level pause is active. + ProtocolPaused = 9, + /// Reentrant execution was detected and blocked. + Reentrancy = 10, + /// Required state/config has not been initialized. + NotInitialized = 11, + /// Initialization was attempted more than once. + AlreadyInitialized = 12, + /// Requested state was not found. + DataNotFound = 13, + /// Division by zero occurred during a calculation. + DivisionByZero = 14, + /// Repayment was attempted with no outstanding debt. + NoDebt = 15, + /// Asset exists but is disabled for the requested action. + AssetNotEnabled = 16, + /// Request exceeded a protocol-enforced limit or bound. + LimitExceeded = 17, + /// Requested action is invalid for the current protocol state. + InvalidState = 18, + /// Required oracle or pricing information is unavailable. + PriceUnavailable = 19, + /// Contract liquidity is too low for the requested flash loan. + InsufficientLiquidity = 20, + /// Flash loan callback address is invalid. + InvalidCallback = 21, + /// Flash loan callback execution failed. + CallbackFailed = 22, + /// Flash loan was not fully repaid within the required flow. + NotRepaid = 23, + /// Treasury address has not been configured. + TreasuryNotSet = 24, + /// Requested reserve withdrawal exceeds available reserves. + InsufficientReserve = 25, + /// Fee configuration value is outside the allowed range. + InvalidFee = 26, + /// Action requires governance flow rather than direct execution. + GovernanceRequired = 27, + /// Generic governance failure surfaced through the public interface. + GovernanceError = 28, +} + +macro_rules! impl_from_error { + ($source:ty, { $($from:path => $to:path,)+ }) => { + impl From<$source> for LendingError { + fn from(error: $source) -> Self { + match error { + $($from => $to,)+ + } + } + } + }; +} + +impl_from_error!(AdminError, { + AdminError::Unauthorized => LendingError::Unauthorized, + AdminError::InvalidParameter => LendingError::InvalidParameter, + AdminError::AdminAlreadySet => LendingError::AlreadyInitialized, +}); + +impl_from_error!(AnalyticsError, { + AnalyticsError::NotInitialized => LendingError::NotInitialized, + AnalyticsError::InvalidParameter => LendingError::InvalidParameter, + AnalyticsError::Overflow => LendingError::Overflow, + AnalyticsError::DataNotFound => LendingError::DataNotFound, +}); + +impl_from_error!(BorrowError, { + BorrowError::InvalidAmount => LendingError::InvalidAmount, + BorrowError::InvalidAsset => LendingError::InvalidAsset, + BorrowError::InsufficientCollateral => LendingError::InsufficientCollateral, + BorrowError::BorrowPaused => LendingError::ProtocolPaused, + BorrowError::InsufficientCollateralRatio => LendingError::InsufficientCollateralRatio, + BorrowError::Overflow => LendingError::Overflow, + BorrowError::Reentrancy => LendingError::Reentrancy, + BorrowError::MaxBorrowExceeded => LendingError::LimitExceeded, + BorrowError::AssetNotEnabled => LendingError::AssetNotEnabled, +}); + +impl_from_error!(DepositError, { + DepositError::InvalidAmount => LendingError::InvalidAmount, + DepositError::InvalidAsset => LendingError::InvalidAsset, + DepositError::InsufficientBalance => LendingError::InsufficientBalance, + DepositError::DepositPaused => LendingError::ProtocolPaused, + DepositError::AssetNotEnabled => LendingError::AssetNotEnabled, + DepositError::Overflow => LendingError::Overflow, + DepositError::Reentrancy => LendingError::Reentrancy, + DepositError::Unauthorized => LendingError::Unauthorized, +}); + +impl_from_error!(FlashLoanError, { + FlashLoanError::InvalidAmount => LendingError::InvalidAmount, + FlashLoanError::InvalidAsset => LendingError::InvalidAsset, + FlashLoanError::InsufficientLiquidity => LendingError::InsufficientLiquidity, + FlashLoanError::FlashLoanPaused => LendingError::ProtocolPaused, + FlashLoanError::NotRepaid => LendingError::NotRepaid, + FlashLoanError::InsufficientRepayment => LendingError::InsufficientBalance, + FlashLoanError::Overflow => LendingError::Overflow, + FlashLoanError::Reentrancy => LendingError::Reentrancy, + FlashLoanError::InvalidCallback => LendingError::InvalidCallback, + FlashLoanError::CallbackFailed => LendingError::CallbackFailed, +}); + +impl From for LendingError { + fn from(error: GovernanceError) -> Self { + match error { + GovernanceError::Unauthorized => LendingError::Unauthorized, + GovernanceError::AlreadyInitialized => LendingError::AlreadyInitialized, + GovernanceError::NotInitialized => LendingError::NotInitialized, + GovernanceError::InvalidQuorum => LendingError::InvalidParameter, + GovernanceError::InvalidVotingPeriod => LendingError::InvalidParameter, + _ => LendingError::GovernanceError, + } + } +} + +impl_from_error!(InterestRateError, { + InterestRateError::Unauthorized => LendingError::Unauthorized, + InterestRateError::InvalidParameter => LendingError::InvalidParameter, + InterestRateError::ParameterChangeTooLarge => LendingError::LimitExceeded, + InterestRateError::Overflow => LendingError::Overflow, + InterestRateError::DivisionByZero => LendingError::DivisionByZero, + InterestRateError::AlreadyInitialized => LendingError::AlreadyInitialized, +}); + +impl_from_error!(LiquidationError, { + LiquidationError::InvalidAmount => LendingError::InvalidAmount, + LiquidationError::InvalidAsset => LendingError::InvalidAsset, + LiquidationError::NotLiquidatable => LendingError::InvalidState, + LiquidationError::LiquidationPaused => LendingError::ProtocolPaused, + LiquidationError::ExceedsCloseFactor => LendingError::LimitExceeded, + LiquidationError::InsufficientBalance => LendingError::InsufficientBalance, + LiquidationError::Overflow => LendingError::Overflow, + LiquidationError::InvalidCollateralAsset => LendingError::InvalidAsset, + LiquidationError::InvalidDebtAsset => LendingError::InvalidAsset, + LiquidationError::PriceNotAvailable => LendingError::PriceUnavailable, + LiquidationError::InsufficientLiquidation => LendingError::InvalidState, + LiquidationError::Reentrancy => LendingError::Reentrancy, +}); + +impl_from_error!(RepayError, { + RepayError::InvalidAmount => LendingError::InvalidAmount, + RepayError::InvalidAsset => LendingError::InvalidAsset, + RepayError::InsufficientBalance => LendingError::InsufficientBalance, + RepayError::RepayPaused => LendingError::ProtocolPaused, + RepayError::NoDebt => LendingError::NoDebt, + RepayError::Overflow => LendingError::Overflow, + RepayError::Reentrancy => LendingError::Reentrancy, +}); + +impl_from_error!(RiskManagementError, { + RiskManagementError::Unauthorized => LendingError::Unauthorized, + RiskManagementError::InvalidParameter => LendingError::InvalidParameter, + RiskManagementError::ParameterChangeTooLarge => LendingError::LimitExceeded, + RiskManagementError::InsufficientCollateralRatio => LendingError::InsufficientCollateralRatio, + RiskManagementError::OperationPaused => LendingError::ProtocolPaused, + RiskManagementError::EmergencyPaused => LendingError::ProtocolPaused, + RiskManagementError::InvalidCollateralRatio => LendingError::InvalidParameter, + RiskManagementError::InvalidLiquidationThreshold => LendingError::InvalidParameter, + RiskManagementError::InvalidCloseFactor => LendingError::InvalidParameter, + RiskManagementError::InvalidLiquidationIncentive => LendingError::InvalidParameter, + RiskManagementError::Overflow => LendingError::Overflow, + RiskManagementError::GovernanceRequired => LendingError::GovernanceRequired, + RiskManagementError::AlreadyInitialized => LendingError::AlreadyInitialized, +}); + +impl_from_error!(RiskParamsError, { + RiskParamsError::Unauthorized => LendingError::Unauthorized, + RiskParamsError::InvalidParameter => LendingError::InvalidParameter, + RiskParamsError::ParameterChangeTooLarge => LendingError::LimitExceeded, + RiskParamsError::InvalidCollateralRatio => LendingError::InvalidParameter, + RiskParamsError::InvalidLiquidationThreshold => LendingError::InvalidParameter, + RiskParamsError::InvalidCloseFactor => LendingError::InvalidParameter, + RiskParamsError::InvalidLiquidationIncentive => LendingError::InvalidParameter, +}); + +impl_from_error!(TreasuryError, { + TreasuryError::Unauthorized => LendingError::Unauthorized, + TreasuryError::InvalidAmount => LendingError::InvalidAmount, + TreasuryError::InsufficientReserve => LendingError::InsufficientReserve, + TreasuryError::Overflow => LendingError::Overflow, + TreasuryError::TreasuryNotSet => LendingError::TreasuryNotSet, + TreasuryError::InvalidFee => LendingError::InvalidFee, +}); + +impl_from_error!(WithdrawError, { + WithdrawError::InvalidAmount => LendingError::InvalidAmount, + WithdrawError::InvalidAsset => LendingError::InvalidAsset, + WithdrawError::InsufficientCollateral => LendingError::InsufficientCollateral, + WithdrawError::WithdrawPaused => LendingError::ProtocolPaused, + WithdrawError::InsufficientCollateralRatio => LendingError::InsufficientCollateralRatio, + WithdrawError::Overflow => LendingError::Overflow, + WithdrawError::Reentrancy => LendingError::Reentrancy, + WithdrawError::Undercollateralized => LendingError::InvalidState, +}); diff --git a/stellar-lend/contracts/hello-world/src/lib.rs b/stellar-lend/contracts/hello-world/src/lib.rs index f852e9cd..d0753ad9 100644 --- a/stellar-lend/contracts/hello-world/src/lib.rs +++ b/stellar-lend/contracts/hello-world/src/lib.rs @@ -30,8 +30,8 @@ pub mod treasury; pub mod types; pub mod withdraw; -use crate::analytics::AnalyticsError; use crate::deposit::Position; +use crate::errors::LendingError; use crate::interest_rate::InterestRateError; use crate::risk_management::RiskManagementError; @@ -55,7 +55,7 @@ impl HelloContract { proposal_threshold: Option, timelock_duration: Option, default_voting_threshold: Option, - ) -> Result<(), governance::GovernanceError> { + ) -> Result<(), LendingError> { governance::initialize( &env, admin, @@ -67,11 +67,12 @@ impl HelloContract { timelock_duration, default_voting_threshold, ) + .map_err(Into::into) } - pub fn initialize(env: Env, admin: Address) -> Result<(), RiskManagementError> { + pub fn initialize(env: Env, admin: Address) -> Result<(), LendingError> { if crate::admin::has_admin(&env) { - return Err(RiskManagementError::Unauthorized); + return Err(LendingError::Unauthorized); } crate::admin::set_admin(&env, admin.clone(), None) .map_err(|_| RiskManagementError::Unauthorized)?; @@ -92,8 +93,8 @@ impl HelloContract { env: Env, caller: Address, new_admin: Address, - ) -> Result<(), admin::AdminError> { - admin::set_admin(&env, new_admin, Some(caller)) + ) -> Result<(), LendingError> { + admin::set_admin(&env, new_admin, Some(caller)).map_err(Into::into) } pub fn deposit_collateral( @@ -101,8 +102,8 @@ impl HelloContract { user: Address, asset: Option
, amount: i128, - ) -> Result { - deposit::deposit_collateral(&env, user, asset, amount) + ) -> Result { + deposit::deposit_collateral(&env, user, asset, amount).map_err(Into::into) } pub fn set_risk_params( @@ -112,7 +113,7 @@ impl HelloContract { liquidation_threshold: Option, close_factor: Option, liquidation_incentive: Option, - ) -> Result<(), RiskManagementError> { + ) -> Result<(), LendingError> { // Authorization is handled by risk_management::require_admin. risk_management::require_admin(&env, &caller)?; risk_params::set_risk_params( @@ -122,7 +123,9 @@ impl HelloContract { close_factor, liquidation_incentive, ) - .map_err(|_| RiskManagementError::InvalidParameter) + .map_err(|_| RiskManagementError::InvalidParameter)?; + + Ok(()) } pub fn borrow_asset( @@ -130,8 +133,8 @@ impl HelloContract { user: Address, asset: Option
, amount: i128, - ) -> Result { - borrow::borrow_asset(&env, user, asset, amount) + ) -> Result { + borrow::borrow_asset(&env, user, asset, amount).map_err(Into::into) } pub fn repay_debt( @@ -139,8 +142,8 @@ impl HelloContract { user: Address, asset: Option
, amount: i128, - ) -> Result<(i128, i128, i128), repay::RepayError> { - repay::repay_debt(&env, user, asset, amount) + ) -> Result<(i128, i128, i128), LendingError> { + repay::repay_debt(&env, user, asset, amount).map_err(Into::into) } pub fn withdraw_collateral( @@ -148,8 +151,8 @@ impl HelloContract { user: Address, asset: Option
, amount: i128, - ) -> Result { - withdraw::withdraw_collateral(&env, user, asset, amount) + ) -> Result { + withdraw::withdraw_collateral(&env, user, asset, amount).map_err(Into::into) } pub fn liquidate( @@ -159,7 +162,7 @@ impl HelloContract { debt_asset: Option
, collateral_asset: Option
, debt_amount: i128, - ) -> Result<(i128, i128, i128), liquidate::LiquidationError> { + ) -> Result<(i128, i128, i128), LendingError> { liquidator.require_auth(); liquidate::liquidate( &env, @@ -169,16 +172,18 @@ impl HelloContract { collateral_asset, debt_amount, ) + .map_err(Into::into) } pub fn set_emergency_pause( env: Env, caller: Address, paused: bool, - ) -> Result<(), RiskManagementError> { + ) -> Result<(), LendingError> { // Authorization is handled by risk_management::require_admin. risk_management::require_admin(&env, &caller)?; risk_management::set_emergency_pause(&env, caller, paused) + .map_err(Into::into) } pub fn execute_flash_loan( @@ -187,8 +192,8 @@ impl HelloContract { asset: Address, amount: i128, callback: Address, - ) -> Result { - flash_loan::execute_flash_loan(&env, user, asset, amount, callback) + ) -> Result { + flash_loan::execute_flash_loan(&env, user, asset, amount, callback).map_err(Into::into) } pub fn repay_flash_loan( @@ -196,38 +201,39 @@ impl HelloContract { user: Address, asset: Address, amount: i128, - ) -> Result<(), flash_loan::FlashLoanError> { - flash_loan::repay_flash_loan(&env, user, asset, amount) + ) -> Result<(), LendingError> { + flash_loan::repay_flash_loan(&env, user, asset, amount).map_err(Into::into) } pub fn can_be_liquidated( env: Env, collateral_value: i128, debt_value: i128, - ) -> Result { - risk_params::can_be_liquidated(&env, collateral_value, debt_value) + ) -> Result { + risk_params::can_be_liquidated(&env, collateral_value, debt_value).map_err(Into::into) } pub fn get_max_liquidatable_amount( env: Env, debt_value: i128, - ) -> Result { - risk_params::get_max_liquidatable_amount(&env, debt_value) + ) -> Result { + risk_params::get_max_liquidatable_amount(&env, debt_value).map_err(Into::into) } pub fn get_liquidation_incentive_amount( env: Env, liquidated_amount: i128, - ) -> Result { - risk_params::get_liquidation_incentive_amount(&env, liquidated_amount) + ) -> Result { + risk_params::get_liquidation_incentive_amount(&env, liquidated_amount).map_err(Into::into) } pub fn require_min_collateral_ratio( env: Env, collateral_value: i128, debt_value: i128, - ) -> Result<(), risk_params::RiskParamsError> { + ) -> Result<(), LendingError> { risk_params::require_min_collateral_ratio(&env, collateral_value, debt_value) + .map_err(Into::into) } // ------------------------------------------------------------------------- @@ -239,8 +245,8 @@ impl HelloContract { env: Env, caller: Address, treasury: Address, - ) -> Result<(), treasury::TreasuryError> { - treasury::set_treasury(&env, caller, treasury) + ) -> Result<(), LendingError> { + treasury::set_treasury(&env, caller, treasury).map_err(Into::into) } /// Return the configured treasury address @@ -260,8 +266,8 @@ impl HelloContract { asset: Option
, recipient: Address, amount: i128, - ) -> Result<(), treasury::TreasuryError> { - treasury::claim_reserves(&env, caller, asset, recipient, amount) + ) -> Result<(), LendingError> { + treasury::claim_reserves(&env, caller, asset, recipient, amount).map_err(Into::into) } /// Update protocol fee percentages (admin-only) @@ -270,7 +276,7 @@ impl HelloContract { caller: Address, interest_fee_bps: i128, liquidation_fee_bps: i128, - ) -> Result<(), treasury::TreasuryError> { + ) -> Result<(), LendingError> { treasury::set_fee_config( &env, caller, @@ -279,6 +285,7 @@ impl HelloContract { liquidation_fee_bps, }, ) + .map_err(Into::into) } /// Return the current fee configuration @@ -312,13 +319,13 @@ impl HelloContract { // ------------------------------------------------------------------------- /// Read-only user health factor query (collateral/debt in basis points). - pub fn get_health_factor(env: Env, user: Address) -> Result { - analytics::calculate_health_factor(&env, &user) + pub fn get_health_factor(env: Env, user: Address) -> Result { + analytics::calculate_health_factor(&env, &user).map_err(Into::into) } /// Read-only user position query. - pub fn get_user_position(env: Env, user: Address) -> Result { - analytics::get_user_position_summary(&env, &user) + pub fn get_user_position(env: Env, user: Address) -> Result { + analytics::get_user_position_summary(&env, &user).map_err(Into::into) } // ------------------------------------------------------------------------- @@ -330,10 +337,10 @@ impl HelloContract { env: Env, asset: Address, params: deposit::AssetParams, - ) -> Result<(), deposit::DepositError> { - let admin = crate::admin::get_admin(&env).ok_or(deposit::DepositError::Unauthorized)?; + ) -> Result<(), LendingError> { + let admin = crate::admin::get_admin(&env).ok_or(LendingError::Unauthorized)?; admin.require_auth(); - deposit::set_asset_params(&env, admin, asset, params) + deposit::set_asset_params(&env, admin, asset, params).map_err(Into::into) } // ------------------------------------------------------------------------- @@ -345,8 +352,8 @@ impl HelloContract { env: Env, caller: Address, config: flash_loan::FlashLoanConfig, - ) -> Result<(), flash_loan::FlashLoanError> { - flash_loan::set_flash_loan_config(&env, caller, config) + ) -> Result<(), LendingError> { + flash_loan::set_flash_loan_config(&env, caller, config).map_err(Into::into) } } diff --git a/stellar-lend/contracts/hello-world/src/tests/cross_contract_test.rs b/stellar-lend/contracts/hello-world/src/tests/cross_contract_test.rs index 53c38404..a1a3d726 100644 --- a/stellar-lend/contracts/hello-world/src/tests/cross_contract_test.rs +++ b/stellar-lend/contracts/hello-world/src/tests/cross_contract_test.rs @@ -196,7 +196,7 @@ fn test_deposit_borrow_interactions() { } #[test] -#[should_panic(expected = "Error(Contract, #3)")] +#[should_panic(expected = "Error(Contract, #20)")] fn test_flash_loan_insufficient_liquidity() { let env = Env::default(); env.mock_all_auths(); diff --git a/stellar-lend/contracts/hello-world/src/treasury_test.rs b/stellar-lend/contracts/hello-world/src/treasury_test.rs index 50ad266a..6f8bc52a 100644 --- a/stellar-lend/contracts/hello-world/src/treasury_test.rs +++ b/stellar-lend/contracts/hello-world/src/treasury_test.rs @@ -66,7 +66,7 @@ fn test_set_fee_config_non_admin_rejected() { } #[test] -#[should_panic(expected = "Error(Contract, #6)")] +#[should_panic(expected = "Error(Contract, #26)")] fn test_set_fee_config_interest_out_of_range() { let (env, admin, contract_id) = setup(); let client = HelloContractClient::new(&env, &contract_id); @@ -74,7 +74,7 @@ fn test_set_fee_config_interest_out_of_range() { } #[test] -#[should_panic(expected = "Error(Contract, #6)")] +#[should_panic(expected = "Error(Contract, #26)")] fn test_set_fee_config_liquidation_out_of_range() { let (env, admin, contract_id) = setup(); let client = HelloContractClient::new(&env, &contract_id); @@ -220,7 +220,7 @@ fn test_claim_reserves_non_admin_rejected() { } #[test] -#[should_panic(expected = "Error(Contract, #3)")] +#[should_panic(expected = "Error(Contract, #25)")] fn test_claim_reserves_exceeds_balance() { let (env, admin, contract_id) = setup(); let client = HelloContractClient::new(&env, &contract_id);