diff --git a/docs/POOL_STATE_LAZY_LOADING.md b/docs/POOL_STATE_LAZY_LOADING.md new file mode 100644 index 00000000..7c13bd74 --- /dev/null +++ b/docs/POOL_STATE_LAZY_LOADING.md @@ -0,0 +1,141 @@ +# Lazy Pool State: On-Demand Loading, Caching, and Monitoring + +This document describes the lazy pool-state architecture introduced for the +StellarLend protocol on Soroban. It is the reference for how pool snapshots are +built, cached, invalidated, and monitored, and it documents the performance +targets and benchmark story for the feature. + +Related: [storage.md](storage.md), [gas-benchmarks.md](gas-benchmarks.md). + +## Problem + +Pool-level state (aggregate supply, borrow, utilization, health, and +cross-asset metrics) is needed by most views and by the liquidator. Eagerly +recomputing it on every mutation is expensive: every deposit, borrow, repay, or +health check would pay for rebuilding the full aggregate even when nothing else +reads it. The previous design recomputed the snapshot unconditionally and +stored it under a single key, so reads were cheap but writes were asymptotically +costly and the value went stale across unrelated mutations. + +## Design + +Pool state is **lazy**: + +1. **On-demand construction** — the snapshot is only built when it is first + requested (`pool_state::load`), not on every mutation. +2. **Epoch-keyed caching** — snapshots are cached under a *temporary* key that + includes the current epoch: + `PoolStateTempKey::Snapshot(pool, epoch)`. Temporary entries survive only for + the current ledger run, so a snapshot is valid for the epoch in which it was + produced and is never stale across epoch boundaries. +3. **Explicit invalidation** — mutations that change aggregate values call + `pool_state::bump_epoch`, which advances `PoolStateKey::Epoch`. Reads fall + back to rebuilding when the cached entry's epoch does not match. Targeted + invalidation of a single pool is available via `pool_state::invalidate`. +4. **Initialization marker** — `PoolStateKey::Initialized(pool)` records that a + pool has been initialized; `default_pool_state_loader` presets safe defaults + for uninitialized pools so that `get_pool_state` never panics on a fresh or + partially-initialized pool. + +### Contract surface + +All state lives in `contracts/hello-world/src/pool_state.rs` with storage keys +in `contracts/hello-world/src/storage.rs`: + +| Key (Type) | Storage | Value | +|------------|---------|-------| +| `PoolStateKey::Epoch` | persistent | `u64` | +| `PoolStateKey::Initialized(pool)` | persistent | `bool` | +| `PoolStateKey::Metrics` | persistent | `PoolStateMetrics` | +| `PoolStateTempKey::Snapshot(pool, epoch)` | temporary | `PoolStateSnapshot` | + +Entrypoints (`contracts/hello-world/src/lib.rs`): + +- `get_pool_state(env, asset) -> PoolStateSnapshot` — build or return the + cached snapshot. +- `get_multiple_pool_states(env, assets: Vec>) -> Vec`. +- `is_pool_state_initialized(env, asset) -> bool`. +- `get_pool_state_epoch(env) -> u64`. +- `get_pool_state_metrics(env) -> PoolStateMetrics`. +- `invalidate_pool_state(env)` — advance the epoch (admin-gated). + +Epoch bumping is wired into the mutation paths that change aggregates (the +`bump_epoch`/`invalidate` hooks in `lib.rs`, e.g. the borrow/repay/withdrawal +and pool-management flows), keeping cache coherence without a central scheduler. + +## API service + +The API layer mirrors the contract with its own bounded cache in +`api/src/services/stellar.service.ts`, keyed constants `NATIVE_POOL_KEY` and +`POOL_STATE_EPOCH_KEY`: + +- `getPoolState` / `getMultiplePoolStates` — reads with response coalescing. +- `getPoolStateEpoch` — cached epoch used for request fencing. +- `invalidatePoolStateCache` — clears the in-memory and Redis caches. +- `getPoolStateCacheMetrics` — hit rate and cost observability. + +The contract epoch is the source of truth; the service cache is best-effort and +always fallible, so the UI remains correct even when caching is bypassed. + +## Consistency + +- Snapshot identity is `(pool, epoch)`. Any write that should invalidate a + snapshot advances the epoch before subsequent reads see the new value. +- Temporary storage guarantees cross-ledger freshness: a snapshot cached in a + previous run is not used, satisfying the "never stale across ledgers" rule + without extra bookkeeping. +- Deeper consistency and reentrancy guarantees are documented in + [REENTRANCY_GUARANTEES.md](../stellar-lend/docs/REENTRANCY_GUARANTEES.md). + +## Performance target: <50 ms + +The acceptance target for a single `get_pool_state` call is **under 50 +milliseconds** of CPU time, including the cold-miss rebuild path. + +Evidence strategy (benchmarks/): + +- `stellar-lend/benchmarks/src/pool_state_benchmarks.rs` exercises cold + (uncached) load, warm cached load, invalidation-then-reload, and metric reads, + reporting instruction counts via the host cost estimator + (`framework.rs`). +- The elapsed-time runner measures wall-clock latency so the <50 ms constraint + is checked directly, not only via instruction counts. + +### Current status / blocker + +The benchmark runner currently cannot build: hello-world fails to compile when +the workspace enables the soroban-sdk `testutils` feature (which any contract- +driving harness requires). `#[contracttype]` under testutils generates +struct→`ScVal` conversions that need a plain `From for ScVal` for every +field, which SDK 27 does not provide for custom types or raw `Val` fields. The +affected contracttypes are: + +- `contracts/hello-world/src/storage.rs:3` — `SnapshotValue { value: Val, .. }` +- `contracts/hello-world/src/rate_limiter.rs:134` — `CongestionState` with + `Option` / `Option` +- `contracts/hello-world/src/mev_protection.rs:70` — `PendingCommit` with + `Option
` + +This is a pre-existing upstream issue, unrelated to pool-state itself, and is +tracked as a follow-up. Once resolved, running +`cargo run --bin run_benchmarks -- pool_state` produces the load numbers and +the elapsed-time assertion (`<50 ms`) is evaluated by the runner. Until then, +the margin argument rests on the instruction-count scale of the load path: a +single-pool snapshot touches a bounded set of keys and aggregates, orders of +magnitude below Soroban's per-invocation instruction ceiling, and cached reads +are a single temporary-storage lookup. + +## Monitoring + +`PoolStateMetrics` (persisted under `PoolStateKey::Metrics`) aggregates hit/miss +and rebuild counters across calls. `get_pool_state_metrics` exposes them +on-chain, and the API service exposes cache hit rate via +`getPoolStateCacheMetrics`. Existing dashboards may consume either endpoint; see +[RISK_MONITORING_DASHBOARD.md](RISK_MONITORING_DASHBOARD.md). + +## Deployment notes + +The feature ships behind the existing upgrade mechanism (`docs/upgrade- +mechanism.md`): a contract upgrade carries the new `pool_state.rs` module; no +data migration is needed because epoch and initialized markers are new keys and +the old snapshot key(s) are not read after upgrade. \ No newline at end of file diff --git a/stellar-lend/contracts/bridge/src/lending_bridge.rs b/stellar-lend/contracts/bridge/src/lending_bridge.rs index 6de2a38b..4d01f10c 100644 --- a/stellar-lend/contracts/bridge/src/lending_bridge.rs +++ b/stellar-lend/contracts/bridge/src/lending_bridge.rs @@ -217,7 +217,7 @@ pub struct LendingBridgeStats { // ─── Events ─────────────────────────────────────────────────────────────────── -#[contractevent] +#[contractevent(topics = ["cross_chain_position_opened"])] #[derive(Clone, Debug)] pub struct CrossChainPositionOpenedEvent { pub user: Address, @@ -230,7 +230,7 @@ pub struct CrossChainPositionOpenedEvent { pub timestamp: u64, } -#[contractevent] +#[contractevent(topics = ["cross_chain_position_repaid"])] #[derive(Clone, Debug)] pub struct CrossChainPositionRepaidEvent { pub user: Address, @@ -269,7 +269,7 @@ pub struct LiquidityRouteRegisteredEvent { pub timestamp: u64, } -#[contractevent] +#[contractevent(topics = ["remote_health_report_submitted"])] #[derive(Clone, Debug)] pub struct RemoteHealthReportSubmittedEvent { pub user: Address, diff --git a/stellar-lend/contracts/hello-world/src/circuit_breaker.rs b/stellar-lend/contracts/hello-world/src/circuit_breaker.rs index f797e627..d4be9f4f 100644 --- a/stellar-lend/contracts/hello-world/src/circuit_breaker.rs +++ b/stellar-lend/contracts/hello-world/src/circuit_breaker.rs @@ -27,6 +27,8 @@ pub enum CircuitBreakerTier { #[contracttype] pub enum CircuitBreakerStatus { Active, + Paused, + Emergency, Tier1Paused, Tier2Paused, Tier3Halted, @@ -68,7 +70,7 @@ pub struct CircuitBreakerConfig { pub auto_deactivate_enabled: bool, pub whitelist_enabled: bool, pub price_deviation_threshold_bps: u64, - pub abnormal_utilization_threshold_bps: u64, + pub abnormal_utilization_bps: u64, pub guardian_multisig: Option
, pub tier1_auto_trigger_enabled: bool, pub tier2_auto_trigger_enabled: bool, @@ -81,7 +83,7 @@ impl Default for CircuitBreakerConfig { auto_deactivate_enabled: true, whitelist_enabled: true, price_deviation_threshold_bps: PRICE_DEVIATION_THRESHOLD_BPS, - abnormal_utilization_threshold_bps: ABNORMAL_UTILIZATION_THRESHOLD_BPS, + abnormal_utilization_bps: ABNORMAL_UTILIZATION_THRESHOLD_BPS, guardian_multisig: None, tier1_auto_trigger_enabled: true, tier2_auto_trigger_enabled: true, @@ -184,11 +186,15 @@ pub fn activate_circuit_breaker( let state = CircuitBreakerState { status, + tier: CircuitBreakerTier::Tier1, activated_at: now, activated_by: caller.clone(), cooldown_period: config.cooldown_period, auto_deactivate_at, reason: reason.clone(), + affected_pool: None, + guardian_multisig: config.guardian_multisig.clone(), + governance_vote_required: false, }; let state_key = storage::DataKey::CircuitBreakerState; @@ -260,8 +266,10 @@ pub fn is_liquidation_allowed(env: &Env, liquidator: &Address) -> Result Ok(true), - CircuitBreakerStatus::Paused => Ok(false), - CircuitBreakerStatus::Emergency => { + CircuitBreakerStatus::Paused | CircuitBreakerStatus::Tier1Paused | CircuitBreakerStatus::Tier2Paused => { + Ok(false) + } + CircuitBreakerStatus::Tier3Halted | CircuitBreakerStatus::Emergency => { // Check whitelist is_whitelisted(env, liquidator) } @@ -583,7 +591,7 @@ pub fn check_automatic_triggers( return Ok(Some(CircuitBreakerTier::Tier1)); } - if config.tier2_auto_trigger_enabled && utilization_bps >= config.abnormal_utilization_threshold_bps { + if config.tier2_auto_trigger_enabled && utilization_bps >= config.abnormal_utilization_bps { return Ok(Some(CircuitBreakerTier::Tier2)); } @@ -658,6 +666,8 @@ pub fn is_operation_allowed( CircuitBreakerStatus::Tier3Halted => { is_whitelisted(env, caller) } + CircuitBreakerStatus::Paused => Ok(false), + CircuitBreakerStatus::Emergency => is_whitelisted(env, caller), } } diff --git a/stellar-lend/contracts/hello-world/src/debt_token.rs b/stellar-lend/contracts/hello-world/src/debt_token.rs index 802f240a..bd80880e 100644 --- a/stellar-lend/contracts/hello-world/src/debt_token.rs +++ b/stellar-lend/contracts/hello-world/src/debt_token.rs @@ -26,7 +26,7 @@ //! - Audit trail through events #![allow(unused)] -use soroban_sdk::{contracterror, contractevent, contracttype, Address, Env, Map, Symbol, Vec}; +use soroban_sdk::{contracterror, contractevent, contracttype, Address, Env, Map, Symbol, TryFromVal, Vec}; use crate::deposit::DepositDataKey; use crate::errors::LendingError; @@ -327,6 +327,17 @@ pub fn transfer_debt_token( /// require the seller's auth) plus the buyer's auth on the purchase itself, not /// by the seller re-signing at sale time. Callers are responsible for ensuring /// whatever authorization model applies to their call site before invoking this. +fn is_zero_address(env: &Env, address: &Address) -> bool { + *address + == Address::try_from_val( + env, + &soroban_sdk::xdr::ScAddress::Contract(soroban_sdk::xdr::ContractId( + soroban_sdk::xdr::Hash([0u8; 32]), + )), + ) + .unwrap() +} + fn move_debt_token_ownership( env: &Env, from: Address, @@ -334,7 +345,7 @@ fn move_debt_token_ownership( token_id: u64, ) -> Result<(), DebtTokenError> { // Validate inputs - if to == Address::zero() { + if is_zero_address(env, &to) { return Err(DebtTokenError::ZeroAddress); } @@ -365,9 +376,11 @@ fn move_debt_token_ownership( // Remove from current owner let mut from_tokens = owner_tokens; - let index = from_tokens.iter().position(|&id| id == token_id) + let index = from_tokens + .iter() + .position(|id| id == token_id) .ok_or(DebtTokenError::TokenNotFound)?; - from_tokens.remove(index); + from_tokens.remove(index as u32); let from_key = DebtTokenDataKey::OwnerTokens(from.clone()); env.storage().persistent().set(&from_key, &from_tokens); @@ -576,9 +589,11 @@ pub fn burn_debt_token( // Remove from owner's token list let mut user_tokens = owner_tokens; - let index = user_tokens.iter().position(|&id| id == token_id) + let index = user_tokens + .iter() + .position(|id| id == token_id) .ok_or(DebtTokenError::TokenNotFound)?; - user_tokens.remove(index); + user_tokens.remove(index as u32); let owner_key = DebtTokenDataKey::OwnerTokens(user.clone()); env.storage().persistent().set(&owner_key, &user_tokens); diff --git a/stellar-lend/contracts/hello-world/src/emergency_withdrawal.rs b/stellar-lend/contracts/hello-world/src/emergency_withdrawal.rs index f30f27cb..c77cbbf9 100644 --- a/stellar-lend/contracts/hello-world/src/emergency_withdrawal.rs +++ b/stellar-lend/contracts/hello-world/src/emergency_withdrawal.rs @@ -26,7 +26,7 @@ const DEFAULT_WITHDRAWAL_CAP_BPS: i128 = 3000; // 30% pub fn initialize_emergency_withdrawal(env: &Env) { let default_state = EmergencyState { is_active: false, - trigger: EmergencyTrigger::AdminEmergency, + trigger: EmergencyTrigger::Admin, started_at: 0, window_opens_at: 0, window_closes_at: 0, @@ -101,7 +101,7 @@ pub fn get_emergency_state(env: &Env) -> EmergencyState { .get(&DepositDataKey::EmergencyState) .unwrap_or(EmergencyState { is_active: false, - trigger: EmergencyTrigger::AdminEmergency, + trigger: EmergencyTrigger::Admin, started_at: 0, window_opens_at: 0, window_closes_at: 0, diff --git a/stellar-lend/contracts/hello-world/src/errors.rs b/stellar-lend/contracts/hello-world/src/errors.rs index e33d2db7..9b3841f5 100644 --- a/stellar-lend/contracts/hello-world/src/errors.rs +++ b/stellar-lend/contracts/hello-world/src/errors.rs @@ -6,6 +6,7 @@ use crate::borrow::BorrowError; use crate::cross_asset::CrossAssetError; use crate::debt_token::DebtTokenError; use crate::deposit::DepositError; +use crate::emergency_withdrawal::EmergencyWithdrawalError; use crate::flash_loan::FlashLoanError; use crate::interest_rate::InterestRateError; use crate::liquidate::LiquidationError; @@ -70,6 +71,10 @@ pub enum GovernanceError { InvalidTimelockStatus = 144, InvalidTimelockConfig = 145, InvalidTimelockDelay = 146, + RecoveryNotReady = 147, + InvalidActionTypeDelay = 148, + EmergencyOverrideAlreadyApproved = 149, + InsufficientEmergencyApprovals = 150, } /// Unified public contract error type for the lending interface. @@ -380,6 +385,10 @@ impl_from_error!(DebtTokenError, { DebtTokenError::ZeroAddress => LendingError::InvalidParameter, DebtTokenError::AlreadyTokenized => LendingError::AlreadyExists, DebtTokenError::PositionNotFound => LendingError::DataNotFound, + DebtTokenError::NotListed => LendingError::DataNotFound, + DebtTokenError::AlreadyListed => LendingError::AlreadyExists, + DebtTokenError::NotSeller => LendingError::Unauthorized, + DebtTokenError::InvalidPrice => LendingError::InvalidParameter, }); impl From for LendingError { @@ -401,3 +410,18 @@ impl From for LendingError { } } } + +impl From for LendingError { + fn from(error: EmergencyWithdrawalError) -> Self { + match error { + EmergencyWithdrawalError::NotActive => LendingError::InvalidState, + EmergencyWithdrawalError::AlreadyActive => LendingError::AlreadyExists, + EmergencyWithdrawalError::WindowNotOpen => LendingError::InvalidState, + EmergencyWithdrawalError::NotAuthorized => LendingError::Unauthorized, + EmergencyWithdrawalError::InsufficientBalance => LendingError::InsufficientBalance, + EmergencyWithdrawalError::ExceedsWithdrawalCap => LendingError::LimitExceeded, + EmergencyWithdrawalError::InvalidParameter => LendingError::InvalidParameter, + EmergencyWithdrawalError::AlreadyWithdrawn => LendingError::AlreadyExists, + } + } +} diff --git a/stellar-lend/contracts/hello-world/src/events.rs b/stellar-lend/contracts/hello-world/src/events.rs index 3c6a3184..760a7fac 100644 --- a/stellar-lend/contracts/hello-world/src/events.rs +++ b/stellar-lend/contracts/hello-world/src/events.rs @@ -24,7 +24,36 @@ pub use shared_events::*; use soroban_sdk::{contractevent, contracttype, Address, Env, String, Symbol, Vec}; -use crate::types::{AssetStatus, ProposalType, VoteType}; +use crate::types::{AssetStatus, EmergencyTrigger, ProposalType, VoteType}; + +/// Convert a local [`ProposalType`] into its shared-events representation. +pub fn to_shared_proposal_type(proposal_type: &ProposalType) -> shared_events::ProposalType { + match proposal_type { + ProposalType::EmergencyPause(_) => shared_events::ProposalType::Emergency, + ProposalType::GenericAction(_) | ProposalType::PauseSwitch(..) => { + shared_events::ProposalType::Standard + } + _ => shared_events::ProposalType::ParameterChange, + } +} + +/// Convert a local [`VoteType`] into its shared-events representation. +pub fn to_shared_vote_type(vote_type: &VoteType) -> shared_events::VoteType { + match vote_type { + VoteType::For => shared_events::VoteType::For, + VoteType::Against => shared_events::VoteType::Against, + VoteType::Abstain => shared_events::VoteType::Abstain, + } +} + +/// Convert a local [`EmergencyTrigger`] into its shared-events representation. +pub fn to_shared_emergency_trigger(trigger: EmergencyTrigger) -> shared_events::EmergencyTrigger { + match trigger { + EmergencyTrigger::Admin => shared_events::EmergencyTrigger::Admin, + EmergencyTrigger::CircuitBreaker => shared_events::EmergencyTrigger::CircuitBreaker, + EmergencyTrigger::OracleFailure => shared_events::EmergencyTrigger::OracleFailure, + } +} // ============================================================================ // Core Lending Events (Existing) @@ -435,7 +464,7 @@ pub fn emit_flash_loan_liquidation_combo(e: &Env, event: FlashLoanLiquidationCom pub fn emit_emergency_triggered(e: &Env, state: crate::types::EmergencyState) { EmergencyTriggeredEvent { - trigger: state.trigger, + trigger: to_shared_emergency_trigger(state.trigger), started_at: state.started_at, window_opens_at: state.window_opens_at, window_closes_at: state.window_closes_at, diff --git a/stellar-lend/contracts/hello-world/src/flash_loan.rs b/stellar-lend/contracts/hello-world/src/flash_loan.rs index 97f1e131..bc040343 100644 --- a/stellar-lend/contracts/hello-world/src/flash_loan.rs +++ b/stellar-lend/contracts/hello-world/src/flash_loan.rs @@ -354,7 +354,7 @@ fn record_flash_loan( amount, fee, timestamp: env.ledger().timestamp(), - sequence_number: env.ledger().sequence_number(), + sequence_number: env.ledger().sequence(), callback: callback.clone(), }; env.storage().temporary().set(&loan_key, &record); @@ -405,7 +405,7 @@ pub fn execute_flash_loan( // 2. Preparation let fee = calculate_flash_loan_fee(env, amount)?; let total_required = amount.checked_add(fee).ok_or(FlashLoanError::Overflow)?; - let start_sequence = env.ledger().sequence_number(); + let start_sequence = env.ledger().sequence(); let token_client = soroban_sdk::token::Client::new(env, &asset); let initial_balance = token_client.balance(&env.current_contract_address()); @@ -429,8 +429,9 @@ pub fn execute_flash_loan( // MUST be allowed to call back into the protocol (e.g., to repay the loan). let lock_key: soroban_sdk::Val = FlashLoanDataKey::FlashLoanGuard(user.clone(), asset.clone()).into_val(env); - let _granular_guard = crate::reentrancy::ReentrancyGuard::new_with_key(env, lock_key) - .map_err(|_| FlashLoanError::Reentrancy)?; + let _granular_guard = + crate::reentrancy::ReentrancyGuard::new_with_key(env, lock_key, false) + .map_err(|_| FlashLoanError::Reentrancy)?; // Record the loan details for repay_flash_loan helper record_flash_loan(env, &user, &asset, amount, fee, &callback); @@ -454,7 +455,7 @@ pub fn execute_flash_loan( let callback_client = stellarlend_flash_loan::FlashLoanReceiverClient::new(env, &callback); callback_client.on_flash_loan(&user, &asset, &amount, &fee); - if env.ledger().sequence_number() != start_sequence { + if env.ledger().sequence() != start_sequence { return Err(FlashLoanError::Expired); } @@ -527,7 +528,7 @@ pub fn repay_flash_loan( .get::(&loan_key) .ok_or(FlashLoanError::NotRepaid)?; - if env.ledger().sequence_number() != record.sequence_number { + if env.ledger().sequence() != record.sequence_number { return Err(FlashLoanError::Expired); } @@ -765,7 +766,7 @@ pub fn execute_flash_loan_liquidation( check_price_impact(env, initial_balance, debt_amount)?; acquire_asset_guard(env, &debt_addr)?; - let start_sequence = env.ledger().sequence_number(); + let start_sequence = env.ledger().sequence(); // Fund the liquidator from pool liquidity (the flash leg). token_client.transfer(&pool, &liquidator, &debt_amount); @@ -792,7 +793,7 @@ pub fn execute_flash_loan_liquidation( ) .map_err(|_| FlashLoanError::CallbackFailed)?; - if env.ledger().sequence_number() != start_sequence { + if env.ledger().sequence() != start_sequence { return Err(FlashLoanError::Expired); } @@ -882,7 +883,7 @@ pub fn execute_multi_asset_flash_loan( } let config = get_flash_loan_config(env); - let start_sequence = env.ledger().sequence_number(); + let start_sequence = env.ledger().sequence(); let mut total_fees: i128 = 0; let pool = env.current_contract_address(); @@ -929,7 +930,7 @@ pub fn execute_multi_asset_flash_loan( (user.clone(), legs.clone(), total_fees).into_val(env), ); - if env.ledger().sequence_number() != start_sequence { + if env.ledger().sequence() != start_sequence { return Err(FlashLoanError::Expired); } diff --git a/stellar-lend/contracts/hello-world/src/governance/proposal.rs b/stellar-lend/contracts/hello-world/src/governance/proposal.rs index 9ca0b217..f409795a 100644 --- a/stellar-lend/contracts/hello-world/src/governance/proposal.rs +++ b/stellar-lend/contracts/hello-world/src/governance/proposal.rs @@ -8,7 +8,7 @@ use crate::types::{ }; use crate::events::{ ProposalCancelledEvent, ProposalCreatedEvent, ProposalExecutedEvent, ProposalFailedEvent, - ProposalQueuedEvent, + ProposalQueuedEvent, to_shared_proposal_type, }; use super::{get_admin, execute_proposal_type}; @@ -85,7 +85,7 @@ pub fn create_proposal( ProposalCreatedEvent { proposal_id: next_id, proposer, - proposal_type: proposal.proposal_type, + proposal_type: to_shared_proposal_type(&proposal.proposal_type), description, start_time: proposal.start_time, end_time: proposal.end_time, diff --git a/stellar-lend/contracts/hello-world/src/governance/recovery.rs b/stellar-lend/contracts/hello-world/src/governance/recovery.rs index a3173847..2054e82e 100644 --- a/stellar-lend/contracts/hello-world/src/governance/recovery.rs +++ b/stellar-lend/contracts/hello-world/src/governance/recovery.rs @@ -2,7 +2,7 @@ use soroban_sdk::{Address, Env, Vec}; use crate::errors::GovernanceError; use crate::storage::{GovernanceDataKey, GuardianConfig}; -use crate::types::{MultisigConfig, RecoveryRequest, DEFAULT_RECOVERY_PERIOD}; +use crate::types::{MultisigConfig, RecoveryRequest, DEFAULT_RECOVERY_DELAY, DEFAULT_RECOVERY_PERIOD}; use crate::events::{RecoveryApprovedEvent, RecoveryExecutedEvent, RecoveryStartedEvent}; pub fn start_recovery( @@ -35,6 +35,7 @@ pub fn start_recovery( initiator: initiator.clone(), initiated_at: now, expires_at: now + DEFAULT_RECOVERY_PERIOD, + ready_at: now + DEFAULT_RECOVERY_DELAY, }; env.storage().persistent().set(&recovery_key, &request); diff --git a/stellar-lend/contracts/hello-world/src/governance/voting.rs b/stellar-lend/contracts/hello-world/src/governance/voting.rs index 223aa23d..19094b97 100644 --- a/stellar-lend/contracts/hello-world/src/governance/voting.rs +++ b/stellar-lend/contracts/hello-world/src/governance/voting.rs @@ -8,7 +8,7 @@ use crate::types::{ }; use crate::events::{ VoteCastEvent, VoteDelegatedEvent, VoteDelegationRevokedEvent, VoteLockedEvent, - VotePowerSnapshotTakenEvent, + VotePowerSnapshotTakenEvent, to_shared_vote_type, }; /// Cast a vote on a proposal. @@ -79,7 +79,7 @@ pub fn vote( VoteCastEvent { proposal_id, voter, - vote_type, + vote_type: to_shared_vote_type(&vote_type), voting_power, timestamp: now, } diff --git a/stellar-lend/contracts/hello-world/src/interest_rate.rs b/stellar-lend/contracts/hello-world/src/interest_rate.rs index a4785fc3..de9cb721 100644 --- a/stellar-lend/contracts/hello-world/src/interest_rate.rs +++ b/stellar-lend/contracts/hello-world/src/interest_rate.rs @@ -22,7 +22,7 @@ //! bounded to ±100%. #![allow(unused)] -use soroban_sdk::{contracterror, contracttype, Address, Env, IntoVal}; +use soroban_sdk::{contracterror, contracttype, Address, Env, IntoVal, Vec}; use crate::deposit::{DepositDataKey, ProtocolAnalytics}; use crate::storage::set_temp_lending_index; diff --git a/stellar-lend/contracts/hello-world/src/lib.rs b/stellar-lend/contracts/hello-world/src/lib.rs index 9ccc65a7..bad19f50 100644 --- a/stellar-lend/contracts/hello-world/src/lib.rs +++ b/stellar-lend/contracts/hello-world/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::too_many_arguments)] #![allow(deprecated)] -use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, String, Vec}; +use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, String, Symbol, Vec}; pub mod admin; pub mod amm; @@ -41,6 +41,7 @@ pub mod risk_management; pub mod risk_params; pub mod safe_math; pub mod storage; +pub mod timelock; pub mod treasury; #[cfg(test)] mod test_utils; @@ -171,7 +172,7 @@ impl HelloContract { governance::get_vote(&env, proposal_id, voter) } - pub fn gov_get_multisig_config(env: Env) -> Option { + pub fn gov_get_multisig_config(env: Env) -> Option { governance::get_multisig_config(&env) } @@ -312,7 +313,7 @@ impl HelloContract { asset: Option
, amount: i128, ) -> Result<(), LendingError> { - cross_asset::cross_asset_deposit(&env, user, asset, amount).map_err(Into::into)?; + cross_asset::cross_asset_deposit(&env, user, asset, amount).map_err(LendingError::from)?; Ok(()) } @@ -367,7 +368,7 @@ impl HelloContract { close_factor_bps: 5_000, liquidation_incentive_bps: 1_000, last_update: env.ledger().timestamp(), - flags: storage::FLAG_BORROWING_ENABLED | storage::FLAG_COLLATERAL_ENABLED, + flags: (storage::FLAG_BORROWING_ENABLED | storage::FLAG_COLLATERAL_ENABLED) as u32, }) } @@ -399,7 +400,9 @@ impl HelloContract { asset: Option
, amount: i128, ) -> Result<(), LendingError> { - cross_asset::cross_asset_borrow(&env, user, asset, amount).map_err(Into::into) + cross_asset::cross_asset_borrow(&env, user, asset, amount) + .map_err(LendingError::from) + .map(|_| ()) } /// Withdraw collateral using cross-asset lending @@ -409,7 +412,7 @@ impl HelloContract { asset: Option
, amount: i128, ) -> Result<(), LendingError> { - cross_asset::cross_asset_withdraw(&env, user, asset, amount).map_err(Into::into)?; + cross_asset::cross_asset_withdraw(&env, user, asset, amount).map_err(LendingError::from)?; Ok(()) } @@ -2241,7 +2244,9 @@ impl HelloContract { decimals: u32, source: Address, ) -> Result<(), LendingError> { - oracle::update_price_feed(&env, caller, asset, price, decimals, source).map_err(oracle_err) + oracle::update_price_feed(&env, caller, asset, price, decimals, source) + .map_err(oracle_err) + .map(|_| ()) } pub fn get_price(env: Env, asset: Address) -> Result { diff --git a/stellar-lend/contracts/hello-world/src/mev_protection.rs b/stellar-lend/contracts/hello-world/src/mev_protection.rs index 3dcd1e65..bd364544 100644 --- a/stellar-lend/contracts/hello-world/src/mev_protection.rs +++ b/stellar-lend/contracts/hello-world/src/mev_protection.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracterror, contracttype, Address, Env, String, Symbol}; +use soroban_sdk::{contracterror, contracttype, Address, Env, String, Symbol, Vec}; #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -1186,7 +1186,7 @@ fn record_ordering_signal( let op_key = operation_symbol(env, &operation); let latest_key = MevDataKey::LatestObservation(op_key.clone(), asset.clone()); let previous_key = MevDataKey::PreviousObservation(op_key.clone(), asset.clone()); - let smoothed_key = MevDataKey::SmoothedFee(op_key, asset.clone()); + let smoothed_key = MevDataKey::SmoothedFee(op_key.clone(), asset.clone()); let now = env.ledger().timestamp(); let latest: Option = env.storage().persistent().get(&latest_key); let previous: Option = env.storage().persistent().get(&previous_key); diff --git a/stellar-lend/contracts/hello-world/src/pool_state.rs b/stellar-lend/contracts/hello-world/src/pool_state.rs index 54936300..e182d5ed 100644 --- a/stellar-lend/contracts/hello-world/src/pool_state.rs +++ b/stellar-lend/contracts/hello-world/src/pool_state.rs @@ -292,7 +292,7 @@ fn build( .max(0); // Reserve component. - let reserve_balance = crate::reserve::get_reserve_balance(env, pool.clone()); + let reserve_balance = crate::treasury::get_reserve_balance(env, pool.clone()); let reserve_factor_bps = crate::reserve::get_reserve_factor(env, pool.clone()); PoolStateSnapshot { diff --git a/stellar-lend/contracts/hello-world/src/rate_guard.rs b/stellar-lend/contracts/hello-world/src/rate_guard.rs index 9ff80e73..c0b2e8dc 100644 --- a/stellar-lend/contracts/hello-world/src/rate_guard.rs +++ b/stellar-lend/contracts/hello-world/src/rate_guard.rs @@ -17,7 +17,7 @@ //! rejected and the transaction reverts. //! 5. A TWAP accumulator tracks the running average for external consumers. -use soroban_sdk::{contracterror, contracttype, Address, Env, Symbol}; +use soroban_sdk::{contracterror, contracttype, Address, Env, Symbol, Vec}; use crate::admin::get_admin; diff --git a/stellar-lend/contracts/hello-world/src/reputation.rs b/stellar-lend/contracts/hello-world/src/reputation.rs index 85b487ec..c93c371e 100644 --- a/stellar-lend/contracts/hello-world/src/reputation.rs +++ b/stellar-lend/contracts/hello-world/src/reputation.rs @@ -105,7 +105,7 @@ pub fn apply_decay(env: &Env, address: Address, is_deployer: bool) -> Result) -> i128 { // Try treasury fee config first - let fee_factor = get_static_reserve_factor(env, asset.clone()); + let fee_factor = get_legacy_reserve_factor(env, asset.clone()); // The treasury fee config provides a default; if explicitly set in storage, use that let storage_factor = get_reserve_factor(env, asset); storage_factor diff --git a/stellar-lend/contracts/hello-world/src/reserve_factor.rs b/stellar-lend/contracts/hello-world/src/reserve_factor.rs index f6be1bad..a9bde104 100644 --- a/stellar-lend/contracts/hello-world/src/reserve_factor.rs +++ b/stellar-lend/contracts/hello-world/src/reserve_factor.rs @@ -170,7 +170,7 @@ pub fn preview_reserve_factor( .clamp(0, BASIS_POINTS_SCALE); let curve = get_reserve_factor_curve(env, asset.clone()); let dynamic = calculate_dynamic_reserve_factor(utilization, &curve)?; - let static_factor = crate::reserve::get_static_reserve_factor(env, asset); + let static_factor = crate::reserve::get_legacy_reserve_factor(env, asset); Ok(ReserveFactorPreview { utilization_bps: utilization, diff --git a/stellar-lend/contracts/hello-world/src/risk_params.rs b/stellar-lend/contracts/hello-world/src/risk_params.rs index 49c8459d..f5488730 100644 --- a/stellar-lend/contracts/hello-world/src/risk_params.rs +++ b/stellar-lend/contracts/hello-world/src/risk_params.rs @@ -130,10 +130,14 @@ pub fn initialize_risk_params(env: &Env) -> Result<(), RiskParamsError> { Ok(()) } -/// Get current risk parameters (legacy storage) +/// Get current risk parameters (legacy, unpacked storage layout) pub fn get_legacy_risk_params(env: &Env) -> Option { let config_key = RiskParamsDataKey::RiskParamsConfig; env.storage() + .persistent() + .get::(&config_key) +} + /// Get current risk parameters. /// /// Reads the packed pool-config slot (issue #722). If only the legacy spread @@ -187,16 +191,6 @@ pub fn migrate_from_legacy(env: &Env) -> bool { } /// Get current risk parameters from packed config (#713) -pub fn get_risk_params(env: &Env) -> Option { - let packed = crate::storage::migrate_from_legacy(env, &None).ok()?; - Some(RiskParams { - min_collateral_ratio: packed.min_collateral_ratio_bps, - liquidation_threshold: packed.liquidation_threshold_bps, - close_factor: packed.close_factor_bps, - liquidation_incentive: packed.liquidation_incentive_bps, - last_update: packed.last_update, - }) -} /// Validate risk configuration fn validate_risk_params(config: &RiskParams) -> Result<(), RiskParamsError> { diff --git a/stellar-lend/contracts/hello-world/src/types.rs b/stellar-lend/contracts/hello-world/src/types.rs index d44789a4..7d012c18 100644 --- a/stellar-lend/contracts/hello-world/src/types.rs +++ b/stellar-lend/contracts/hello-world/src/types.rs @@ -235,6 +235,7 @@ pub struct RecoveryRequest { pub initiator: Address, pub initiated_at: u64, pub expires_at: u64, + pub ready_at: u64, } // ======================================================================== diff --git a/stellar-lend/contracts/pool-factory/src/lib.rs b/stellar-lend/contracts/pool-factory/src/lib.rs index 3d3edfaf..40f2fe79 100644 --- a/stellar-lend/contracts/pool-factory/src/lib.rs +++ b/stellar-lend/contracts/pool-factory/src/lib.rs @@ -72,7 +72,7 @@ impl PoolFactory { let pool_index = pool_count + 1; - let pool_address = Address::from_contract_id(&env.contract_id()); + let pool_address = env.current_contract_address(); let pool = Pool { address: pool_address.clone(), @@ -123,8 +123,8 @@ impl PoolFactory { .get(&Symbol::new(&env, "pools")) .unwrap_or_else(|| Vec::new(&env)); - if (index as usize) < pools.len() { - Some(pools.get(index as usize).unwrap()) + if index < pools.len() { + Some(pools.get(index).unwrap()) } else { None } @@ -150,10 +150,10 @@ impl PoolFactory { .get(&pools_key) .unwrap_or_else(|| Vec::new(&env)); - if (pool_index as usize) < pools.len() { - let mut pool = pools.get(pool_index as usize).unwrap(); + if pool_index < pools.len() { + let mut pool = pools.get(pool_index).unwrap(); pool.config = config; - pools.set(pool_index as usize, pool); + pools.set(pool_index, pool); env.storage().instance().set(&pools_key, &pools); } }