This document describes the persistent storage structure of the StellarLend protocol on Soroban. It serves as a reference for developers, auditors, and for planning contract upgrades.
StellarLend uses Soroban's persistent() storage for all long-term data. This ensures that user balances, protocol configurations, and risk parameters remain available across ledger boundaries. All keys are defined using contracttype enums or Symbol to ensure type safety and avoid collisions.
| Key (Symbol/Type) | Value Type | Description |
|---|---|---|
admin |
Address |
Protocol admin address authorized to manage assets. |
configs |
Map<AssetKey, AssetConfig> |
Configuration for each supported asset (factors, caps, prices). |
positions |
Map<UserAssetKey, AssetPosition> |
Per-user, per-asset collateral and debt balances. |
supplies |
Map<AssetKey, i128> |
Total supply (deposits) for each asset. |
borrows |
Map<AssetKey, i128> |
Total borrows (debt) for each asset. |
assets |
Vec<AssetKey> |
List of all registered assets in the protocol. |
Key (RiskDataKey) |
Value Type | Description |
|---|---|---|
RiskConfig |
RiskConfig |
Global risk parameters (MCR, liquidation threshold, close factor). |
Admin |
Address |
Admin address for risk management operations. |
EmergencyPause |
bool |
Global flag to halt all protocol operations. |
Key (DepositDataKey) |
Value Type | Description |
|---|---|---|
CollateralBalance(Address) |
i128 |
Per-user cumulative collateral balance (deprecated in favor of cross_asset positions). |
AssetParams(Address) |
AssetParams |
Legacy asset parameters. |
Position(Address) |
Position |
User's unified position (legacy module). |
ProtocolAnalytics |
ProtocolAnalytics |
Aggregate protocol metrics (deposits, borrows, TVL). |
UserAnalytics(Address) |
UserAnalytics |
Detailed per-user activity and risk metrics. |
Key (InterestRateDataKey) |
Value Type | Description |
|---|---|---|
InterestRateConfig |
InterestRateConfig |
Kink-based model parameters (base rate, kink, multipliers). |
Admin |
Address |
Admin address for interest rate adjustments. |
Key (OracleDataKey) |
Value Type | Description |
|---|---|---|
PriceFeed(Address) |
PriceFeed |
Latest price, timestamp, and provider for an asset. |
FallbackOracle(Address) |
Address |
Designated fallback price provider for an asset. |
PriceCache(Address) |
CachedPrice |
TTL-bounded price cache for gas efficiency. |
OracleConfig |
OracleConfig |
Global oracle safety parameters (deviation, staleness). |
Key (FlashLoanDataKey) |
Value Type | Description |
|---|---|---|
FlashLoanConfig |
FlashLoanConfig |
Fee basis points and amount limits. |
ActiveFlashLoan(Addr, Addr) |
FlashLoanRecord |
Reentrancy guard and transient loan record. |
Key (AnalyticsDataKey) |
Value Type | Description |
|---|---|---|
ProtocolMetrics |
ProtocolMetrics |
Cached protocol-wide stats snapshot. |
UserMetrics(Address) |
UserMetrics |
Cached per-user stats snapshot. |
ActivityLog |
Vec<ActivityEntry> |
Global activity history (max 10,000 entries). |
TotalUsers |
u64 |
Total number of unique users. |
TotalTransactions |
u64 |
Global transaction counter. |
pub struct AssetPosition {
pub collateral: i128, // Asset's native units
pub debt_principal: i128, // Principal borrowed
pub accrued_interest: i128, // Accumulated interest
pub last_updated: u64, // Timestamp of last update
}pub struct RiskConfig {
pub min_collateral_ratio: i128, // Basis points (11000 = 110%)
pub liquidation_threshold: i128, // Basis points
pub close_factor: i128, // Basis points
pub liquidation_incentive: i128, // Basis points
pub pause_switches: Map<Symbol, bool>,
pub last_update: u64,
}Soroban supports contract upgrades via env.deployer().update_current_contract_wasm(new_wasm_hash). This replaces the contract code while preserving existing storage.
- Append Only: Always add new variants to the end of
contracttypeenums to preserve discriminant mapping. - Structural Stability: Avoid deleting or reordering fields in structs. If a field is deprecated, keep it but ignore its value.
- Key Consistency: Ensure that
contracttypedefinitions used for storage keys are identical across versions.
If a storage layout change is unavoidable (e.g., merging two maps into one), follow this process:
- Deployment: Deploy the new contract code.
- Migration Transaction: Execute a one-time admin function that reads old data, transforms it, and writes it to new keys.
- Cleanup: Remove the old keys to reclaim rent/storage costs.
- Verification: Execute a test suite against the migrated state.
- No Overwrites: Storage keys are designed to be unique. Map-based keys use composite structures like
UserAssetKey(Address, AssetKey)to prevent users from affecting each other's data. - Persistent Only: All critical protocol state is stored in
persistent()storage to prevent expiration (subject to rent payments). - Admin Isolation: Admin addresses are stored in module-specific keys, allowing for granular permission management or a unified global admin.
- All
contracttypeenums have unique variants. - No
temporary()orinstance()storage is used for critical state. -
AssetKeycorrectly handles both Native (XLM) and Token assets. - Key collisions between modules are avoided by using unique Enum types for keys.
The pool risk configuration stored by hello-world (risk_params.rs) used to
live as one spread struct:
RiskParamsDataKey::RiskParamsConfig → RiskParams {
min_collateral_ratio: i128, // 16 bytes
liquidation_threshold: i128, // 16 bytes
close_factor: i128, // 16 bytes
liquidation_incentive: i128, // 16 bytes
last_update: u64, // 8 bytes
}
// ≈ 72 bytes payload per read/write
All four bps fields are validated to 0..=50_000 — well inside u16 — so the
whole record packs into a single u128:
RiskParamsDataKey::PackedRiskParamsConfig → u128
bits 0..16 min_collateral_ratio (u16 bps)
bits 16..32 liquidation_threshold (u16 bps)
bits 32..48 close_factor (u16 bps)
bits 48..64 liquidation_incentive (u16 bps)
bits 64..128 last_update (u64 timestamp)
// 16 bytes payload — ~4.5× smaller than the spread layout
Implemented as pure helpers in risk_params.rs (pack_risk_params /
unpack_risk_params). The public interface is unchanged: readers still receive
a RiskParams via get_risk_params.
- Lazy:
get_risk_paramsfalls back to the legacy slot and migrates on first read after upgrade. - Idempotent + explicit:
risk_params::migrate_from_legacyreturnsfalsewhen unnecessary; exposed as the entrypointmigrate_pool_config_packed.
- Pack small validated integers (16 bits is enough for any bps field).
- Keep the unpacked struct as the public view; packing is a representation detail.
- Migrate lazily rather than forcing a migration transaction on users.
- Bound persistent
Veclogs (e.g.MAX_SANDWICH_LOGin the MEV module). - Document the bit layout next to the key variant and in this document.