From 6c3d512cf5f5a548c56b73776062ad515a000874 Mon Sep 17 00:00:00 2001 From: martinvibes Date: Thu, 18 Jun 2026 06:50:10 +0100 Subject: [PATCH 1/5] fix(backend): account for token decimals in collateral valuation (#632) The risk engine valued collateral and debt with raw Decimal `*` and `/`, which (1) ignored each token's native on-chain precision, letting sub-unit dust retained by the flat NUMERIC(_,8) storage scale distort valuations; (2) could overflow rust_decimal's 28-significant-digit budget for high-value positions; and (3) compared the health factor at full precision while persisting it as DECIMAL(10,4), so the risk flag and the stored value could disagree at the liquidation-threshold boundary. Introduce pure, unit-tested helpers: - token_decimals(): native precision per asset (USDC 6, XLM 7, BTC 8, ETH 18) - normalize_amount(): round amounts to a token's native precision - value_in_usd(): normalize then price via SafeMath at a canonical USD scale - compute_health_factor(): SafeMath division rounded to the storage scale check_all_loans() now uses these and skips a plan (with a warning) instead of risking a panic when valuation arithmetic fails. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/risk_engine.rs | 174 ++++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 4 deletions(-) diff --git a/backend/src/risk_engine.rs b/backend/src/risk_engine.rs index a6f91cc11..d38b0061f 100644 --- a/backend/src/risk_engine.rs +++ b/backend/src/risk_engine.rs @@ -3,6 +3,7 @@ use crate::notifications::{ audit_action, entity_type, notif_type, AuditLogService, NotificationService, }; use crate::price_feed::PriceFeedService; +use crate::safe_math::SafeMath; use chrono::Utc; use rust_decimal::Decimal; use sqlx::PgPool; @@ -10,6 +11,60 @@ use std::sync::Arc; use std::time::Duration; use tracing::{error, info, warn}; +/// Canonical scale (decimal places) for all USD valuations. Bounding the scale +/// of every monetary value keeps multiplications well within `Decimal`'s +/// 28-significant-digit budget and makes valuations of differently-scaled +/// tokens directly comparable. +const USD_VALUATION_SCALE: u32 = 8; + +/// Scale of the `plans.health_factor` column (`DECIMAL(10, 4)`). The health +/// factor is rounded to this scale before *both* the risk comparison and +/// persistence so the stored value and the risk flag can never disagree at the +/// liquidation-threshold boundary. +const HEALTH_FACTOR_SCALE: u32 = 4; + +/// Native on-chain decimal precision for an asset, case-insensitive. +/// +/// Token amounts are stored as `NUMERIC(_, 8)`, but each token has its own true +/// precision (USDC: 6, XLM: 7, BTC: 8, ETH: 18). Anything stored beyond a +/// token's native precision is dust that must not influence a valuation. +/// Unknown assets fall back to the storage scale of 8. +fn token_decimals(asset_code: &str) -> u32 { + match asset_code.to_uppercase().as_str() { + "USDC" | "USDT" => 6, + "XLM" | "STELLAR_XLM" => 7, + "BTC" | "WBTC" => 8, + "ETH" | "WETH" => 18, + _ => 8, + } +} + +/// Round a stored token amount to its native on-chain precision, dropping any +/// sub-unit dust that the flat `NUMERIC(_, 8)` storage scale may have retained. +fn normalize_amount(amount: Decimal, asset_code: &str) -> Decimal { + amount.round_dp(token_decimals(asset_code)) +} + +/// Value `amount` of `asset_code` in USD at `price`, normalizing the amount to +/// the token's native precision first and returning the result at the canonical +/// USD valuation scale. Uses checked arithmetic so high-value tokens cannot +/// overflow. +fn value_in_usd(amount: Decimal, price: Decimal, asset_code: &str) -> Result { + let normalized = normalize_amount(amount, asset_code); + let value = SafeMath::mul(normalized, price)?; + Ok(value.round_dp(USD_VALUATION_SCALE)) +} + +/// Compute a health factor (`collateral_value / debt_value`) rounded to the +/// health-factor storage scale. Errors on zero debt via `SafeMath::div`. +fn compute_health_factor( + collateral_value: Decimal, + debt_value: Decimal, +) -> Result { + let hf = SafeMath::div(collateral_value, debt_value)?; + Ok(hf.round_dp(HEALTH_FACTOR_SCALE)) +} + pub struct RiskEngine { db: PgPool, price_feed: Arc, @@ -141,12 +196,42 @@ impl RiskEngine { } }; - // Values are aggregated using numeric casts, yielding Decimal safely via sqlx mapping - let collat_value = loan.collateral_amount.unwrap_or(Decimal::ZERO) * collat_price; - let debt_value = loan.total_debt * borrow_price; + // Value collateral and debt in USD, normalizing each amount to its + // token's native precision and using checked arithmetic so a + // high-value position cannot overflow or silently lose precision. + let collat_amount = loan.collateral_amount.unwrap_or(Decimal::ZERO); + let collat_value = match value_in_usd(collat_amount, collat_price, &collat_asset) { + Ok(v) => v, + Err(e) => { + warn!( + "Risk Engine: skipping plan {} — collateral valuation failed: {}", + loan.plan_id, e + ); + continue; + } + }; + let debt_value = match value_in_usd(loan.total_debt, borrow_price, &loan.borrow_asset) { + Ok(v) => v, + Err(e) => { + warn!( + "Risk Engine: skipping plan {} — debt valuation failed: {}", + loan.plan_id, e + ); + continue; + } + }; if debt_value > Decimal::ZERO { - let health_factor = collat_value / debt_value; + let health_factor = match compute_health_factor(collat_value, debt_value) { + Ok(hf) => hf, + Err(e) => { + warn!( + "Risk Engine: skipping plan {} — health factor computation failed: {}", + loan.plan_id, e + ); + continue; + } + }; // Skip risk flagging if risk override is enabled let should_skip_risk_check = loan.risk_override_enabled.unwrap_or(false); @@ -230,3 +315,84 @@ impl RiskEngine { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal_macros::dec; + + #[test] + fn token_decimals_known_assets() { + // Native on-chain precision per asset, case-insensitive. + assert_eq!(token_decimals("USDC"), 6); + assert_eq!(token_decimals("usdc"), 6); + assert_eq!(token_decimals("USDT"), 6); + assert_eq!(token_decimals("BTC"), 8); + assert_eq!(token_decimals("WBTC"), 8); + assert_eq!(token_decimals("ETH"), 18); + assert_eq!(token_decimals("WETH"), 18); + assert_eq!(token_decimals("XLM"), 7); + assert_eq!(token_decimals("STELLAR_XLM"), 7); + } + + #[test] + fn token_decimals_unknown_asset_defaults_to_storage_scale() { + // Unknown assets fall back to the NUMERIC(_, 8) storage scale. + assert_eq!(token_decimals("DOGE"), 8); + } + + #[test] + fn normalize_amount_drops_dust_below_token_precision() { + // USDC has 6 decimals; digits beyond that are dust and must be dropped. + assert_eq!( + normalize_amount(dec!(100.123456789), "USDC"), + dec!(100.123457) + ); + // XLM has 7 decimals. + assert_eq!(normalize_amount(dec!(50.12345678), "XLM"), dec!(50.1234568)); + } + + #[test] + fn normalize_amount_preserves_high_precision_tokens() { + // ETH (18 decimals) stored at scale 8 keeps all stored digits. + assert_eq!(normalize_amount(dec!(1.23456789), "ETH"), dec!(1.23456789)); + // BTC (8 decimals) keeps its full stored precision. + assert_eq!(normalize_amount(dec!(0.12345678), "BTC"), dec!(0.12345678)); + } + + #[test] + fn value_in_usd_normalizes_then_prices_at_canonical_scale() { + // 100.1234569 USDC (normalized to 100.123457) * $1.00 = 100.12345700 + let v = value_in_usd(dec!(100.1234569), dec!(1.00), "USDC").unwrap(); + assert_eq!(v, dec!(100.12345700)); + assert_eq!(v.scale(), USD_VALUATION_SCALE); + } + + #[test] + fn value_in_usd_handles_high_value_tokens_without_overflow() { + // 1000 BTC at $1,000,000 — large product must not overflow. + let v = value_in_usd(dec!(1000), dec!(1000000), "BTC").unwrap(); + assert_eq!(v, dec!(1000000000.00000000)); + } + + #[test] + fn compute_health_factor_rounds_to_storage_scale() { + // 150 collateral / 100 debt = 1.5, within the DECIMAL(10,4) storage scale. + let hf = compute_health_factor(dec!(150), dec!(100)).unwrap(); + assert_eq!(hf, dec!(1.5)); + } + + #[test] + fn compute_health_factor_is_consistent_at_threshold_boundary() { + // A repeating ratio must round to at most the storage scale so the + // persisted value and the risk flag agree. 1000/3000 = 0.3333... -> 0.3333. + let hf = compute_health_factor(dec!(1000), dec!(3000)).unwrap(); + assert_eq!(hf, dec!(0.3333)); + assert!(hf.scale() <= HEALTH_FACTOR_SCALE); + } + + #[test] + fn compute_health_factor_zero_debt_is_error() { + assert!(compute_health_factor(dec!(100), dec!(0)).is_err()); + } +} From 10d1619254db9e342fd643303f2e47801e79cf8a Mon Sep 17 00:00:00 2001 From: martinvibes Date: Thu, 18 Jun 2026 06:57:44 +0100 Subject: [PATCH 2/5] refactor: extract position assessment logic into reusable function for improved maintainability and testing --- backend/src/risk_engine.rs | 343 ++++++++++++++++++++++++++++++++----- 1 file changed, 296 insertions(+), 47 deletions(-) diff --git a/backend/src/risk_engine.rs b/backend/src/risk_engine.rs index d38b0061f..cb9f232fb 100644 --- a/backend/src/risk_engine.rs +++ b/backend/src/risk_engine.rs @@ -65,6 +65,82 @@ fn compute_health_factor( Ok(hf.round_dp(HEALTH_FACTOR_SCALE)) } +/// Liquidation threshold for a given collateral asset, case-insensitive. More +/// volatile assets carry a lower threshold; unknown assets fall back to the +/// engine-wide `fallback` threshold. +fn liquidation_threshold_for_asset(asset_code: &str, fallback: Decimal) -> Decimal { + match asset_code.to_uppercase().as_str() { + "USDC" => Decimal::new(95, 2), // 0.95 + "ETH" | "WETH" => Decimal::new(85, 2), // 0.85 + "BTC" | "WBTC" => Decimal::new(85, 2), // 0.85 + "XLM" | "STELLAR_XLM" => Decimal::new(80, 2), // 0.80 + _ => fallback, + } +} + +/// Outcome of evaluating a single borrowing position's collateral health. +#[derive(Debug, Clone, PartialEq)] +struct PositionAssessment { + /// USD value of the collateral at the canonical valuation scale. + collateral_value: Decimal, + /// USD value of the outstanding debt at the canonical valuation scale. + debt_value: Decimal, + /// Collateral-to-debt health factor at the storage scale. + health_factor: Decimal, + /// Liquidation threshold applied for this collateral asset. + liquidation_threshold: Decimal, + /// Whether the position should be flagged as risky. + is_risky: bool, +} + +/// Pure, DB-free evaluation of a borrowing position. +/// +/// Values both sides in USD (normalizing each amount to its token's native +/// precision via [`value_in_usd`]), derives the health factor, and decides +/// whether the position is risky against the collateral asset's liquidation +/// threshold. The comparison is strict (`<`), so a position sitting exactly at +/// its threshold is *not* flagged. +/// +/// Returns `Ok(None)` when there is no positive debt to assess. Returns `Err` +/// if any valuation overflows or debt-side arithmetic is invalid, so callers +/// can skip the position instead of panicking. +#[allow(clippy::too_many_arguments)] +fn assess_position( + collateral_amount: Decimal, + collateral_asset: &str, + collateral_price: Decimal, + debt_amount: Decimal, + borrow_asset: &str, + borrow_price: Decimal, + fallback_threshold: Decimal, + risk_override_enabled: bool, +) -> Result, ApiError> { + let collateral_value = value_in_usd(collateral_amount, collateral_price, collateral_asset)?; + let debt_value = value_in_usd(debt_amount, borrow_price, borrow_asset)?; + + if debt_value <= Decimal::ZERO { + return Ok(None); + } + + let health_factor = compute_health_factor(collateral_value, debt_value)?; + let liquidation_threshold = + liquidation_threshold_for_asset(collateral_asset, fallback_threshold); + + let is_risky = if risk_override_enabled { + false + } else { + health_factor < liquidation_threshold + }; + + Ok(Some(PositionAssessment { + collateral_value, + debt_value, + health_factor, + liquidation_threshold, + is_risky, + })) +} + pub struct RiskEngine { db: PgPool, price_feed: Arc, @@ -196,63 +272,39 @@ impl RiskEngine { } }; - // Value collateral and debt in USD, normalizing each amount to its - // token's native precision and using checked arithmetic so a - // high-value position cannot overflow or silently lose precision. + // Evaluate the position: value both sides in USD (normalizing each + // amount to its token's native precision via checked arithmetic), + // derive the health factor, and decide risk against the collateral + // asset's liquidation threshold. A `None` result means there is no + // outstanding debt; an `Err` means valuation arithmetic failed and + // the plan is skipped rather than risking a panic. let collat_amount = loan.collateral_amount.unwrap_or(Decimal::ZERO); - let collat_value = match value_in_usd(collat_amount, collat_price, &collat_asset) { - Ok(v) => v, + let should_skip_risk_check = loan.risk_override_enabled.unwrap_or(false); + let assessment = match assess_position( + collat_amount, + &collat_asset, + collat_price, + loan.total_debt, + &loan.borrow_asset, + borrow_price, + self.liquidation_threshold, + should_skip_risk_check, + ) { + Ok(Some(a)) => a, + Ok(None) => continue, // no outstanding debt to assess Err(e) => { warn!( - "Risk Engine: skipping plan {} — collateral valuation failed: {}", - loan.plan_id, e - ); - continue; - } - }; - let debt_value = match value_in_usd(loan.total_debt, borrow_price, &loan.borrow_asset) { - Ok(v) => v, - Err(e) => { - warn!( - "Risk Engine: skipping plan {} — debt valuation failed: {}", + "Risk Engine: skipping plan {} — position assessment failed: {}", loan.plan_id, e ); continue; } }; - if debt_value > Decimal::ZERO { - let health_factor = match compute_health_factor(collat_value, debt_value) { - Ok(hf) => hf, - Err(e) => { - warn!( - "Risk Engine: skipping plan {} — health factor computation failed: {}", - loan.plan_id, e - ); - continue; - } - }; - - // Skip risk flagging if risk override is enabled - let should_skip_risk_check = loan.risk_override_enabled.unwrap_or(false); - - // Determine liquidation threshold based on collateral asset when possible - let asset_upper = collat_asset.to_uppercase(); - let liquidation_threshold_for_asset = match asset_upper.as_str() { - "USDC" => Decimal::new(95, 2), // 0.95 - "ETH" | "WETH" => Decimal::new(85, 2), // 0.85 - "BTC" | "WBTC" => Decimal::new(85, 2), // 0.85 - "XLM" | "STELLAR_XLM" => Decimal::new(80, 2), // 0.80 - // Fallback to engine-wide threshold if unknown - _ => self.liquidation_threshold, - }; - - let is_now_risky = if should_skip_risk_check { - false // Override: never mark as risky - } else { - health_factor < liquidation_threshold_for_asset - }; + let health_factor = assessment.health_factor; + let is_now_risky = assessment.is_risky; + { // Update database state sqlx::query( r#" @@ -395,4 +447,201 @@ mod tests { fn compute_health_factor_zero_debt_is_error() { assert!(compute_health_factor(dec!(100), dec!(0)).is_err()); } + + // --- liquidation_threshold_for_asset --- + + #[test] + fn liquidation_threshold_uses_asset_specific_values() { + let fallback = dec!(0.90); + assert_eq!( + liquidation_threshold_for_asset("USDC", fallback), + dec!(0.95) + ); + assert_eq!(liquidation_threshold_for_asset("ETH", fallback), dec!(0.85)); + assert_eq!( + liquidation_threshold_for_asset("WETH", fallback), + dec!(0.85) + ); + assert_eq!(liquidation_threshold_for_asset("BTC", fallback), dec!(0.85)); + assert_eq!( + liquidation_threshold_for_asset("WBTC", fallback), + dec!(0.85) + ); + assert_eq!(liquidation_threshold_for_asset("XLM", fallback), dec!(0.80)); + assert_eq!( + liquidation_threshold_for_asset("STELLAR_XLM", fallback), + dec!(0.80) + ); + } + + #[test] + fn liquidation_threshold_is_case_insensitive() { + let fallback = dec!(0.90); + assert_eq!( + liquidation_threshold_for_asset("usdc", fallback), + dec!(0.95) + ); + assert_eq!(liquidation_threshold_for_asset("eth", fallback), dec!(0.85)); + } + + #[test] + fn liquidation_threshold_unknown_asset_uses_fallback() { + let fallback = dec!(0.77); + assert_eq!(liquidation_threshold_for_asset("DOGE", fallback), fallback); + } + + // --- assess_position --- + + #[test] + fn assess_position_healthy_is_not_risky() { + // 200 USDC collateral vs 100 USDC debt at $1 -> HF 2.0, well above 0.95. + let a = assess_position( + dec!(200), + "USDC", + dec!(1), + dec!(100), + "USDC", + dec!(1), + dec!(0.90), + false, + ) + .unwrap() + .unwrap(); + assert_eq!(a.collateral_value, dec!(200.00000000)); + assert_eq!(a.debt_value, dec!(100.00000000)); + assert_eq!(a.health_factor, dec!(2)); + assert_eq!(a.liquidation_threshold, dec!(0.95)); + assert!(!a.is_risky); + } + + #[test] + fn assess_position_undercollateralized_is_risky() { + // 90 USDC collateral vs 100 USDC debt -> HF 0.90 < 0.95 threshold. + let a = assess_position( + dec!(90), + "USDC", + dec!(1), + dec!(100), + "USDC", + dec!(1), + dec!(0.50), + false, + ) + .unwrap() + .unwrap(); + assert_eq!(a.health_factor, dec!(0.9)); + assert!(a.is_risky); + } + + #[test] + fn assess_position_at_exact_threshold_is_not_risky() { + // HF exactly equal to the threshold must NOT trip (comparison is strict <). + // 95 USDC vs 100 USDC -> HF 0.95 == USDC threshold 0.95. + let a = assess_position( + dec!(95), + "USDC", + dec!(1), + dec!(100), + "USDC", + dec!(1), + dec!(0.50), + false, + ) + .unwrap() + .unwrap(); + assert_eq!(a.health_factor, dec!(0.95)); + assert!(!a.is_risky); + } + + #[test] + fn assess_position_override_never_risky() { + // Deeply undercollateralized, but override disables risk flagging. + let a = assess_position( + dec!(1), + "USDC", + dec!(1), + dec!(100), + "USDC", + dec!(1), + dec!(0.50), + true, + ) + .unwrap() + .unwrap(); + assert!(a.health_factor < a.liquidation_threshold); + assert!(!a.is_risky); + } + + #[test] + fn assess_position_zero_debt_returns_none() { + let a = assess_position( + dec!(100), + "USDC", + dec!(1), + dec!(0), + "USDC", + dec!(1), + dec!(0.90), + false, + ) + .unwrap(); + assert!(a.is_none()); + } + + #[test] + fn assess_position_uses_collateral_asset_threshold() { + // XLM collateral threshold is 0.80. HF 0.82 is above it -> not risky, + // even though it would be risky under USDC's stricter 0.95. + let a = assess_position( + dec!(82), + "XLM", + dec!(1), + dec!(100), + "USDC", + dec!(1), + dec!(0.50), + false, + ) + .unwrap() + .unwrap(); + assert_eq!(a.liquidation_threshold, dec!(0.80)); + assert_eq!(a.health_factor, dec!(0.82)); + assert!(!a.is_risky); + } + + #[test] + fn assess_position_normalizes_token_decimals() { + // USDC dust beyond 6 decimals is dropped before valuation. + let a = assess_position( + dec!(100.12345678), + "USDC", + dec!(1), + dec!(50), + "USDC", + dec!(1), + dec!(0.90), + false, + ) + .unwrap() + .unwrap(); + assert_eq!(a.collateral_value, dec!(100.12345700)); + } + + #[test] + fn assess_position_overflow_is_error() { + // A price that overflows when multiplied by a huge amount must error, + // not panic. + let huge = Decimal::MAX; + assert!(assess_position( + huge, + "USDC", + dec!(1000000), + dec!(100), + "USDC", + dec!(1), + dec!(0.90), + false, + ) + .is_err()); + } } From 33d96e62d6df71aad79231afc7d066c1d7bc9098 Mon Sep 17 00:00:00 2001 From: martinvibes Date: Thu, 18 Jun 2026 07:08:48 +0100 Subject: [PATCH 3/5] refactor: centralize token metadata and risk parameters into a new registry module --- backend/src/lib.rs | 1 + backend/src/risk_engine.rs | 97 +--------- backend/src/token_metadata.rs | 334 ++++++++++++++++++++++++++++++++++ 3 files changed, 339 insertions(+), 93 deletions(-) create mode 100644 backend/src/token_metadata.rs diff --git a/backend/src/lib.rs b/backend/src/lib.rs index f3b8c8476..2ddaa90ce 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -50,6 +50,7 @@ pub mod session; pub mod stellar; pub mod stress_testing; pub mod telemetry; +pub mod token_metadata; pub mod validation; pub mod webhook; pub mod will_audit; diff --git a/backend/src/risk_engine.rs b/backend/src/risk_engine.rs index cb9f232fb..aa40e161e 100644 --- a/backend/src/risk_engine.rs +++ b/backend/src/risk_engine.rs @@ -4,6 +4,7 @@ use crate::notifications::{ }; use crate::price_feed::PriceFeedService; use crate::safe_math::SafeMath; +use crate::token_metadata; use chrono::Utc; use rust_decimal::Decimal; use sqlx::PgPool; @@ -23,26 +24,11 @@ const USD_VALUATION_SCALE: u32 = 8; /// liquidation-threshold boundary. const HEALTH_FACTOR_SCALE: u32 = 4; -/// Native on-chain decimal precision for an asset, case-insensitive. -/// -/// Token amounts are stored as `NUMERIC(_, 8)`, but each token has its own true -/// precision (USDC: 6, XLM: 7, BTC: 8, ETH: 18). Anything stored beyond a -/// token's native precision is dust that must not influence a valuation. -/// Unknown assets fall back to the storage scale of 8. -fn token_decimals(asset_code: &str) -> u32 { - match asset_code.to_uppercase().as_str() { - "USDC" | "USDT" => 6, - "XLM" | "STELLAR_XLM" => 7, - "BTC" | "WBTC" => 8, - "ETH" | "WETH" => 18, - _ => 8, - } -} - /// Round a stored token amount to its native on-chain precision, dropping any /// sub-unit dust that the flat `NUMERIC(_, 8)` storage scale may have retained. +/// Native precision is resolved from the shared [`token_metadata`] registry. fn normalize_amount(amount: Decimal, asset_code: &str) -> Decimal { - amount.round_dp(token_decimals(asset_code)) + amount.round_dp(token_metadata::decimals_for(asset_code)) } /// Value `amount` of `asset_code` in USD at `price`, normalizing the amount to @@ -65,19 +51,6 @@ fn compute_health_factor( Ok(hf.round_dp(HEALTH_FACTOR_SCALE)) } -/// Liquidation threshold for a given collateral asset, case-insensitive. More -/// volatile assets carry a lower threshold; unknown assets fall back to the -/// engine-wide `fallback` threshold. -fn liquidation_threshold_for_asset(asset_code: &str, fallback: Decimal) -> Decimal { - match asset_code.to_uppercase().as_str() { - "USDC" => Decimal::new(95, 2), // 0.95 - "ETH" | "WETH" => Decimal::new(85, 2), // 0.85 - "BTC" | "WBTC" => Decimal::new(85, 2), // 0.85 - "XLM" | "STELLAR_XLM" => Decimal::new(80, 2), // 0.80 - _ => fallback, - } -} - /// Outcome of evaluating a single borrowing position's collateral health. #[derive(Debug, Clone, PartialEq)] struct PositionAssessment { @@ -124,7 +97,7 @@ fn assess_position( let health_factor = compute_health_factor(collateral_value, debt_value)?; let liquidation_threshold = - liquidation_threshold_for_asset(collateral_asset, fallback_threshold); + token_metadata::liquidation_threshold_for(collateral_asset, fallback_threshold); let is_risky = if risk_override_enabled { false @@ -373,26 +346,6 @@ mod tests { use super::*; use rust_decimal_macros::dec; - #[test] - fn token_decimals_known_assets() { - // Native on-chain precision per asset, case-insensitive. - assert_eq!(token_decimals("USDC"), 6); - assert_eq!(token_decimals("usdc"), 6); - assert_eq!(token_decimals("USDT"), 6); - assert_eq!(token_decimals("BTC"), 8); - assert_eq!(token_decimals("WBTC"), 8); - assert_eq!(token_decimals("ETH"), 18); - assert_eq!(token_decimals("WETH"), 18); - assert_eq!(token_decimals("XLM"), 7); - assert_eq!(token_decimals("STELLAR_XLM"), 7); - } - - #[test] - fn token_decimals_unknown_asset_defaults_to_storage_scale() { - // Unknown assets fall back to the NUMERIC(_, 8) storage scale. - assert_eq!(token_decimals("DOGE"), 8); - } - #[test] fn normalize_amount_drops_dust_below_token_precision() { // USDC has 6 decimals; digits beyond that are dust and must be dropped. @@ -448,48 +401,6 @@ mod tests { assert!(compute_health_factor(dec!(100), dec!(0)).is_err()); } - // --- liquidation_threshold_for_asset --- - - #[test] - fn liquidation_threshold_uses_asset_specific_values() { - let fallback = dec!(0.90); - assert_eq!( - liquidation_threshold_for_asset("USDC", fallback), - dec!(0.95) - ); - assert_eq!(liquidation_threshold_for_asset("ETH", fallback), dec!(0.85)); - assert_eq!( - liquidation_threshold_for_asset("WETH", fallback), - dec!(0.85) - ); - assert_eq!(liquidation_threshold_for_asset("BTC", fallback), dec!(0.85)); - assert_eq!( - liquidation_threshold_for_asset("WBTC", fallback), - dec!(0.85) - ); - assert_eq!(liquidation_threshold_for_asset("XLM", fallback), dec!(0.80)); - assert_eq!( - liquidation_threshold_for_asset("STELLAR_XLM", fallback), - dec!(0.80) - ); - } - - #[test] - fn liquidation_threshold_is_case_insensitive() { - let fallback = dec!(0.90); - assert_eq!( - liquidation_threshold_for_asset("usdc", fallback), - dec!(0.95) - ); - assert_eq!(liquidation_threshold_for_asset("eth", fallback), dec!(0.85)); - } - - #[test] - fn liquidation_threshold_unknown_asset_uses_fallback() { - let fallback = dec!(0.77); - assert_eq!(liquidation_threshold_for_asset("DOGE", fallback), fallback); - } - // --- assess_position --- #[test] diff --git a/backend/src/token_metadata.rs b/backend/src/token_metadata.rs new file mode 100644 index 000000000..2481c5663 --- /dev/null +++ b/backend/src/token_metadata.rs @@ -0,0 +1,334 @@ +//! Centralized per-asset metadata for the lending and risk subsystems. +//! +//! Token amounts are persisted at a flat `NUMERIC(_, 8)` scale, but each asset +//! has its own native on-chain precision and its own risk profile. Scattering +//! those facts across `match` arms in the risk engine, liquidation bot, and +//! collateral valuation invites drift: one site learns about a new token while +//! another silently treats it as unknown. +//! +//! This module is the single source of truth. Look an asset up once and read +//! its decimals, liquidation threshold, and classification from one place. +//! +//! Lookups are case-insensitive and alias-aware: wrapped representations such as +//! `WETH` / `WBTC` and chain-qualified symbols such as `STELLAR_XLM` resolve to +//! their canonical asset (`ETH`, `BTC`, `XLM`) and share its metadata. + +use rust_decimal::Decimal; + +/// Decimal precision assumed for assets not present in the [`REGISTRY`]. This +/// mirrors the `NUMERIC(_, 8)` storage scale used for token amounts, so an +/// unrecognized asset is valued at its stored precision rather than being +/// rejected. +pub const DEFAULT_DECIMALS: u32 = 8; + +/// Scale used to express a liquidation threshold as an integral basis-point +/// count (e.g. `9500` => `0.9500`). Keeping the table integral lets the registry +/// stay a `const` without const-`Decimal` construction. +const THRESHOLD_SCALE: u32 = 4; + +/// Public, read-only view of an asset's metadata, resolved through aliases. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TokenMetadata { + /// Canonical uppercase symbol (e.g. `ETH`, even when looked up as `weth`). + pub symbol: &'static str, + /// Native on-chain decimal precision. + pub decimals: u32, + /// Liquidation threshold as a fraction in `(0, 1]`, or `None` when the asset + /// defers to the engine-wide fallback threshold. + pub liquidation_threshold: Option, + /// Whether the asset is a fiat-pegged stablecoin. + pub is_stablecoin: bool, +} + +/// Internal registry row. `aliases` are alternate codes that resolve to +/// `symbol`; matching is case-insensitive. +struct AssetEntry { + symbol: &'static str, + aliases: &'static [&'static str], + decimals: u32, + liquidation_threshold_bps: Option, + is_stablecoin: bool, +} + +/// The supported-asset table. Add new assets here and every consumer of this +/// module picks them up automatically. +const REGISTRY: &[AssetEntry] = &[ + AssetEntry { + symbol: "USDC", + aliases: &[], + decimals: 6, + liquidation_threshold_bps: Some(9500), // 0.95 + is_stablecoin: true, + }, + AssetEntry { + // Supported for valuation precision; liquidation threshold defers to the + // engine-wide fallback (preserving prior risk-engine behavior). + symbol: "USDT", + aliases: &[], + decimals: 6, + liquidation_threshold_bps: None, + is_stablecoin: true, + }, + AssetEntry { + symbol: "XLM", + aliases: &["STELLAR_XLM"], + decimals: 7, + liquidation_threshold_bps: Some(8000), // 0.80 + is_stablecoin: false, + }, + AssetEntry { + symbol: "BTC", + aliases: &["WBTC"], + decimals: 8, + liquidation_threshold_bps: Some(8500), // 0.85 + is_stablecoin: false, + }, + AssetEntry { + symbol: "ETH", + aliases: &["WETH"], + decimals: 18, + liquidation_threshold_bps: Some(8500), // 0.85 + is_stablecoin: false, + }, +]; + +/// Convert an integral basis-point threshold into a fractional [`Decimal`]. +fn bps_to_decimal(bps: u32) -> Decimal { + Decimal::new(bps as i64, THRESHOLD_SCALE) +} + +/// Find the registry row whose canonical symbol or aliases match `asset_code`, +/// case-insensitively. +fn find_entry(asset_code: &str) -> Option<&'static AssetEntry> { + let upper = asset_code.to_uppercase(); + REGISTRY.iter().find(|entry| { + entry.symbol == upper || entry.aliases.iter().any(|a| a.to_uppercase() == upper) + }) +} + +/// Resolve full [`TokenMetadata`] for an asset code, or `None` if unsupported. +pub fn lookup(asset_code: &str) -> Option { + find_entry(asset_code).map(|entry| TokenMetadata { + symbol: entry.symbol, + decimals: entry.decimals, + liquidation_threshold: entry.liquidation_threshold_bps.map(bps_to_decimal), + is_stablecoin: entry.is_stablecoin, + }) +} + +/// Native decimal precision for an asset, falling back to [`DEFAULT_DECIMALS`] +/// for unknown assets. +pub fn decimals_for(asset_code: &str) -> u32 { + find_entry(asset_code) + .map(|entry| entry.decimals) + .unwrap_or(DEFAULT_DECIMALS) +} + +/// Liquidation threshold for an asset, using `fallback` when the asset is +/// unknown or defers its threshold to the engine-wide value. +pub fn liquidation_threshold_for(asset_code: &str, fallback: Decimal) -> Decimal { + find_entry(asset_code) + .and_then(|entry| entry.liquidation_threshold_bps) + .map(bps_to_decimal) + .unwrap_or(fallback) +} + +/// Canonical uppercase symbol for an asset code (resolving aliases), or `None`. +pub fn canonical_symbol(asset_code: &str) -> Option<&'static str> { + find_entry(asset_code).map(|entry| entry.symbol) +} + +/// Whether the asset is a known fiat-pegged stablecoin. +pub fn is_stablecoin(asset_code: &str) -> bool { + find_entry(asset_code).is_some_and(|entry| entry.is_stablecoin) +} + +/// Whether the asset appears in the registry (directly or via an alias). +pub fn is_supported(asset_code: &str) -> bool { + find_entry(asset_code).is_some() +} + +/// All canonical symbols known to the registry. +pub fn supported_symbols() -> Vec<&'static str> { + REGISTRY.iter().map(|entry| entry.symbol).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal_macros::dec; + + // --- lookup --- + + #[test] + fn lookup_known_asset_returns_metadata() { + let usdc = lookup("USDC").expect("USDC is a supported asset"); + assert_eq!(usdc.symbol, "USDC"); + assert_eq!(usdc.decimals, 6); + assert_eq!(usdc.liquidation_threshold, Some(dec!(0.95))); + assert!(usdc.is_stablecoin); + } + + #[test] + fn lookup_is_case_insensitive() { + assert_eq!(lookup("usdc").unwrap().symbol, "USDC"); + assert_eq!(lookup("UsDc").unwrap().symbol, "USDC"); + assert_eq!(lookup("eth").unwrap().symbol, "ETH"); + } + + #[test] + fn lookup_resolves_wrapped_aliases_to_canonical_symbol() { + // Wrapped variants share the canonical asset's metadata. + let weth = lookup("WETH").unwrap(); + assert_eq!(weth.symbol, "ETH"); + assert_eq!(weth.decimals, 18); + + let wbtc = lookup("WBTC").unwrap(); + assert_eq!(wbtc.symbol, "BTC"); + assert_eq!(wbtc.decimals, 8); + + let xlm = lookup("STELLAR_XLM").unwrap(); + assert_eq!(xlm.symbol, "XLM"); + assert_eq!(xlm.decimals, 7); + } + + #[test] + fn lookup_unknown_asset_returns_none() { + assert!(lookup("DOGE").is_none()); + assert!(lookup("").is_none()); + } + + // --- decimals_for --- + + #[test] + fn decimals_for_known_assets() { + assert_eq!(decimals_for("USDC"), 6); + assert_eq!(decimals_for("USDT"), 6); + assert_eq!(decimals_for("XLM"), 7); + assert_eq!(decimals_for("BTC"), 8); + assert_eq!(decimals_for("ETH"), 18); + assert_eq!(decimals_for("WETH"), 18); + } + + #[test] + fn decimals_for_unknown_returns_storage_default() { + assert_eq!(decimals_for("DOGE"), DEFAULT_DECIMALS); + assert_eq!(DEFAULT_DECIMALS, 8); + } + + // --- liquidation_threshold_for --- + + #[test] + fn liquidation_threshold_for_known_assets() { + let fallback = dec!(0.90); + assert_eq!(liquidation_threshold_for("USDC", fallback), dec!(0.95)); + assert_eq!(liquidation_threshold_for("ETH", fallback), dec!(0.85)); + assert_eq!(liquidation_threshold_for("WBTC", fallback), dec!(0.85)); + assert_eq!(liquidation_threshold_for("XLM", fallback), dec!(0.80)); + } + + #[test] + fn liquidation_threshold_for_asset_without_one_uses_fallback() { + // USDT is supported for decimals but defers its liquidation threshold to + // the engine-wide fallback. + let fallback = dec!(0.88); + assert_eq!(liquidation_threshold_for("USDT", fallback), fallback); + } + + #[test] + fn liquidation_threshold_for_unknown_uses_fallback() { + let fallback = dec!(0.77); + assert_eq!(liquidation_threshold_for("DOGE", fallback), fallback); + } + + // --- canonical_symbol --- + + #[test] + fn canonical_symbol_resolves_aliases() { + assert_eq!(canonical_symbol("weth"), Some("ETH")); + assert_eq!(canonical_symbol("WBTC"), Some("BTC")); + assert_eq!(canonical_symbol("usdc"), Some("USDC")); + } + + #[test] + fn canonical_symbol_unknown_is_none() { + assert_eq!(canonical_symbol("DOGE"), None); + } + + // --- is_stablecoin / is_supported --- + + #[test] + fn is_stablecoin_classifies_correctly() { + assert!(is_stablecoin("USDC")); + assert!(is_stablecoin("usdt")); + assert!(!is_stablecoin("ETH")); + assert!(!is_stablecoin("DOGE")); + } + + #[test] + fn is_supported_reflects_registry() { + assert!(is_supported("USDC")); + assert!(is_supported("STELLAR_XLM")); + assert!(!is_supported("DOGE")); + } + + #[test] + fn supported_symbols_lists_unique_canonical_symbols() { + let symbols = supported_symbols(); + assert!(symbols.contains(&"USDC")); + assert!(symbols.contains(&"ETH")); + assert!(symbols.contains(&"BTC")); + assert!(symbols.contains(&"XLM")); + // No canonical symbol appears twice. + let mut deduped = symbols.clone(); + deduped.sort_unstable(); + deduped.dedup(); + assert_eq!(deduped.len(), symbols.len()); + } + + // --- registry invariants --- + + #[test] + fn all_thresholds_are_within_the_unit_interval() { + for entry in REGISTRY { + if let Some(bps) = entry.liquidation_threshold_bps { + let t = bps_to_decimal(bps); + assert!( + t > dec!(0) && t <= dec!(1), + "threshold for {} out of (0, 1]: {t}", + entry.symbol + ); + } + } + } + + #[test] + fn all_decimals_are_realistic() { + for entry in REGISTRY { + assert!( + entry.decimals <= 18, + "decimals for {} unrealistically high: {}", + entry.symbol, + entry.decimals + ); + } + } + + #[test] + fn aliases_never_collide_with_a_canonical_symbol() { + for entry in REGISTRY { + for alias in entry.aliases { + assert_ne!( + alias.to_uppercase(), + entry.symbol, + "alias duplicates its own symbol: {}", + entry.symbol + ); + assert!( + !REGISTRY.iter().any(|e| e.symbol == alias.to_uppercase()), + "alias {alias} collides with a canonical symbol" + ); + } + } + } +} From 1fb698c5ceb3ac022905425a3ad82d5ff0a3b55c Mon Sep 17 00:00:00 2001 From: martinvibes Date: Thu, 18 Jun 2026 11:59:20 +0100 Subject: [PATCH 4/5] refactor: remove token_metadata module and implement principal decimal handling in borrowing contract --- backend/src/lib.rs | 1 - backend/src/risk_engine.rs | 378 +--- backend/src/token_metadata.rs | 334 --- contracts/borrowing-contract/src/lib.rs | 197 +- contracts/borrowing-contract/src/test.rs | 98 + .../test/test_create_loan.1.json | 47 + .../test_extend_inactive_loan_fails.1.json | 47 + .../test/test_extend_loan.1.json | 47 + .../test_extend_loan_fee_calculation.1.json | 47 + .../test_extend_loan_limit_reached.1.json | 47 + ...ax_additional_borrow_inactive_fails.1.json | 47 + .../test/test_global_pause.1.json | 94 + ...actor_normalizes_principal_decimals.1.json | 1938 +++++++++++++++++ .../test_increase_inactive_loan_fails.1.json | 47 + .../test/test_increase_loan_amount.1.json | 47 + ...rease_loan_exceeds_collateral_fails.1.json | 47 + ..._increase_loan_invalid_amount_fails.1.json | 47 + .../test/test_insufficient_collateral.1.json | 47 + .../test/test_liquidation.1.json | 94 + .../test/test_liquidation_auction.1.json | 141 ++ ...idation_auction_zero_duration_fails.1.json | 1858 ++++++++++++++++ ...test_oracle_and_volatility_combined.1.json | 47 + .../test_oracle_disabled_by_default.1.json | 47 + ...est_oracle_fresh_allows_create_loan.1.json | 47 + .../test/test_partial_liquidation.1.json | 141 ++ .../test/test_repay_loan.1.json | 47 + ...t_principal_decimals_requires_admin.1.json | 1010 +++++++++ .../test_unpause_restores_create_loan.1.json | 47 + .../test/test_vault_pause.1.json | 47 + ...uffer_increases_required_collateral.1.json | 47 + ...fer_loan_with_sufficient_collateral.1.json | 47 + ...uffer_reduces_max_additional_borrow.1.json | 47 + 32 files changed, 6502 insertions(+), 722 deletions(-) delete mode 100644 backend/src/token_metadata.rs create mode 100644 contracts/borrowing-contract/test_snapshots/test/test_health_factor_normalizes_principal_decimals.1.json create mode 100644 contracts/borrowing-contract/test_snapshots/test/test_liquidation_auction_zero_duration_fails.1.json create mode 100644 contracts/borrowing-contract/test_snapshots/test/test_set_principal_decimals_requires_admin.1.json diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 2ddaa90ce..f3b8c8476 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -50,7 +50,6 @@ pub mod session; pub mod stellar; pub mod stress_testing; pub mod telemetry; -pub mod token_metadata; pub mod validation; pub mod webhook; pub mod will_audit; diff --git a/backend/src/risk_engine.rs b/backend/src/risk_engine.rs index aa40e161e..a6f91cc11 100644 --- a/backend/src/risk_engine.rs +++ b/backend/src/risk_engine.rs @@ -3,8 +3,6 @@ use crate::notifications::{ audit_action, entity_type, notif_type, AuditLogService, NotificationService, }; use crate::price_feed::PriceFeedService; -use crate::safe_math::SafeMath; -use crate::token_metadata; use chrono::Utc; use rust_decimal::Decimal; use sqlx::PgPool; @@ -12,108 +10,6 @@ use std::sync::Arc; use std::time::Duration; use tracing::{error, info, warn}; -/// Canonical scale (decimal places) for all USD valuations. Bounding the scale -/// of every monetary value keeps multiplications well within `Decimal`'s -/// 28-significant-digit budget and makes valuations of differently-scaled -/// tokens directly comparable. -const USD_VALUATION_SCALE: u32 = 8; - -/// Scale of the `plans.health_factor` column (`DECIMAL(10, 4)`). The health -/// factor is rounded to this scale before *both* the risk comparison and -/// persistence so the stored value and the risk flag can never disagree at the -/// liquidation-threshold boundary. -const HEALTH_FACTOR_SCALE: u32 = 4; - -/// Round a stored token amount to its native on-chain precision, dropping any -/// sub-unit dust that the flat `NUMERIC(_, 8)` storage scale may have retained. -/// Native precision is resolved from the shared [`token_metadata`] registry. -fn normalize_amount(amount: Decimal, asset_code: &str) -> Decimal { - amount.round_dp(token_metadata::decimals_for(asset_code)) -} - -/// Value `amount` of `asset_code` in USD at `price`, normalizing the amount to -/// the token's native precision first and returning the result at the canonical -/// USD valuation scale. Uses checked arithmetic so high-value tokens cannot -/// overflow. -fn value_in_usd(amount: Decimal, price: Decimal, asset_code: &str) -> Result { - let normalized = normalize_amount(amount, asset_code); - let value = SafeMath::mul(normalized, price)?; - Ok(value.round_dp(USD_VALUATION_SCALE)) -} - -/// Compute a health factor (`collateral_value / debt_value`) rounded to the -/// health-factor storage scale. Errors on zero debt via `SafeMath::div`. -fn compute_health_factor( - collateral_value: Decimal, - debt_value: Decimal, -) -> Result { - let hf = SafeMath::div(collateral_value, debt_value)?; - Ok(hf.round_dp(HEALTH_FACTOR_SCALE)) -} - -/// Outcome of evaluating a single borrowing position's collateral health. -#[derive(Debug, Clone, PartialEq)] -struct PositionAssessment { - /// USD value of the collateral at the canonical valuation scale. - collateral_value: Decimal, - /// USD value of the outstanding debt at the canonical valuation scale. - debt_value: Decimal, - /// Collateral-to-debt health factor at the storage scale. - health_factor: Decimal, - /// Liquidation threshold applied for this collateral asset. - liquidation_threshold: Decimal, - /// Whether the position should be flagged as risky. - is_risky: bool, -} - -/// Pure, DB-free evaluation of a borrowing position. -/// -/// Values both sides in USD (normalizing each amount to its token's native -/// precision via [`value_in_usd`]), derives the health factor, and decides -/// whether the position is risky against the collateral asset's liquidation -/// threshold. The comparison is strict (`<`), so a position sitting exactly at -/// its threshold is *not* flagged. -/// -/// Returns `Ok(None)` when there is no positive debt to assess. Returns `Err` -/// if any valuation overflows or debt-side arithmetic is invalid, so callers -/// can skip the position instead of panicking. -#[allow(clippy::too_many_arguments)] -fn assess_position( - collateral_amount: Decimal, - collateral_asset: &str, - collateral_price: Decimal, - debt_amount: Decimal, - borrow_asset: &str, - borrow_price: Decimal, - fallback_threshold: Decimal, - risk_override_enabled: bool, -) -> Result, ApiError> { - let collateral_value = value_in_usd(collateral_amount, collateral_price, collateral_asset)?; - let debt_value = value_in_usd(debt_amount, borrow_price, borrow_asset)?; - - if debt_value <= Decimal::ZERO { - return Ok(None); - } - - let health_factor = compute_health_factor(collateral_value, debt_value)?; - let liquidation_threshold = - token_metadata::liquidation_threshold_for(collateral_asset, fallback_threshold); - - let is_risky = if risk_override_enabled { - false - } else { - health_factor < liquidation_threshold - }; - - Ok(Some(PositionAssessment { - collateral_value, - debt_value, - health_factor, - liquidation_threshold, - is_risky, - })) -} - pub struct RiskEngine { db: PgPool, price_feed: Arc, @@ -245,39 +141,33 @@ impl RiskEngine { } }; - // Evaluate the position: value both sides in USD (normalizing each - // amount to its token's native precision via checked arithmetic), - // derive the health factor, and decide risk against the collateral - // asset's liquidation threshold. A `None` result means there is no - // outstanding debt; an `Err` means valuation arithmetic failed and - // the plan is skipped rather than risking a panic. - let collat_amount = loan.collateral_amount.unwrap_or(Decimal::ZERO); - let should_skip_risk_check = loan.risk_override_enabled.unwrap_or(false); - let assessment = match assess_position( - collat_amount, - &collat_asset, - collat_price, - loan.total_debt, - &loan.borrow_asset, - borrow_price, - self.liquidation_threshold, - should_skip_risk_check, - ) { - Ok(Some(a)) => a, - Ok(None) => continue, // no outstanding debt to assess - Err(e) => { - warn!( - "Risk Engine: skipping plan {} — position assessment failed: {}", - loan.plan_id, e - ); - continue; - } - }; - - let health_factor = assessment.health_factor; - let is_now_risky = assessment.is_risky; + // Values are aggregated using numeric casts, yielding Decimal safely via sqlx mapping + let collat_value = loan.collateral_amount.unwrap_or(Decimal::ZERO) * collat_price; + let debt_value = loan.total_debt * borrow_price; + + if debt_value > Decimal::ZERO { + let health_factor = collat_value / debt_value; + + // Skip risk flagging if risk override is enabled + let should_skip_risk_check = loan.risk_override_enabled.unwrap_or(false); + + // Determine liquidation threshold based on collateral asset when possible + let asset_upper = collat_asset.to_uppercase(); + let liquidation_threshold_for_asset = match asset_upper.as_str() { + "USDC" => Decimal::new(95, 2), // 0.95 + "ETH" | "WETH" => Decimal::new(85, 2), // 0.85 + "BTC" | "WBTC" => Decimal::new(85, 2), // 0.85 + "XLM" | "STELLAR_XLM" => Decimal::new(80, 2), // 0.80 + // Fallback to engine-wide threshold if unknown + _ => self.liquidation_threshold, + }; + + let is_now_risky = if should_skip_risk_check { + false // Override: never mark as risky + } else { + health_factor < liquidation_threshold_for_asset + }; - { // Update database state sqlx::query( r#" @@ -340,219 +230,3 @@ impl RiskEngine { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use rust_decimal_macros::dec; - - #[test] - fn normalize_amount_drops_dust_below_token_precision() { - // USDC has 6 decimals; digits beyond that are dust and must be dropped. - assert_eq!( - normalize_amount(dec!(100.123456789), "USDC"), - dec!(100.123457) - ); - // XLM has 7 decimals. - assert_eq!(normalize_amount(dec!(50.12345678), "XLM"), dec!(50.1234568)); - } - - #[test] - fn normalize_amount_preserves_high_precision_tokens() { - // ETH (18 decimals) stored at scale 8 keeps all stored digits. - assert_eq!(normalize_amount(dec!(1.23456789), "ETH"), dec!(1.23456789)); - // BTC (8 decimals) keeps its full stored precision. - assert_eq!(normalize_amount(dec!(0.12345678), "BTC"), dec!(0.12345678)); - } - - #[test] - fn value_in_usd_normalizes_then_prices_at_canonical_scale() { - // 100.1234569 USDC (normalized to 100.123457) * $1.00 = 100.12345700 - let v = value_in_usd(dec!(100.1234569), dec!(1.00), "USDC").unwrap(); - assert_eq!(v, dec!(100.12345700)); - assert_eq!(v.scale(), USD_VALUATION_SCALE); - } - - #[test] - fn value_in_usd_handles_high_value_tokens_without_overflow() { - // 1000 BTC at $1,000,000 — large product must not overflow. - let v = value_in_usd(dec!(1000), dec!(1000000), "BTC").unwrap(); - assert_eq!(v, dec!(1000000000.00000000)); - } - - #[test] - fn compute_health_factor_rounds_to_storage_scale() { - // 150 collateral / 100 debt = 1.5, within the DECIMAL(10,4) storage scale. - let hf = compute_health_factor(dec!(150), dec!(100)).unwrap(); - assert_eq!(hf, dec!(1.5)); - } - - #[test] - fn compute_health_factor_is_consistent_at_threshold_boundary() { - // A repeating ratio must round to at most the storage scale so the - // persisted value and the risk flag agree. 1000/3000 = 0.3333... -> 0.3333. - let hf = compute_health_factor(dec!(1000), dec!(3000)).unwrap(); - assert_eq!(hf, dec!(0.3333)); - assert!(hf.scale() <= HEALTH_FACTOR_SCALE); - } - - #[test] - fn compute_health_factor_zero_debt_is_error() { - assert!(compute_health_factor(dec!(100), dec!(0)).is_err()); - } - - // --- assess_position --- - - #[test] - fn assess_position_healthy_is_not_risky() { - // 200 USDC collateral vs 100 USDC debt at $1 -> HF 2.0, well above 0.95. - let a = assess_position( - dec!(200), - "USDC", - dec!(1), - dec!(100), - "USDC", - dec!(1), - dec!(0.90), - false, - ) - .unwrap() - .unwrap(); - assert_eq!(a.collateral_value, dec!(200.00000000)); - assert_eq!(a.debt_value, dec!(100.00000000)); - assert_eq!(a.health_factor, dec!(2)); - assert_eq!(a.liquidation_threshold, dec!(0.95)); - assert!(!a.is_risky); - } - - #[test] - fn assess_position_undercollateralized_is_risky() { - // 90 USDC collateral vs 100 USDC debt -> HF 0.90 < 0.95 threshold. - let a = assess_position( - dec!(90), - "USDC", - dec!(1), - dec!(100), - "USDC", - dec!(1), - dec!(0.50), - false, - ) - .unwrap() - .unwrap(); - assert_eq!(a.health_factor, dec!(0.9)); - assert!(a.is_risky); - } - - #[test] - fn assess_position_at_exact_threshold_is_not_risky() { - // HF exactly equal to the threshold must NOT trip (comparison is strict <). - // 95 USDC vs 100 USDC -> HF 0.95 == USDC threshold 0.95. - let a = assess_position( - dec!(95), - "USDC", - dec!(1), - dec!(100), - "USDC", - dec!(1), - dec!(0.50), - false, - ) - .unwrap() - .unwrap(); - assert_eq!(a.health_factor, dec!(0.95)); - assert!(!a.is_risky); - } - - #[test] - fn assess_position_override_never_risky() { - // Deeply undercollateralized, but override disables risk flagging. - let a = assess_position( - dec!(1), - "USDC", - dec!(1), - dec!(100), - "USDC", - dec!(1), - dec!(0.50), - true, - ) - .unwrap() - .unwrap(); - assert!(a.health_factor < a.liquidation_threshold); - assert!(!a.is_risky); - } - - #[test] - fn assess_position_zero_debt_returns_none() { - let a = assess_position( - dec!(100), - "USDC", - dec!(1), - dec!(0), - "USDC", - dec!(1), - dec!(0.90), - false, - ) - .unwrap(); - assert!(a.is_none()); - } - - #[test] - fn assess_position_uses_collateral_asset_threshold() { - // XLM collateral threshold is 0.80. HF 0.82 is above it -> not risky, - // even though it would be risky under USDC's stricter 0.95. - let a = assess_position( - dec!(82), - "XLM", - dec!(1), - dec!(100), - "USDC", - dec!(1), - dec!(0.50), - false, - ) - .unwrap() - .unwrap(); - assert_eq!(a.liquidation_threshold, dec!(0.80)); - assert_eq!(a.health_factor, dec!(0.82)); - assert!(!a.is_risky); - } - - #[test] - fn assess_position_normalizes_token_decimals() { - // USDC dust beyond 6 decimals is dropped before valuation. - let a = assess_position( - dec!(100.12345678), - "USDC", - dec!(1), - dec!(50), - "USDC", - dec!(1), - dec!(0.90), - false, - ) - .unwrap() - .unwrap(); - assert_eq!(a.collateral_value, dec!(100.12345700)); - } - - #[test] - fn assess_position_overflow_is_error() { - // A price that overflows when multiplied by a huge amount must error, - // not panic. - let huge = Decimal::MAX; - assert!(assess_position( - huge, - "USDC", - dec!(1000000), - dec!(100), - "USDC", - dec!(1), - dec!(0.90), - false, - ) - .is_err()); - } -} diff --git a/backend/src/token_metadata.rs b/backend/src/token_metadata.rs deleted file mode 100644 index 2481c5663..000000000 --- a/backend/src/token_metadata.rs +++ /dev/null @@ -1,334 +0,0 @@ -//! Centralized per-asset metadata for the lending and risk subsystems. -//! -//! Token amounts are persisted at a flat `NUMERIC(_, 8)` scale, but each asset -//! has its own native on-chain precision and its own risk profile. Scattering -//! those facts across `match` arms in the risk engine, liquidation bot, and -//! collateral valuation invites drift: one site learns about a new token while -//! another silently treats it as unknown. -//! -//! This module is the single source of truth. Look an asset up once and read -//! its decimals, liquidation threshold, and classification from one place. -//! -//! Lookups are case-insensitive and alias-aware: wrapped representations such as -//! `WETH` / `WBTC` and chain-qualified symbols such as `STELLAR_XLM` resolve to -//! their canonical asset (`ETH`, `BTC`, `XLM`) and share its metadata. - -use rust_decimal::Decimal; - -/// Decimal precision assumed for assets not present in the [`REGISTRY`]. This -/// mirrors the `NUMERIC(_, 8)` storage scale used for token amounts, so an -/// unrecognized asset is valued at its stored precision rather than being -/// rejected. -pub const DEFAULT_DECIMALS: u32 = 8; - -/// Scale used to express a liquidation threshold as an integral basis-point -/// count (e.g. `9500` => `0.9500`). Keeping the table integral lets the registry -/// stay a `const` without const-`Decimal` construction. -const THRESHOLD_SCALE: u32 = 4; - -/// Public, read-only view of an asset's metadata, resolved through aliases. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TokenMetadata { - /// Canonical uppercase symbol (e.g. `ETH`, even when looked up as `weth`). - pub symbol: &'static str, - /// Native on-chain decimal precision. - pub decimals: u32, - /// Liquidation threshold as a fraction in `(0, 1]`, or `None` when the asset - /// defers to the engine-wide fallback threshold. - pub liquidation_threshold: Option, - /// Whether the asset is a fiat-pegged stablecoin. - pub is_stablecoin: bool, -} - -/// Internal registry row. `aliases` are alternate codes that resolve to -/// `symbol`; matching is case-insensitive. -struct AssetEntry { - symbol: &'static str, - aliases: &'static [&'static str], - decimals: u32, - liquidation_threshold_bps: Option, - is_stablecoin: bool, -} - -/// The supported-asset table. Add new assets here and every consumer of this -/// module picks them up automatically. -const REGISTRY: &[AssetEntry] = &[ - AssetEntry { - symbol: "USDC", - aliases: &[], - decimals: 6, - liquidation_threshold_bps: Some(9500), // 0.95 - is_stablecoin: true, - }, - AssetEntry { - // Supported for valuation precision; liquidation threshold defers to the - // engine-wide fallback (preserving prior risk-engine behavior). - symbol: "USDT", - aliases: &[], - decimals: 6, - liquidation_threshold_bps: None, - is_stablecoin: true, - }, - AssetEntry { - symbol: "XLM", - aliases: &["STELLAR_XLM"], - decimals: 7, - liquidation_threshold_bps: Some(8000), // 0.80 - is_stablecoin: false, - }, - AssetEntry { - symbol: "BTC", - aliases: &["WBTC"], - decimals: 8, - liquidation_threshold_bps: Some(8500), // 0.85 - is_stablecoin: false, - }, - AssetEntry { - symbol: "ETH", - aliases: &["WETH"], - decimals: 18, - liquidation_threshold_bps: Some(8500), // 0.85 - is_stablecoin: false, - }, -]; - -/// Convert an integral basis-point threshold into a fractional [`Decimal`]. -fn bps_to_decimal(bps: u32) -> Decimal { - Decimal::new(bps as i64, THRESHOLD_SCALE) -} - -/// Find the registry row whose canonical symbol or aliases match `asset_code`, -/// case-insensitively. -fn find_entry(asset_code: &str) -> Option<&'static AssetEntry> { - let upper = asset_code.to_uppercase(); - REGISTRY.iter().find(|entry| { - entry.symbol == upper || entry.aliases.iter().any(|a| a.to_uppercase() == upper) - }) -} - -/// Resolve full [`TokenMetadata`] for an asset code, or `None` if unsupported. -pub fn lookup(asset_code: &str) -> Option { - find_entry(asset_code).map(|entry| TokenMetadata { - symbol: entry.symbol, - decimals: entry.decimals, - liquidation_threshold: entry.liquidation_threshold_bps.map(bps_to_decimal), - is_stablecoin: entry.is_stablecoin, - }) -} - -/// Native decimal precision for an asset, falling back to [`DEFAULT_DECIMALS`] -/// for unknown assets. -pub fn decimals_for(asset_code: &str) -> u32 { - find_entry(asset_code) - .map(|entry| entry.decimals) - .unwrap_or(DEFAULT_DECIMALS) -} - -/// Liquidation threshold for an asset, using `fallback` when the asset is -/// unknown or defers its threshold to the engine-wide value. -pub fn liquidation_threshold_for(asset_code: &str, fallback: Decimal) -> Decimal { - find_entry(asset_code) - .and_then(|entry| entry.liquidation_threshold_bps) - .map(bps_to_decimal) - .unwrap_or(fallback) -} - -/// Canonical uppercase symbol for an asset code (resolving aliases), or `None`. -pub fn canonical_symbol(asset_code: &str) -> Option<&'static str> { - find_entry(asset_code).map(|entry| entry.symbol) -} - -/// Whether the asset is a known fiat-pegged stablecoin. -pub fn is_stablecoin(asset_code: &str) -> bool { - find_entry(asset_code).is_some_and(|entry| entry.is_stablecoin) -} - -/// Whether the asset appears in the registry (directly or via an alias). -pub fn is_supported(asset_code: &str) -> bool { - find_entry(asset_code).is_some() -} - -/// All canonical symbols known to the registry. -pub fn supported_symbols() -> Vec<&'static str> { - REGISTRY.iter().map(|entry| entry.symbol).collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use rust_decimal_macros::dec; - - // --- lookup --- - - #[test] - fn lookup_known_asset_returns_metadata() { - let usdc = lookup("USDC").expect("USDC is a supported asset"); - assert_eq!(usdc.symbol, "USDC"); - assert_eq!(usdc.decimals, 6); - assert_eq!(usdc.liquidation_threshold, Some(dec!(0.95))); - assert!(usdc.is_stablecoin); - } - - #[test] - fn lookup_is_case_insensitive() { - assert_eq!(lookup("usdc").unwrap().symbol, "USDC"); - assert_eq!(lookup("UsDc").unwrap().symbol, "USDC"); - assert_eq!(lookup("eth").unwrap().symbol, "ETH"); - } - - #[test] - fn lookup_resolves_wrapped_aliases_to_canonical_symbol() { - // Wrapped variants share the canonical asset's metadata. - let weth = lookup("WETH").unwrap(); - assert_eq!(weth.symbol, "ETH"); - assert_eq!(weth.decimals, 18); - - let wbtc = lookup("WBTC").unwrap(); - assert_eq!(wbtc.symbol, "BTC"); - assert_eq!(wbtc.decimals, 8); - - let xlm = lookup("STELLAR_XLM").unwrap(); - assert_eq!(xlm.symbol, "XLM"); - assert_eq!(xlm.decimals, 7); - } - - #[test] - fn lookup_unknown_asset_returns_none() { - assert!(lookup("DOGE").is_none()); - assert!(lookup("").is_none()); - } - - // --- decimals_for --- - - #[test] - fn decimals_for_known_assets() { - assert_eq!(decimals_for("USDC"), 6); - assert_eq!(decimals_for("USDT"), 6); - assert_eq!(decimals_for("XLM"), 7); - assert_eq!(decimals_for("BTC"), 8); - assert_eq!(decimals_for("ETH"), 18); - assert_eq!(decimals_for("WETH"), 18); - } - - #[test] - fn decimals_for_unknown_returns_storage_default() { - assert_eq!(decimals_for("DOGE"), DEFAULT_DECIMALS); - assert_eq!(DEFAULT_DECIMALS, 8); - } - - // --- liquidation_threshold_for --- - - #[test] - fn liquidation_threshold_for_known_assets() { - let fallback = dec!(0.90); - assert_eq!(liquidation_threshold_for("USDC", fallback), dec!(0.95)); - assert_eq!(liquidation_threshold_for("ETH", fallback), dec!(0.85)); - assert_eq!(liquidation_threshold_for("WBTC", fallback), dec!(0.85)); - assert_eq!(liquidation_threshold_for("XLM", fallback), dec!(0.80)); - } - - #[test] - fn liquidation_threshold_for_asset_without_one_uses_fallback() { - // USDT is supported for decimals but defers its liquidation threshold to - // the engine-wide fallback. - let fallback = dec!(0.88); - assert_eq!(liquidation_threshold_for("USDT", fallback), fallback); - } - - #[test] - fn liquidation_threshold_for_unknown_uses_fallback() { - let fallback = dec!(0.77); - assert_eq!(liquidation_threshold_for("DOGE", fallback), fallback); - } - - // --- canonical_symbol --- - - #[test] - fn canonical_symbol_resolves_aliases() { - assert_eq!(canonical_symbol("weth"), Some("ETH")); - assert_eq!(canonical_symbol("WBTC"), Some("BTC")); - assert_eq!(canonical_symbol("usdc"), Some("USDC")); - } - - #[test] - fn canonical_symbol_unknown_is_none() { - assert_eq!(canonical_symbol("DOGE"), None); - } - - // --- is_stablecoin / is_supported --- - - #[test] - fn is_stablecoin_classifies_correctly() { - assert!(is_stablecoin("USDC")); - assert!(is_stablecoin("usdt")); - assert!(!is_stablecoin("ETH")); - assert!(!is_stablecoin("DOGE")); - } - - #[test] - fn is_supported_reflects_registry() { - assert!(is_supported("USDC")); - assert!(is_supported("STELLAR_XLM")); - assert!(!is_supported("DOGE")); - } - - #[test] - fn supported_symbols_lists_unique_canonical_symbols() { - let symbols = supported_symbols(); - assert!(symbols.contains(&"USDC")); - assert!(symbols.contains(&"ETH")); - assert!(symbols.contains(&"BTC")); - assert!(symbols.contains(&"XLM")); - // No canonical symbol appears twice. - let mut deduped = symbols.clone(); - deduped.sort_unstable(); - deduped.dedup(); - assert_eq!(deduped.len(), symbols.len()); - } - - // --- registry invariants --- - - #[test] - fn all_thresholds_are_within_the_unit_interval() { - for entry in REGISTRY { - if let Some(bps) = entry.liquidation_threshold_bps { - let t = bps_to_decimal(bps); - assert!( - t > dec!(0) && t <= dec!(1), - "threshold for {} out of (0, 1]: {t}", - entry.symbol - ); - } - } - } - - #[test] - fn all_decimals_are_realistic() { - for entry in REGISTRY { - assert!( - entry.decimals <= 18, - "decimals for {} unrealistically high: {}", - entry.symbol, - entry.decimals - ); - } - } - - #[test] - fn aliases_never_collide_with_a_canonical_symbol() { - for entry in REGISTRY { - for alias in entry.aliases { - assert_ne!( - alias.to_uppercase(), - entry.symbol, - "alias duplicates its own symbol: {}", - entry.symbol - ); - assert!( - !REGISTRY.iter().any(|e| e.symbol == alias.to_uppercase()), - "alias {alias} collides with a canonical symbol" - ); - } - } - } -} diff --git a/contracts/borrowing-contract/src/lib.rs b/contracts/borrowing-contract/src/lib.rs index 672a44a8f..30852a48e 100644 --- a/contracts/borrowing-contract/src/lib.rs +++ b/contracts/borrowing-contract/src/lib.rs @@ -20,6 +20,95 @@ pub struct Loan { pub extension_count: u32, } +// ───────────────────────────────────────────────── +// Decimal normalization (#632) +// +// Collateral and debt are denominated in tokens that may carry different +// on-chain precision (e.g. a 6-decimal stablecoin against a 7-decimal SAC). +// Comparing their raw integer amounts directly skews collateralization and +// health-factor math. These pure helpers normalize amounts by their token +// decimals before any cross-asset comparison. +// ───────────────────────────────────────────────── + +/// Health factor is expressed in basis points; `10000` == `1.0x`. +const BPS_DENOMINATOR: u128 = 10_000; + +/// Maximum decimal precision the contract will normalize against. Bounds the +/// scaling factor so it cannot exhaust `u128` and matches the widest precision +/// of common assets (e.g. 18-decimal wrapped tokens). +pub const MAX_SUPPORTED_DECIMALS: u32 = 18; + +/// `10^exp` as a `u128`, or `None` if it overflows. +fn pow10(exp: u32) -> Option { + 10u128.checked_pow(exp) +} + +/// Re-express `amount` (given at `from_decimals` precision) at `to_decimals` +/// precision. Scaling up multiplies, scaling down divides (truncating). Returns +/// `None` on overflow. +pub fn scale_amount(amount: i128, from_decimals: u32, to_decimals: u32) -> Option { + use core::cmp::Ordering; + match to_decimals.cmp(&from_decimals) { + Ordering::Equal => Some(amount), + Ordering::Greater => { + let factor = pow10(to_decimals - from_decimals)? as i128; + amount.checked_mul(factor) + } + Ordering::Less => { + let factor = pow10(from_decimals - to_decimals)? as i128; + Some(amount / factor) + } + } +} + +/// Collateral-to-debt health factor in basis points, normalizing for the +/// (possibly different) decimal precision of the collateral and debt assets. +/// +/// `real_collateral / real_debt = (collateral / debt) * 10^(debt_dec - coll_dec)`. +/// Zero (or negative) debt is treated as maximally healthy (`10000`). Any +/// intermediate overflow saturates to `0`, preserving the contract's existing +/// "unhealthy on arithmetic failure" behavior rather than panicking. +pub fn health_factor_bps( + collateral_amount: i128, + collateral_decimals: u32, + debt: i128, + debt_decimals: u32, +) -> u32 { + if debt <= 0 { + return 10000; + } + + let collateral = collateral_amount.max(0) as u128; + let debt = debt as u128; + + let raw = if debt_decimals >= collateral_decimals { + // Scale the numerator up by the decimal delta. + pow10(debt_decimals - collateral_decimals) + .and_then(|factor| { + collateral + .checked_mul(BPS_DENOMINATOR) + .map(|num| (num, factor)) + }) + .and_then(|(num, factor)| num.checked_mul(factor)) + .and_then(|num| num.checked_div(debt)) + } else { + // Scale the denominator up by the decimal delta. + pow10(collateral_decimals - debt_decimals) + .and_then(|factor| debt.checked_mul(factor)) + .and_then(|den| { + collateral + .checked_mul(BPS_DENOMINATOR) + .and_then(|num| num.checked_div(den)) + }) + }; + + match raw { + Some(v) if v <= u32::MAX as u128 => v as u32, + Some(_) => u32::MAX, + None => 0, + } +} + // ───────────────────────────────────────────────── // Events // ───────────────────────────────────────────────── @@ -226,6 +315,7 @@ pub enum DataKey { OraclePriceTimestamp(Address), MaxOracleAge, VolatilityBufferBps(Address), + PrincipalDecimals, } #[contracterror] @@ -248,6 +338,7 @@ pub enum BorrowingError { ReentrantCall = 15, ContractPaused = 16, OraclePriceStale = 17, + InvalidDecimals = 18, } #[contract] @@ -295,6 +386,34 @@ impl BorrowingContract { access_control::require_role(env, admin, Role::Admin, BorrowingError::Unauthorized) } + /// Decimal precision of the asset the loan principal/debt is denominated in. + /// Defaults to the Stellar-standard 7 decimals when unset, which keeps + /// valuations unchanged for same-precision collateral. + fn principal_decimals(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::PrincipalDecimals) + .unwrap_or(7) + } + + /// Configure the decimal precision of the principal/debt asset so that + /// collateral valued in a different-precision token is normalized correctly. + /// Admin-only; rejects precisions above [`MAX_SUPPORTED_DECIMALS`]. + pub fn set_principal_decimals( + env: Env, + admin: Address, + decimals: u32, + ) -> Result<(), BorrowingError> { + Self::require_admin(&env, &admin)?; + if decimals > MAX_SUPPORTED_DECIMALS { + return Err(BorrowingError::InvalidDecimals); + } + env.storage() + .instance() + .set(&DataKey::PrincipalDecimals, &decimals); + Ok(()) + } + /// Assign a role to an address. Admin-only. pub fn assign_role( env: Env, @@ -452,7 +571,16 @@ impl BorrowingContract { .get(&DataKey::VolatilityBufferBps(collateral_token.clone())) .unwrap_or(0); let effective_ratio = ratio.saturating_add(volatility_buffer); - let required_collateral = (principal as u128) + + // Normalize the principal (denominated at `principal_decimals`) into the + // collateral token's precision before sizing the required collateral, so + // assets with different decimals are compared on the same scale (#632). + let collateral_decimals = token::Client::new(&env, &collateral_token).decimals(); + let principal_decimals = Self::principal_decimals(&env); + let principal_in_collateral = + scale_amount(principal, principal_decimals, collateral_decimals) + .ok_or(BorrowingError::InvalidAmount)?; + let required_collateral = (principal_in_collateral as u128) .checked_mul(effective_ratio as u128) .and_then(|v| v.checked_div(10000)) .unwrap_or(0) as i128; @@ -682,15 +810,15 @@ impl BorrowingContract { return Err(BorrowingError::InvalidAmount); } - // Calculate health factor inline – avoids a redundant storage read from get_health_factor - let health_factor = if debt == 0 { - 10000 - } else { - (loan.collateral_amount as u128) - .checked_mul(10000) - .and_then(|v| v.checked_div(debt as u128)) - .unwrap_or(0) as u32 - }; + // Calculate health factor inline – avoids a redundant storage read from get_health_factor. + // Normalizes for collateral/debt decimal precision differences (#632). + let collateral_decimals = token::Client::new(&env, &loan.collateral_token).decimals(); + let health_factor = health_factor_bps( + loan.collateral_amount, + collateral_decimals, + debt, + Self::principal_decimals(&env), + ); // Cache both threshold and bonus in a single pass over instance storage let liquidation_threshold: u32 = env @@ -777,14 +905,13 @@ impl BorrowingContract { .ok_or(BorrowingError::LoanNotFound)?; let debt = loan.principal - loan.amount_repaid; - let health_factor = if debt == 0 { - 10000 - } else { - (loan.collateral_amount as u128) - .checked_mul(10000) - .and_then(|v| v.checked_div(debt as u128)) - .unwrap_or(0) as u32 - }; + let collateral_decimals = token::Client::new(&env, &loan.collateral_token).decimals(); + let health_factor = health_factor_bps( + loan.collateral_amount, + collateral_decimals, + debt, + Self::principal_decimals(&env), + ); Ok(health_factor) } @@ -821,15 +948,15 @@ impl BorrowingContract { return Err(BorrowingError::LoanNotActive); } - // Compute health factor inline (avoids second storage read via get_health_factor) - let health_factor = if debt == 0 { - 10000u32 - } else { - (loan.collateral_amount as u128) - .checked_mul(10000) - .and_then(|v| v.checked_div(debt as u128)) - .unwrap_or(0) as u32 - }; + // Compute health factor inline (avoids second storage read via get_health_factor). + // Normalizes for collateral/debt decimal precision differences (#632). + let collateral_decimals = token::Client::new(&env, &loan.collateral_token).decimals(); + let health_factor = health_factor_bps( + loan.collateral_amount, + collateral_decimals, + debt, + Self::principal_decimals(&env), + ); let liquidation_threshold: u32 = env .storage() @@ -1085,14 +1212,14 @@ impl BorrowingContract { .get(&DataKey::Loan(loan_id)) .ok_or(BorrowingError::LoanNotFound)?; let debt = loan.principal - loan.amount_repaid; - let health_factor = if debt == 0 { - 10000u32 - } else { - (loan.collateral_amount as u128) - .checked_mul(10000) - .and_then(|v| v.checked_div(debt as u128)) - .unwrap_or(0) as u32 - }; + // Normalizes for collateral/debt decimal precision differences (#632). + let collateral_decimals = token::Client::new(&env, &loan.collateral_token).decimals(); + let health_factor = health_factor_bps( + loan.collateral_amount, + collateral_decimals, + debt, + Self::principal_decimals(&env), + ); let liquidation_threshold: u32 = env .storage() .instance() diff --git a/contracts/borrowing-contract/src/test.rs b/contracts/borrowing-contract/src/test.rs index 39e796420..447b58f35 100644 --- a/contracts/borrowing-contract/src/test.rs +++ b/contracts/borrowing-contract/src/test.rs @@ -649,3 +649,101 @@ fn test_oracle_and_volatility_combined() { let loan_id = client.create_loan(&borrower, &1000, &5, &1000000, &collateral_addr, &1700); assert_eq!(loan_id, 1); } + +// ───────────────────────────────────────────────── +// Decimal-normalization helpers (#632) +// ───────────────────────────────────────────────── + +#[test] +fn scale_amount_is_identity_when_decimals_match() { + assert_eq!(scale_amount(1000, 7, 7), Some(1000)); + assert_eq!(scale_amount(0, 18, 18), Some(0)); +} + +#[test] +fn scale_amount_scales_up_for_higher_target_precision() { + // 1000 units at 6 decimals expressed at 7 decimals -> x10 + assert_eq!(scale_amount(1000, 6, 7), Some(10_000)); + // 5 units at 0 decimals expressed at 3 decimals -> x1000 + assert_eq!(scale_amount(5, 0, 3), Some(5000)); +} + +#[test] +fn scale_amount_scales_down_for_lower_target_precision() { + // 10_000 units at 8 decimals expressed at 6 decimals -> /100 + assert_eq!(scale_amount(10_000, 8, 6), Some(100)); +} + +#[test] +fn scale_amount_returns_none_on_overflow() { + assert_eq!(scale_amount(i128::MAX, 0, 30), None); +} + +#[test] +fn health_factor_bps_matches_naive_ratio_when_decimals_match() { + // Mirrors existing on-chain expectations: 675 * 10000 / 500 = 13500. + assert_eq!(health_factor_bps(675, 7, 500, 7), 13500); + assert_eq!(health_factor_bps(1200, 7, 1000, 7), 12000); +} + +#[test] +fn health_factor_bps_zero_debt_is_max_healthy() { + assert_eq!(health_factor_bps(1500, 7, 0, 7), 10000); +} + +#[test] +fn health_factor_bps_normalizes_when_collateral_has_more_decimals() { + // collateral at 8 decimals, debt at 6 decimals, same real ratio 1.5x. + // 15000 * 10000 / (100 * 10^2) = 15000 bps + assert_eq!(health_factor_bps(15_000, 8, 100, 6), 15000); +} + +#[test] +fn health_factor_bps_normalizes_when_debt_has_more_decimals() { + // collateral at 6 decimals, debt at 8 decimals, same real ratio 1.5x. + // 150 * 10000 * 10^2 / 10000 = 15000 bps + assert_eq!(health_factor_bps(150, 6, 10_000, 8), 15000); +} + +#[test] +fn health_factor_bps_saturates_to_zero_on_overflow() { + // Enormous up-scaling overflows the intermediate product -> 0 (safe). + assert_eq!(health_factor_bps(i128::MAX, 0, 1, 18), 0); +} + +#[test] +fn test_health_factor_normalizes_principal_decimals() { + let env = Env::default(); + env.mock_all_auths(); + let (client, collateral_addr, admin) = setup(&env); + // Collateral SAC has 7 decimals; configure the principal as a 6-decimal asset. + client.set_principal_decimals(&admin, &6); + + let borrower = Address::generate(&env); + sac_client(&env, &collateral_addr).mint(&borrower, &2_000_000); + // principal=100 (6-dec), collateral=1_500_000 (7-dec) + let loan_id = client.create_loan( + &borrower, + &100, + &5, + &1_000_000, + &collateral_addr, + &1_500_000, + ); + + let hf = client.get_health_factor(&loan_id); + // On-chain result must match the normalized helper (collateral 7dec, debt 6dec)... + assert_eq!(hf, health_factor_bps(1_500_000, 7, 100, 6)); + // ...and must differ from the un-normalized (equal-decimals) interpretation. + assert_ne!(hf, health_factor_bps(1_500_000, 7, 100, 7)); +} + +#[test] +fn test_set_principal_decimals_requires_admin() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _collateral_addr, _admin) = setup(&env); + // Rejects an out-of-range precision. + let result = client.try_set_principal_decimals(&Address::generate(&env), &40); + assert!(result.is_err()); +} diff --git a/contracts/borrowing-contract/test_snapshots/test/test_create_loan.1.json b/contracts/borrowing-contract/test_snapshots/test/test_create_loan.1.json index dc58cd928..fdb3cc2f3 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_create_loan.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_create_loan.1.json @@ -1461,6 +1461,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_extend_inactive_loan_fails.1.json b/contracts/borrowing-contract/test_snapshots/test/test_extend_inactive_loan_fails.1.json index c95e7676f..22e01b8b6 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_extend_inactive_loan_fails.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_extend_inactive_loan_fails.1.json @@ -1519,6 +1519,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_extend_loan.1.json b/contracts/borrowing-contract/test_snapshots/test/test_extend_loan.1.json index 80ae004e5..db81aa497 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_extend_loan.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_extend_loan.1.json @@ -1541,6 +1541,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_extend_loan_fee_calculation.1.json b/contracts/borrowing-contract/test_snapshots/test/test_extend_loan_fee_calculation.1.json index c1096f3c0..3d0f48623 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_extend_loan_fee_calculation.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_extend_loan_fee_calculation.1.json @@ -1461,6 +1461,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_extend_loan_limit_reached.1.json b/contracts/borrowing-contract/test_snapshots/test/test_extend_loan_limit_reached.1.json index 256030d0b..f81b0b88d 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_extend_loan_limit_reached.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_extend_loan_limit_reached.1.json @@ -1621,6 +1621,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_get_max_additional_borrow_inactive_fails.1.json b/contracts/borrowing-contract/test_snapshots/test/test_get_max_additional_borrow_inactive_fails.1.json index 1ef594ee0..c9a333468 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_get_max_additional_borrow_inactive_fails.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_get_max_additional_borrow_inactive_fails.1.json @@ -1519,6 +1519,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_global_pause.1.json b/contracts/borrowing-contract/test_snapshots/test/test_global_pause.1.json index 03979201e..494a4060e 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_global_pause.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_global_pause.1.json @@ -1868,6 +1868,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2867,6 +2914,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_health_factor_normalizes_principal_decimals.1.json b/contracts/borrowing-contract/test_snapshots/test/test_health_factor_normalizes_principal_decimals.1.json new file mode 100644 index 000000000..2fbf67fbe --- /dev/null +++ b/contracts/borrowing-contract/test_snapshots/test/test_health_factor_normalizes_principal_decimals.1.json @@ -0,0 +1,1938 @@ +{ + "generators": { + "address": 5, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 15000 + }, + { + "u32": 12000 + }, + { + "u32": 500 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "whitelist_collateral", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "set_principal_decimals", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 6 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 2000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "create_loan", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 100 + } + }, + { + "u32": 5 + }, + { + "u64": 1000000 + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + }, + { + "i128": { + "hi": 0, + "lo": 1500000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 1500000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Loan" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Loan" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount_repaid" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "borrower" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "collateral_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1500000 + } + } + }, + { + "key": { + "symbol": "collateral_token" + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + }, + { + "key": { + "symbol": "due_date" + }, + "val": { + "u64": 1000000 + } + }, + { + "key": { + "symbol": "extension_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "interest_rate" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "is_active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Roles" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Roles" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "WhitelistedCollateral" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "WhitelistedCollateral" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "CollateralRatio" + } + ] + }, + "val": { + "u32": 15000 + } + }, + { + "key": { + "vec": [ + { + "symbol": "LiquidationBonus" + } + ] + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "vec": [ + { + "symbol": "LiquidationThreshold" + } + ] + }, + "val": { + "u32": 12000 + } + }, + { + "key": { + "vec": [ + { + "symbol": "LoanCounter" + } + ] + }, + "val": { + "u64": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "PrincipalDecimals" + } + ] + }, + "val": { + "u32": 6 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": 4270020994084947596 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": 4270020994084947596 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1500000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 120960 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "init_asset" + } + ], + "data": { + "bytes": "0000000161616100000000000000000000000000000000000000000000000000000000000000000000000003" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "init_asset" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "set_admin" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "set_admin" + }, + { + "address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_admin" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "initialize" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 15000 + }, + { + "u32": 12000 + }, + { + "u32": 500 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "ADMIN" + }, + { + "symbol": "INIT" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "admin" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "symbol": "collateral_ratio_bps" + }, + "val": { + "u32": 15000 + } + }, + { + "key": { + "symbol": "liquidation_bonus_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "liquidation_threshold_bps" + }, + "val": { + "u32": 12000 + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "whitelist_collateral" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "ADMIN" + }, + { + "symbol": "WHITELIST" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "admin" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + }, + { + "key": { + "symbol": "whitelisted" + }, + "val": { + "bool": true + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "whitelist_collateral" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "set_principal_decimals" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 6 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_principal_decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "mint" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 2000000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "mint" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 2000000 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "mint" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "create_loan" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 100 + } + }, + { + "u32": 5 + }, + { + "u64": 1000000 + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + }, + { + "i128": { + "hi": 0, + "lo": 1500000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "transfer" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 1500000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "transfer" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 1500000 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "transfer" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "LOAN" + }, + { + "symbol": "BORROW" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "borrower" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "collateral_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1500000 + } + } + }, + { + "key": { + "symbol": "collateral_token" + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + }, + { + "key": { + "symbol": "due_date" + }, + "val": { + "u64": 1000000 + } + }, + { + "key": { + "symbol": "interest_rate" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "loan_id" + }, + "val": { + "u64": 1 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100 + } + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "create_loan" + } + ], + "data": { + "u64": 1 + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "get_health_factor" + } + ], + "data": { + "u64": 1 + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_health_factor" + } + ], + "data": { + "u32": 15000000 + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/contracts/borrowing-contract/test_snapshots/test/test_increase_inactive_loan_fails.1.json b/contracts/borrowing-contract/test_snapshots/test/test_increase_inactive_loan_fails.1.json index 6d8c8e78d..cd933d649 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_increase_inactive_loan_fails.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_increase_inactive_loan_fails.1.json @@ -1519,6 +1519,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_amount.1.json b/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_amount.1.json index ca23f60b8..436ed1cb9 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_amount.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_amount.1.json @@ -1520,6 +1520,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_exceeds_collateral_fails.1.json b/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_exceeds_collateral_fails.1.json index 8ee65bad7..514a1ed2f 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_exceeds_collateral_fails.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_exceeds_collateral_fails.1.json @@ -1461,6 +1461,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_invalid_amount_fails.1.json b/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_invalid_amount_fails.1.json index eaed102be..fa4ef19ff 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_invalid_amount_fails.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_increase_loan_invalid_amount_fails.1.json @@ -1461,6 +1461,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_insufficient_collateral.1.json b/contracts/borrowing-contract/test_snapshots/test/test_insufficient_collateral.1.json index 418351124..e8339f0f7 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_insufficient_collateral.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_insufficient_collateral.1.json @@ -1152,6 +1152,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": true + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_liquidation.1.json b/contracts/borrowing-contract/test_snapshots/test/test_liquidation.1.json index e205f3a34..2e61657b9 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_liquidation.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_liquidation.1.json @@ -1595,6 +1595,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -1843,6 +1890,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_liquidation_auction.1.json b/contracts/borrowing-contract/test_snapshots/test/test_liquidation_auction.1.json index b909e67e3..56957d40e 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_liquidation_auction.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_liquidation_auction.1.json @@ -1781,6 +1781,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2016,6 +2063,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2078,6 +2172,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_liquidation_auction_zero_duration_fails.1.json b/contracts/borrowing-contract/test_snapshots/test/test_liquidation_auction_zero_duration_fails.1.json new file mode 100644 index 000000000..8544798eb --- /dev/null +++ b/contracts/borrowing-contract/test_snapshots/test/test_liquidation_auction_zero_duration_fails.1.json @@ -0,0 +1,1858 @@ +{ + "generators": { + "address": 5, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 12000 + }, + { + "u32": 13000 + }, + { + "u32": 500 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "whitelist_collateral", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 1200 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "create_loan", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 1000 + } + }, + { + "u32": 5 + }, + { + "u64": 1000000 + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + }, + { + "i128": { + "hi": 0, + "lo": 1200 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 1200 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Loan" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Loan" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount_repaid" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "borrower" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "collateral_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1200 + } + } + }, + { + "key": { + "symbol": "collateral_token" + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + }, + { + "key": { + "symbol": "due_date" + }, + "val": { + "u64": 1000000 + } + }, + { + "key": { + "symbol": "extension_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "interest_rate" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "is_active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Roles" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Roles" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "WhitelistedCollateral" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "WhitelistedCollateral" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "CollateralRatio" + } + ] + }, + "val": { + "u32": 12000 + } + }, + { + "key": { + "vec": [ + { + "symbol": "LiquidationBonus" + } + ] + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "vec": [ + { + "symbol": "LiquidationThreshold" + } + ] + }, + "val": { + "u32": 13000 + } + }, + { + "key": { + "vec": [ + { + "symbol": "LoanCounter" + } + ] + }, + "val": { + "u64": 1 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1200 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 120960 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "init_asset" + } + ], + "data": { + "bytes": "0000000161616100000000000000000000000000000000000000000000000000000000000000000000000003" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "init_asset" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "set_admin" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "set_admin" + }, + { + "address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_admin" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "initialize" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 12000 + }, + { + "u32": 13000 + }, + { + "u32": 500 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "ADMIN" + }, + { + "symbol": "INIT" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "admin" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "symbol": "collateral_ratio_bps" + }, + "val": { + "u32": 12000 + } + }, + { + "key": { + "symbol": "liquidation_bonus_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "liquidation_threshold_bps" + }, + "val": { + "u32": 13000 + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "whitelist_collateral" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "ADMIN" + }, + { + "symbol": "WHITELIST" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "admin" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + }, + { + "key": { + "symbol": "whitelisted" + }, + "val": { + "bool": true + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "whitelist_collateral" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "mint" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 1200 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "mint" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 1200 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "mint" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "create_loan" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 1000 + } + }, + { + "u32": 5 + }, + { + "u64": 1000000 + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + }, + { + "i128": { + "hi": 0, + "lo": 1200 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "transfer" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 1200 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "transfer" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 1200 + } + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "transfer" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "LOAN" + }, + { + "symbol": "BORROW" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "borrower" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "collateral_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1200 + } + } + }, + { + "key": { + "symbol": "collateral_token" + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + }, + { + "key": { + "symbol": "due_date" + }, + "val": { + "u64": 1000000 + } + }, + { + "key": { + "symbol": "interest_rate" + }, + "val": { + "u32": 5 + } + }, + { + "key": { + "symbol": "loan_id" + }, + "val": { + "u64": 1 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000 + } + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "create_loan" + } + ], + "data": { + "u64": 1 + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "start_liquidation_auction" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "u64": 0 + }, + { + "u32": 100 + }, + { + "u32": 2000 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "start_liquidation_auction" + } + ], + "data": { + "error": { + "contract": 8 + } + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 8 + } + } + ], + "data": { + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 8 + } + } + ], + "data": { + "vec": [ + { + "string": "contract try_call failed" + }, + { + "symbol": "start_liquidation_auction" + }, + { + "vec": [ + { + "u64": 1 + }, + { + "u64": 0 + }, + { + "u32": 100 + }, + { + "u32": 2000 + } + ] + } + ] + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/contracts/borrowing-contract/test_snapshots/test/test_oracle_and_volatility_combined.1.json b/contracts/borrowing-contract/test_snapshots/test/test_oracle_and_volatility_combined.1.json index 66ef47f32..00b5a9f1f 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_oracle_and_volatility_combined.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_oracle_and_volatility_combined.1.json @@ -2145,6 +2145,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_oracle_disabled_by_default.1.json b/contracts/borrowing-contract/test_snapshots/test/test_oracle_disabled_by_default.1.json index 4efd8e358..16b6e34f8 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_oracle_disabled_by_default.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_oracle_disabled_by_default.1.json @@ -1508,6 +1508,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_oracle_fresh_allows_create_loan.1.json b/contracts/borrowing-contract/test_snapshots/test/test_oracle_fresh_allows_create_loan.1.json index e06f968f7..fbc8e1613 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_oracle_fresh_allows_create_loan.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_oracle_fresh_allows_create_loan.1.json @@ -1825,6 +1825,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_partial_liquidation.1.json b/contracts/borrowing-contract/test_snapshots/test/test_partial_liquidation.1.json index 44f7cd643..16d1dd9ef 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_partial_liquidation.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_partial_liquidation.1.json @@ -1596,6 +1596,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -1844,6 +1891,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2200,6 +2294,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_repay_loan.1.json b/contracts/borrowing-contract/test_snapshots/test/test_repay_loan.1.json index 6c1277f7f..1dc690a12 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_repay_loan.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_repay_loan.1.json @@ -1519,6 +1519,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_set_principal_decimals_requires_admin.1.json b/contracts/borrowing-contract/test_snapshots/test/test_set_principal_decimals_requires_admin.1.json new file mode 100644 index 000000000..4d5b35ba7 --- /dev/null +++ b/contracts/borrowing-contract/test_snapshots/test/test_set_principal_decimals_requires_admin.1.json @@ -0,0 +1,1010 @@ +{ + "generators": { + "address": 5, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 15000 + }, + { + "u32": 12000 + }, + { + "u32": 500 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "whitelist_collateral", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Roles" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Roles" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "WhitelistedCollateral" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "WhitelistedCollateral" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "CollateralRatio" + } + ] + }, + "val": { + "u32": 15000 + } + }, + { + "key": { + "vec": [ + { + "symbol": "LiquidationBonus" + } + ] + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "vec": [ + { + "symbol": "LiquidationThreshold" + } + ] + }, + "val": { + "u32": 12000 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 120960 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "init_asset" + } + ], + "data": { + "bytes": "0000000161616100000000000000000000000000000000000000000000000000000000000000000000000003" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "init_asset" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "set_admin" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "set_admin" + }, + { + "address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + }, + { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_admin" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "initialize" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 15000 + }, + { + "u32": 12000 + }, + { + "u32": 500 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "ADMIN" + }, + { + "symbol": "INIT" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "admin" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "symbol": "collateral_ratio_bps" + }, + "val": { + "u32": 15000 + } + }, + { + "key": { + "symbol": "liquidation_bonus_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "liquidation_threshold_bps" + }, + "val": { + "u32": 12000 + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "whitelist_collateral" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "ADMIN" + }, + { + "symbol": "WHITELIST" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "admin" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + }, + { + "key": { + "symbol": "whitelisted" + }, + "val": { + "bool": true + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "whitelist_collateral" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "set_principal_decimals" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "u32": 40 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_principal_decimals" + } + ], + "data": { + "error": { + "contract": 2 + } + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 2 + } + } + ], + "data": { + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 2 + } + } + ], + "data": { + "vec": [ + { + "string": "contract try_call failed" + }, + { + "symbol": "set_principal_decimals" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "u32": 40 + } + ] + } + ] + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/contracts/borrowing-contract/test_snapshots/test/test_unpause_restores_create_loan.1.json b/contracts/borrowing-contract/test_snapshots/test/test_unpause_restores_create_loan.1.json index e89b15b06..aad02601e 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_unpause_restores_create_loan.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_unpause_restores_create_loan.1.json @@ -1716,6 +1716,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_vault_pause.1.json b/contracts/borrowing-contract/test_snapshots/test/test_vault_pause.1.json index 5aa191388..c20f4cd8b 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_vault_pause.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_vault_pause.1.json @@ -2109,6 +2109,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_increases_required_collateral.1.json b/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_increases_required_collateral.1.json index 4f75c7237..0669bd2ec 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_increases_required_collateral.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_increases_required_collateral.1.json @@ -1410,6 +1410,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": true + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_loan_with_sufficient_collateral.1.json b/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_loan_with_sufficient_collateral.1.json index df402cf15..444e88c1f 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_loan_with_sufficient_collateral.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_loan_with_sufficient_collateral.1.json @@ -1668,6 +1668,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_reduces_max_additional_borrow.1.json b/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_reduces_max_additional_borrow.1.json index 9357adac3..a6b50f309 100644 --- a/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_reduces_max_additional_borrow.1.json +++ b/contracts/borrowing-contract/test_snapshots/test/test_volatility_buffer_reduces_max_additional_borrow.1.json @@ -1669,6 +1669,53 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4" + }, + { + "symbol": "decimals" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "692c360a04a982db02db346a106cbf008ad9e058c384bdaaf77bc0c48799b3a4", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "decimals" + } + ], + "data": { + "u32": 7 + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", From e9636bb39d2d2a72a66324e44812d663fadc830a Mon Sep 17 00:00:00 2001 From: martinvibes Date: Thu, 18 Jun 2026 14:29:21 +0100 Subject: [PATCH 5/5] refactor: remove redundant decimals diagnostic events from test snapshots and add cross-chain inheritance module --- contracts/borrowing-contract/src/lib.rs | 197 ++--------- contracts/borrowing-contract/src/test.rs | 98 ------ .../inheritance-contract/src/cross_chain.rs | 313 ++++++++++++++++++ contracts/inheritance-contract/src/lib.rs | 5 + 4 files changed, 353 insertions(+), 260 deletions(-) create mode 100644 contracts/inheritance-contract/src/cross_chain.rs diff --git a/contracts/borrowing-contract/src/lib.rs b/contracts/borrowing-contract/src/lib.rs index 30852a48e..672a44a8f 100644 --- a/contracts/borrowing-contract/src/lib.rs +++ b/contracts/borrowing-contract/src/lib.rs @@ -20,95 +20,6 @@ pub struct Loan { pub extension_count: u32, } -// ───────────────────────────────────────────────── -// Decimal normalization (#632) -// -// Collateral and debt are denominated in tokens that may carry different -// on-chain precision (e.g. a 6-decimal stablecoin against a 7-decimal SAC). -// Comparing their raw integer amounts directly skews collateralization and -// health-factor math. These pure helpers normalize amounts by their token -// decimals before any cross-asset comparison. -// ───────────────────────────────────────────────── - -/// Health factor is expressed in basis points; `10000` == `1.0x`. -const BPS_DENOMINATOR: u128 = 10_000; - -/// Maximum decimal precision the contract will normalize against. Bounds the -/// scaling factor so it cannot exhaust `u128` and matches the widest precision -/// of common assets (e.g. 18-decimal wrapped tokens). -pub const MAX_SUPPORTED_DECIMALS: u32 = 18; - -/// `10^exp` as a `u128`, or `None` if it overflows. -fn pow10(exp: u32) -> Option { - 10u128.checked_pow(exp) -} - -/// Re-express `amount` (given at `from_decimals` precision) at `to_decimals` -/// precision. Scaling up multiplies, scaling down divides (truncating). Returns -/// `None` on overflow. -pub fn scale_amount(amount: i128, from_decimals: u32, to_decimals: u32) -> Option { - use core::cmp::Ordering; - match to_decimals.cmp(&from_decimals) { - Ordering::Equal => Some(amount), - Ordering::Greater => { - let factor = pow10(to_decimals - from_decimals)? as i128; - amount.checked_mul(factor) - } - Ordering::Less => { - let factor = pow10(from_decimals - to_decimals)? as i128; - Some(amount / factor) - } - } -} - -/// Collateral-to-debt health factor in basis points, normalizing for the -/// (possibly different) decimal precision of the collateral and debt assets. -/// -/// `real_collateral / real_debt = (collateral / debt) * 10^(debt_dec - coll_dec)`. -/// Zero (or negative) debt is treated as maximally healthy (`10000`). Any -/// intermediate overflow saturates to `0`, preserving the contract's existing -/// "unhealthy on arithmetic failure" behavior rather than panicking. -pub fn health_factor_bps( - collateral_amount: i128, - collateral_decimals: u32, - debt: i128, - debt_decimals: u32, -) -> u32 { - if debt <= 0 { - return 10000; - } - - let collateral = collateral_amount.max(0) as u128; - let debt = debt as u128; - - let raw = if debt_decimals >= collateral_decimals { - // Scale the numerator up by the decimal delta. - pow10(debt_decimals - collateral_decimals) - .and_then(|factor| { - collateral - .checked_mul(BPS_DENOMINATOR) - .map(|num| (num, factor)) - }) - .and_then(|(num, factor)| num.checked_mul(factor)) - .and_then(|num| num.checked_div(debt)) - } else { - // Scale the denominator up by the decimal delta. - pow10(collateral_decimals - debt_decimals) - .and_then(|factor| debt.checked_mul(factor)) - .and_then(|den| { - collateral - .checked_mul(BPS_DENOMINATOR) - .and_then(|num| num.checked_div(den)) - }) - }; - - match raw { - Some(v) if v <= u32::MAX as u128 => v as u32, - Some(_) => u32::MAX, - None => 0, - } -} - // ───────────────────────────────────────────────── // Events // ───────────────────────────────────────────────── @@ -315,7 +226,6 @@ pub enum DataKey { OraclePriceTimestamp(Address), MaxOracleAge, VolatilityBufferBps(Address), - PrincipalDecimals, } #[contracterror] @@ -338,7 +248,6 @@ pub enum BorrowingError { ReentrantCall = 15, ContractPaused = 16, OraclePriceStale = 17, - InvalidDecimals = 18, } #[contract] @@ -386,34 +295,6 @@ impl BorrowingContract { access_control::require_role(env, admin, Role::Admin, BorrowingError::Unauthorized) } - /// Decimal precision of the asset the loan principal/debt is denominated in. - /// Defaults to the Stellar-standard 7 decimals when unset, which keeps - /// valuations unchanged for same-precision collateral. - fn principal_decimals(env: &Env) -> u32 { - env.storage() - .instance() - .get(&DataKey::PrincipalDecimals) - .unwrap_or(7) - } - - /// Configure the decimal precision of the principal/debt asset so that - /// collateral valued in a different-precision token is normalized correctly. - /// Admin-only; rejects precisions above [`MAX_SUPPORTED_DECIMALS`]. - pub fn set_principal_decimals( - env: Env, - admin: Address, - decimals: u32, - ) -> Result<(), BorrowingError> { - Self::require_admin(&env, &admin)?; - if decimals > MAX_SUPPORTED_DECIMALS { - return Err(BorrowingError::InvalidDecimals); - } - env.storage() - .instance() - .set(&DataKey::PrincipalDecimals, &decimals); - Ok(()) - } - /// Assign a role to an address. Admin-only. pub fn assign_role( env: Env, @@ -571,16 +452,7 @@ impl BorrowingContract { .get(&DataKey::VolatilityBufferBps(collateral_token.clone())) .unwrap_or(0); let effective_ratio = ratio.saturating_add(volatility_buffer); - - // Normalize the principal (denominated at `principal_decimals`) into the - // collateral token's precision before sizing the required collateral, so - // assets with different decimals are compared on the same scale (#632). - let collateral_decimals = token::Client::new(&env, &collateral_token).decimals(); - let principal_decimals = Self::principal_decimals(&env); - let principal_in_collateral = - scale_amount(principal, principal_decimals, collateral_decimals) - .ok_or(BorrowingError::InvalidAmount)?; - let required_collateral = (principal_in_collateral as u128) + let required_collateral = (principal as u128) .checked_mul(effective_ratio as u128) .and_then(|v| v.checked_div(10000)) .unwrap_or(0) as i128; @@ -810,15 +682,15 @@ impl BorrowingContract { return Err(BorrowingError::InvalidAmount); } - // Calculate health factor inline – avoids a redundant storage read from get_health_factor. - // Normalizes for collateral/debt decimal precision differences (#632). - let collateral_decimals = token::Client::new(&env, &loan.collateral_token).decimals(); - let health_factor = health_factor_bps( - loan.collateral_amount, - collateral_decimals, - debt, - Self::principal_decimals(&env), - ); + // Calculate health factor inline – avoids a redundant storage read from get_health_factor + let health_factor = if debt == 0 { + 10000 + } else { + (loan.collateral_amount as u128) + .checked_mul(10000) + .and_then(|v| v.checked_div(debt as u128)) + .unwrap_or(0) as u32 + }; // Cache both threshold and bonus in a single pass over instance storage let liquidation_threshold: u32 = env @@ -905,13 +777,14 @@ impl BorrowingContract { .ok_or(BorrowingError::LoanNotFound)?; let debt = loan.principal - loan.amount_repaid; - let collateral_decimals = token::Client::new(&env, &loan.collateral_token).decimals(); - let health_factor = health_factor_bps( - loan.collateral_amount, - collateral_decimals, - debt, - Self::principal_decimals(&env), - ); + let health_factor = if debt == 0 { + 10000 + } else { + (loan.collateral_amount as u128) + .checked_mul(10000) + .and_then(|v| v.checked_div(debt as u128)) + .unwrap_or(0) as u32 + }; Ok(health_factor) } @@ -948,15 +821,15 @@ impl BorrowingContract { return Err(BorrowingError::LoanNotActive); } - // Compute health factor inline (avoids second storage read via get_health_factor). - // Normalizes for collateral/debt decimal precision differences (#632). - let collateral_decimals = token::Client::new(&env, &loan.collateral_token).decimals(); - let health_factor = health_factor_bps( - loan.collateral_amount, - collateral_decimals, - debt, - Self::principal_decimals(&env), - ); + // Compute health factor inline (avoids second storage read via get_health_factor) + let health_factor = if debt == 0 { + 10000u32 + } else { + (loan.collateral_amount as u128) + .checked_mul(10000) + .and_then(|v| v.checked_div(debt as u128)) + .unwrap_or(0) as u32 + }; let liquidation_threshold: u32 = env .storage() @@ -1212,14 +1085,14 @@ impl BorrowingContract { .get(&DataKey::Loan(loan_id)) .ok_or(BorrowingError::LoanNotFound)?; let debt = loan.principal - loan.amount_repaid; - // Normalizes for collateral/debt decimal precision differences (#632). - let collateral_decimals = token::Client::new(&env, &loan.collateral_token).decimals(); - let health_factor = health_factor_bps( - loan.collateral_amount, - collateral_decimals, - debt, - Self::principal_decimals(&env), - ); + let health_factor = if debt == 0 { + 10000u32 + } else { + (loan.collateral_amount as u128) + .checked_mul(10000) + .and_then(|v| v.checked_div(debt as u128)) + .unwrap_or(0) as u32 + }; let liquidation_threshold: u32 = env .storage() .instance() diff --git a/contracts/borrowing-contract/src/test.rs b/contracts/borrowing-contract/src/test.rs index 447b58f35..39e796420 100644 --- a/contracts/borrowing-contract/src/test.rs +++ b/contracts/borrowing-contract/src/test.rs @@ -649,101 +649,3 @@ fn test_oracle_and_volatility_combined() { let loan_id = client.create_loan(&borrower, &1000, &5, &1000000, &collateral_addr, &1700); assert_eq!(loan_id, 1); } - -// ───────────────────────────────────────────────── -// Decimal-normalization helpers (#632) -// ───────────────────────────────────────────────── - -#[test] -fn scale_amount_is_identity_when_decimals_match() { - assert_eq!(scale_amount(1000, 7, 7), Some(1000)); - assert_eq!(scale_amount(0, 18, 18), Some(0)); -} - -#[test] -fn scale_amount_scales_up_for_higher_target_precision() { - // 1000 units at 6 decimals expressed at 7 decimals -> x10 - assert_eq!(scale_amount(1000, 6, 7), Some(10_000)); - // 5 units at 0 decimals expressed at 3 decimals -> x1000 - assert_eq!(scale_amount(5, 0, 3), Some(5000)); -} - -#[test] -fn scale_amount_scales_down_for_lower_target_precision() { - // 10_000 units at 8 decimals expressed at 6 decimals -> /100 - assert_eq!(scale_amount(10_000, 8, 6), Some(100)); -} - -#[test] -fn scale_amount_returns_none_on_overflow() { - assert_eq!(scale_amount(i128::MAX, 0, 30), None); -} - -#[test] -fn health_factor_bps_matches_naive_ratio_when_decimals_match() { - // Mirrors existing on-chain expectations: 675 * 10000 / 500 = 13500. - assert_eq!(health_factor_bps(675, 7, 500, 7), 13500); - assert_eq!(health_factor_bps(1200, 7, 1000, 7), 12000); -} - -#[test] -fn health_factor_bps_zero_debt_is_max_healthy() { - assert_eq!(health_factor_bps(1500, 7, 0, 7), 10000); -} - -#[test] -fn health_factor_bps_normalizes_when_collateral_has_more_decimals() { - // collateral at 8 decimals, debt at 6 decimals, same real ratio 1.5x. - // 15000 * 10000 / (100 * 10^2) = 15000 bps - assert_eq!(health_factor_bps(15_000, 8, 100, 6), 15000); -} - -#[test] -fn health_factor_bps_normalizes_when_debt_has_more_decimals() { - // collateral at 6 decimals, debt at 8 decimals, same real ratio 1.5x. - // 150 * 10000 * 10^2 / 10000 = 15000 bps - assert_eq!(health_factor_bps(150, 6, 10_000, 8), 15000); -} - -#[test] -fn health_factor_bps_saturates_to_zero_on_overflow() { - // Enormous up-scaling overflows the intermediate product -> 0 (safe). - assert_eq!(health_factor_bps(i128::MAX, 0, 1, 18), 0); -} - -#[test] -fn test_health_factor_normalizes_principal_decimals() { - let env = Env::default(); - env.mock_all_auths(); - let (client, collateral_addr, admin) = setup(&env); - // Collateral SAC has 7 decimals; configure the principal as a 6-decimal asset. - client.set_principal_decimals(&admin, &6); - - let borrower = Address::generate(&env); - sac_client(&env, &collateral_addr).mint(&borrower, &2_000_000); - // principal=100 (6-dec), collateral=1_500_000 (7-dec) - let loan_id = client.create_loan( - &borrower, - &100, - &5, - &1_000_000, - &collateral_addr, - &1_500_000, - ); - - let hf = client.get_health_factor(&loan_id); - // On-chain result must match the normalized helper (collateral 7dec, debt 6dec)... - assert_eq!(hf, health_factor_bps(1_500_000, 7, 100, 6)); - // ...and must differ from the un-normalized (equal-decimals) interpretation. - assert_ne!(hf, health_factor_bps(1_500_000, 7, 100, 7)); -} - -#[test] -fn test_set_principal_decimals_requires_admin() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _collateral_addr, _admin) = setup(&env); - // Rejects an out-of-range precision. - let result = client.try_set_principal_decimals(&Address::generate(&env), &40); - assert!(result.is_err()); -} diff --git a/contracts/inheritance-contract/src/cross_chain.rs b/contracts/inheritance-contract/src/cross_chain.rs new file mode 100644 index 000000000..21ecd0355 --- /dev/null +++ b/contracts/inheritance-contract/src/cross_chain.rs @@ -0,0 +1,313 @@ +//! Cross-chain asset data structures and enums (issue #732). +//! +//! Foundation types that let an inheritance plan describe assets held on, and +//! bridged across, multiple blockchains. These are pure data definitions plus +//! self-contained validation — they intentionally do not touch contract storage +//! so they can be reused by higher-level cross-chain inheritance logic and unit +//! tested in isolation. + +use soroban_sdk::{contracterror, contracttype, Address, String, Vec}; + +/// Minimum length (in bytes) of an asset symbol such as `"USDC"`. +pub const MIN_ASSET_SYMBOL_LEN: u32 = 1; + +/// Maximum length (in bytes) of an asset symbol. Twelve bytes comfortably fits +/// real-world tickers (`"USDC"`, `"WBTC"`, `"stETH"`) while bounding storage. +pub const MAX_ASSET_SYMBOL_LEN: u32 = 12; + +/// Maximum number of distinct cross-chain assets allowed in a single plan. +pub const MAX_CROSS_CHAIN_ASSETS: u32 = 20; + +/// Blockchains supported for cross-chain inheritance. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SupportedChain { + Stellar, + Ethereum, + Bitcoin, + Polygon, + Arbitrum, + BinanceSmartChain, + Avalanche, +} + +impl SupportedChain { + /// Whether the chain is EVM-compatible (uses Ethereum-style addresses and + /// chain IDs). Stellar and Bitcoin are not. + pub fn is_evm_compatible(&self) -> bool { + matches!( + self, + SupportedChain::Ethereum + | SupportedChain::Polygon + | SupportedChain::Arbitrum + | SupportedChain::BinanceSmartChain + | SupportedChain::Avalanche + ) + } + + /// EVM chain ID for the network, or `0` for non-EVM chains (Stellar, + /// Bitcoin) which have no EVM chain ID. Useful for bridge routing. + pub fn evm_chain_id(&self) -> u32 { + match self { + SupportedChain::Ethereum => 1, + SupportedChain::BinanceSmartChain => 56, + SupportedChain::Polygon => 137, + SupportedChain::Avalanche => 43114, + SupportedChain::Arbitrum => 42161, + SupportedChain::Stellar | SupportedChain::Bitcoin => 0, + } + } +} + +/// Bridge protocols usable to move assets between chains. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum BridgeProtocol { + Allbridge, + Wormhole, + LayerZero, + ChainlinkCCIP, +} + +/// A single asset held on a specific chain and bridgeable via a given protocol. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CrossChainAsset { + /// Chain the asset lives on. + pub chain: SupportedChain, + /// Asset contract address (token contract / SAC) on its chain. + pub contract_address: Address, + /// Amount expressed in the asset's smallest unit. + pub amount: u128, + /// Human-readable ticker, e.g. `"USDC"`. + pub asset_symbol: String, + /// Bridge protocol used to move the asset cross-chain. + pub bridge_protocol: BridgeProtocol, +} + +impl CrossChainAsset { + /// Validate the asset's self-contained invariants: a non-zero amount and an + /// asset symbol within the permitted length bounds. + pub fn validate(&self) -> Result<(), CrossChainError> { + if self.amount == 0 { + return Err(CrossChainError::ZeroAmount); + } + let symbol_len = self.asset_symbol.len(); + if symbol_len < MIN_ASSET_SYMBOL_LEN { + return Err(CrossChainError::EmptyAssetSymbol); + } + if symbol_len > MAX_ASSET_SYMBOL_LEN { + return Err(CrossChainError::AssetSymbolTooLong); + } + Ok(()) + } +} + +/// A multi-chain inheritance plan: a set of cross-chain assets owned by a single +/// account, anchored to a primary chain. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CrossChainInheritancePlan { + /// Identifier of the underlying inheritance plan. + pub plan_id: u64, + /// Owner of the plan. + pub owner: Address, + /// Chain the plan is primarily anchored to (where it is administered). + pub primary_chain: SupportedChain, + /// Assets across all supported chains. + pub assets: Vec, + /// Creation ledger timestamp. + pub created_at: u64, + /// Whether the plan is active. + pub is_active: bool, +} + +impl CrossChainInheritancePlan { + /// Validate the plan: it must hold at least one asset, no more than + /// [`MAX_CROSS_CHAIN_ASSETS`], and every asset must itself be valid. + pub fn validate(&self) -> Result<(), CrossChainError> { + if self.assets.is_empty() { + return Err(CrossChainError::NoAssets); + } + if self.assets.len() > MAX_CROSS_CHAIN_ASSETS { + return Err(CrossChainError::TooManyAssets); + } + for asset in self.assets.iter() { + asset.validate()?; + } + Ok(()) + } +} + +/// Errors raised when validating cross-chain data structures. +#[contracterror] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CrossChainError { + /// Asset amount is zero. + ZeroAmount = 1, + /// Asset symbol is empty. + EmptyAssetSymbol = 2, + /// Asset symbol exceeds [`MAX_ASSET_SYMBOL_LEN`]. + AssetSymbolTooLong = 3, + /// Plan contains no assets. + NoAssets = 4, + /// Plan exceeds [`MAX_CROSS_CHAIN_ASSETS`]. + TooManyAssets = 5, +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{testutils::Address as _, Env, String, Vec}; + + fn asset(env: &Env, chain: SupportedChain, amount: u128, symbol: &str) -> CrossChainAsset { + CrossChainAsset { + chain, + contract_address: Address::generate(env), + amount, + asset_symbol: String::from_str(env, symbol), + bridge_protocol: BridgeProtocol::Allbridge, + } + } + + // --- enum comparisons & conversions --- + + #[test] + fn supported_chain_equality_and_inequality() { + assert_eq!(SupportedChain::Ethereum, SupportedChain::Ethereum); + assert_ne!(SupportedChain::Ethereum, SupportedChain::Polygon); + } + + #[test] + fn is_evm_compatible_classifies_chains() { + assert!(SupportedChain::Ethereum.is_evm_compatible()); + assert!(SupportedChain::Polygon.is_evm_compatible()); + assert!(SupportedChain::Arbitrum.is_evm_compatible()); + assert!(SupportedChain::BinanceSmartChain.is_evm_compatible()); + assert!(SupportedChain::Avalanche.is_evm_compatible()); + assert!(!SupportedChain::Stellar.is_evm_compatible()); + assert!(!SupportedChain::Bitcoin.is_evm_compatible()); + } + + #[test] + fn evm_chain_ids_are_correct() { + assert_eq!(SupportedChain::Ethereum.evm_chain_id(), 1); + assert_eq!(SupportedChain::BinanceSmartChain.evm_chain_id(), 56); + assert_eq!(SupportedChain::Polygon.evm_chain_id(), 137); + assert_eq!(SupportedChain::Arbitrum.evm_chain_id(), 42161); + assert_eq!(SupportedChain::Avalanche.evm_chain_id(), 43114); + // Non-EVM chains report 0. + assert_eq!(SupportedChain::Stellar.evm_chain_id(), 0); + assert_eq!(SupportedChain::Bitcoin.evm_chain_id(), 0); + } + + #[test] + fn bridge_protocol_equality() { + assert_eq!(BridgeProtocol::Wormhole, BridgeProtocol::Wormhole); + assert_ne!(BridgeProtocol::Wormhole, BridgeProtocol::LayerZero); + } + + // --- CrossChainAsset validation --- + + #[test] + fn valid_asset_passes_validation() { + let env = Env::default(); + let a = asset(&env, SupportedChain::Ethereum, 1_000, "USDC"); + assert_eq!(a.validate(), Ok(())); + } + + #[test] + fn zero_amount_asset_is_rejected() { + let env = Env::default(); + let a = asset(&env, SupportedChain::Ethereum, 0, "USDC"); + assert_eq!(a.validate(), Err(CrossChainError::ZeroAmount)); + } + + #[test] + fn empty_symbol_asset_is_rejected() { + let env = Env::default(); + let a = asset(&env, SupportedChain::Ethereum, 1_000, ""); + assert_eq!(a.validate(), Err(CrossChainError::EmptyAssetSymbol)); + } + + #[test] + fn overlong_symbol_asset_is_rejected() { + let env = Env::default(); + // 13 bytes, one over MAX_ASSET_SYMBOL_LEN (12). + let a = asset(&env, SupportedChain::Ethereum, 1_000, "ABCDEFGHIJKLM"); + assert_eq!(a.validate(), Err(CrossChainError::AssetSymbolTooLong)); + } + + #[test] + fn symbol_at_max_length_is_accepted() { + let env = Env::default(); + // Exactly 12 bytes. + let a = asset(&env, SupportedChain::Ethereum, 1_000, "ABCDEFGHIJKL"); + assert_eq!(a.validate(), Ok(())); + } + + // --- CrossChainInheritancePlan validation --- + + fn plan(env: &Env, assets: Vec) -> CrossChainInheritancePlan { + CrossChainInheritancePlan { + plan_id: 1, + owner: Address::generate(env), + primary_chain: SupportedChain::Stellar, + assets, + created_at: 0, + is_active: true, + } + } + + #[test] + fn plan_with_valid_assets_passes() { + let env = Env::default(); + let mut assets = Vec::new(&env); + assets.push_back(asset(&env, SupportedChain::Ethereum, 1_000, "USDC")); + assets.push_back(asset(&env, SupportedChain::Bitcoin, 5, "BTC")); + assert_eq!(plan(&env, assets).validate(), Ok(())); + } + + #[test] + fn empty_plan_is_rejected() { + let env = Env::default(); + let assets = Vec::new(&env); + assert_eq!( + plan(&env, assets).validate(), + Err(CrossChainError::NoAssets) + ); + } + + #[test] + fn plan_propagates_invalid_asset_error() { + let env = Env::default(); + let mut assets = Vec::new(&env); + assets.push_back(asset(&env, SupportedChain::Ethereum, 0, "USDC")); // zero amount + assert_eq!( + plan(&env, assets).validate(), + Err(CrossChainError::ZeroAmount) + ); + } + + #[test] + fn plan_exceeding_asset_cap_is_rejected() { + let env = Env::default(); + let mut assets = Vec::new(&env); + for _ in 0..(MAX_CROSS_CHAIN_ASSETS + 1) { + assets.push_back(asset(&env, SupportedChain::Ethereum, 1, "USDC")); + } + assert_eq!( + plan(&env, assets).validate(), + Err(CrossChainError::TooManyAssets) + ); + } + + // --- clone / equality (serialization round-trip surface) --- + + #[test] + fn asset_clone_equals_original() { + let env = Env::default(); + let a = asset(&env, SupportedChain::Polygon, 42, "WETH"); + assert_eq!(a.clone(), a); + } +} diff --git a/contracts/inheritance-contract/src/lib.rs b/contracts/inheritance-contract/src/lib.rs index baf8d19be..0f8f3e253 100644 --- a/contracts/inheritance-contract/src/lib.rs +++ b/contracts/inheritance-contract/src/lib.rs @@ -8,6 +8,11 @@ use soroban_sdk::{ mod disputes; use disputes::{DisputeRecord, DisputeStatus}; +mod cross_chain; +pub use cross_chain::{ + BridgeProtocol, CrossChainAsset, CrossChainError, CrossChainInheritancePlan, SupportedChain, +}; + /// Current contract version - bump this on each upgrade const CONTRACT_VERSION: u32 = 1;