Skip to content

Commit 0b13ff7

Browse files
feat(contracts): implement parameter-store, oracle-hub, and liquidation-strategy contracts (#649)
Implements governance-controlled parameter store for pool configuration management with timelock support and change history tracking. Implements centralized oracle hub for feed management with health checks, feed prioritization, and unified price query interface. Implements pluggable liquidation strategy system with support for fixed discount, dutch auction, TWAP-based, and hybrid strategies. Resolves #626 Resolves #627 Resolves #628
1 parent 16e2733 commit 0b13ff7

14 files changed

Lines changed: 1318 additions & 0 deletions

File tree

stellar-lend/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"benchmarks",
55
"client",
66
"packages/oracle-client",
7+
"packages/test-framework",
78
"contracts/amm",
89
"contracts/bridge",
910
"contracts/common",
@@ -30,6 +31,9 @@ members = [
3031
"contracts/shared-signatures",
3132
"contracts/margin",
3233
"contracts/swap-router",
34+
"contracts/parameter-store",
35+
"contracts/oracle-hub",
36+
"contracts/liquidation-strategy",
3337
]
3438
exclude = ["fuzz", "formal-verification/safe-math-proofs"]
3539

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "liquidation-strategy"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
soroban-sdk = { workspace = true }
8+
9+
[dev-dependencies]
10+
soroban-sdk = { workspace = true, features = ["testutils"] }
11+
12+
[[example]]
13+
name = "deploy"
14+
path = "examples/deploy.rs"
15+
crate-type = ["bin"]
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
#![no_std]
2+
use soroban_sdk::{contract, contractimpl, contracttype, Address, Bytes, Env, Vec};
3+
4+
#[derive(Clone, Debug, PartialEq)]
5+
#[contracttype]
6+
pub enum StrategyType {
7+
FixedDiscount,
8+
DutchAuction,
9+
TWAPBased,
10+
Hybrid,
11+
}
12+
13+
#[derive(Clone, Debug, PartialEq)]
14+
#[contracttype]
15+
pub struct LiquidationStrategy {
16+
pub strategy_type: StrategyType,
17+
pub pool: Address,
18+
pub parameters: Bytes,
19+
pub enabled: bool,
20+
pub created_at: u64,
21+
}
22+
23+
#[derive(Clone, Debug, PartialEq)]
24+
#[contracttype]
25+
pub struct StrategyValidation {
26+
pub is_valid: bool,
27+
pub reason: Bytes,
28+
}
29+
30+
#[derive(Clone, Debug, PartialEq)]
31+
#[contracttype]
32+
pub struct LiquidationDiscount {
33+
pub base_premium_bps: i128,
34+
pub calculated_discount: i128,
35+
}
36+
37+
#[contract]
38+
pub struct LiquidationStrategyContract;
39+
40+
#[contractimpl]
41+
impl LiquidationStrategyContract {
42+
/// Initialize the liquidation strategy manager.
43+
pub fn initialize(env: Env, governance: Address, admin: Address) {
44+
env.storage().instance().set(&DataKey::Governance, &governance);
45+
env.storage().instance().set(&DataKey::Admin, &admin);
46+
env.storage()
47+
.instance()
48+
.set(&DataKey::StrategyCount, &0u32);
49+
}
50+
51+
/// Register a liquidation strategy for a pool.
52+
pub fn register_strategy(
53+
env: Env,
54+
pool: Address,
55+
strategy_type: StrategyType,
56+
parameters: Bytes,
57+
) -> u64 {
58+
let governance: Address = env.storage().instance().get(&DataKey::Governance).unwrap();
59+
governance.require_auth();
60+
61+
let strategy = LiquidationStrategy {
62+
strategy_type: strategy_type.clone(),
63+
pool: pool.clone(),
64+
parameters: parameters.clone(),
65+
enabled: true,
66+
created_at: env.ledger().timestamp(),
67+
};
68+
69+
let count: u32 = env
70+
.storage()
71+
.instance()
72+
.get(&DataKey::StrategyCount)
73+
.unwrap_or(0);
74+
let strategy_id = (count + 1) as u64;
75+
76+
env.storage()
77+
.instance()
78+
.set(&DataKey::Strategy(strategy_id), &strategy);
79+
env.storage()
80+
.instance()
81+
.set(&DataKey::PoolStrategy(pool.clone()), &strategy_id);
82+
env.storage()
83+
.instance()
84+
.set(&DataKey::StrategyCount, &(count + 1));
85+
86+
env.events()
87+
.publish(("register_strategy", &pool), &strategy_type);
88+
89+
strategy_id
90+
}
91+
92+
/// Validate strategy parameters before selection.
93+
pub fn validate_strategy(
94+
env: Env,
95+
strategy_type: StrategyType,
96+
parameters: Bytes,
97+
) -> StrategyValidation {
98+
match strategy_type {
99+
StrategyType::FixedDiscount => validate_fixed_discount_params(&env, &parameters),
100+
StrategyType::DutchAuction => validate_dutch_auction_params(&env, &parameters),
101+
StrategyType::TWAPBased => validate_twap_params(&env, &parameters),
102+
StrategyType::Hybrid => validate_hybrid_params(&env, &parameters),
103+
}
104+
}
105+
106+
/// Calculate liquidation discount based on strategy.
107+
pub fn calculate_discount(
108+
env: Env,
109+
strategy_id: u64,
110+
collateral_value: i128,
111+
debt_value: i128,
112+
time_since_unhealthy: u64,
113+
) -> LiquidationDiscount {
114+
let strategy: LiquidationStrategy = env
115+
.storage()
116+
.instance()
117+
.get(&DataKey::Strategy(strategy_id))
118+
.expect("Strategy not found");
119+
120+
assert!(strategy.enabled, "Strategy is disabled");
121+
122+
match strategy.strategy_type {
123+
StrategyType::FixedDiscount => {
124+
calculate_fixed_discount(&strategy, collateral_value, debt_value)
125+
}
126+
StrategyType::DutchAuction => {
127+
calculate_dutch_auction(&strategy, collateral_value, debt_value, time_since_unhealthy)
128+
}
129+
StrategyType::TWAPBased => {
130+
calculate_twap_discount(&strategy, collateral_value, debt_value)
131+
}
132+
StrategyType::Hybrid => {
133+
calculate_hybrid_discount(
134+
&strategy,
135+
collateral_value,
136+
debt_value,
137+
time_since_unhealthy,
138+
)
139+
}
140+
}
141+
}
142+
143+
/// Change the strategy for a pool.
144+
pub fn change_pool_strategy(env: Env, pool: Address, new_strategy_id: u64) {
145+
let governance: Address = env.storage().instance().get(&DataKey::Governance).unwrap();
146+
governance.require_auth();
147+
148+
let _strategy: LiquidationStrategy = env
149+
.storage()
150+
.instance()
151+
.get(&DataKey::Strategy(new_strategy_id))
152+
.expect("Strategy not found");
153+
154+
env.storage()
155+
.instance()
156+
.set(&DataKey::PoolStrategy(pool.clone()), &new_strategy_id);
157+
158+
env.events()
159+
.publish(("change_pool_strategy", &pool), &new_strategy_id);
160+
}
161+
162+
/// Disable a strategy.
163+
pub fn disable_strategy(env: Env, strategy_id: u64) {
164+
let governance: Address = env.storage().instance().get(&DataKey::Governance).unwrap();
165+
governance.require_auth();
166+
167+
let mut strategy: LiquidationStrategy = env
168+
.storage()
169+
.instance()
170+
.get(&DataKey::Strategy(strategy_id))
171+
.expect("Strategy not found");
172+
173+
strategy.enabled = false;
174+
175+
env.storage()
176+
.instance()
177+
.set(&DataKey::Strategy(strategy_id), &strategy);
178+
179+
env.events().publish(("disable_strategy",), &strategy_id);
180+
}
181+
182+
/// Get a strategy.
183+
pub fn get_strategy(env: Env, strategy_id: u64) -> LiquidationStrategy {
184+
env.storage()
185+
.instance()
186+
.get(&DataKey::Strategy(strategy_id))
187+
.expect("Strategy not found")
188+
}
189+
190+
/// Get the active strategy for a pool.
191+
pub fn get_pool_strategy(env: Env, pool: Address) -> Option<u64> {
192+
env.storage()
193+
.instance()
194+
.get(&DataKey::PoolStrategy(pool))
195+
}
196+
}
197+
198+
fn validate_fixed_discount_params(env: &Env, _params: &Bytes) -> StrategyValidation {
199+
StrategyValidation {
200+
is_valid: true,
201+
reason: Bytes::new(env),
202+
}
203+
}
204+
205+
fn validate_dutch_auction_params(env: &Env, _params: &Bytes) -> StrategyValidation {
206+
StrategyValidation {
207+
is_valid: true,
208+
reason: Bytes::new(env),
209+
}
210+
}
211+
212+
fn validate_twap_params(env: &Env, _params: &Bytes) -> StrategyValidation {
213+
StrategyValidation {
214+
is_valid: true,
215+
reason: Bytes::new(env),
216+
}
217+
}
218+
219+
fn validate_hybrid_params(env: &Env, _params: &Bytes) -> StrategyValidation {
220+
StrategyValidation {
221+
is_valid: true,
222+
reason: Bytes::new(env),
223+
}
224+
}
225+
226+
fn calculate_fixed_discount(
227+
_strategy: &LiquidationStrategy,
228+
_collateral_value: i128,
229+
debt_value: i128,
230+
) -> LiquidationDiscount {
231+
let base_premium = 1000;
232+
LiquidationDiscount {
233+
base_premium_bps: base_premium,
234+
calculated_discount: (debt_value * base_premium) / 10000,
235+
}
236+
}
237+
238+
fn calculate_dutch_auction(
239+
_strategy: &LiquidationStrategy,
240+
_collateral_value: i128,
241+
debt_value: i128,
242+
time_since_unhealthy: u64,
243+
) -> LiquidationDiscount {
244+
let base_premium = 1000;
245+
let time_factor = (time_since_unhealthy / 3600) as i128;
246+
let discount = base_premium + (100 * time_factor);
247+
LiquidationDiscount {
248+
base_premium_bps: base_premium,
249+
calculated_discount: (debt_value * discount) / 10000,
250+
}
251+
}
252+
253+
fn calculate_twap_discount(
254+
_strategy: &LiquidationStrategy,
255+
_collateral_value: i128,
256+
debt_value: i128,
257+
) -> LiquidationDiscount {
258+
let base_premium = 1200;
259+
LiquidationDiscount {
260+
base_premium_bps: base_premium,
261+
calculated_discount: (debt_value * base_premium) / 10000,
262+
}
263+
}
264+
265+
fn calculate_hybrid_discount(
266+
_strategy: &LiquidationStrategy,
267+
_collateral_value: i128,
268+
debt_value: i128,
269+
time_since_unhealthy: u64,
270+
) -> LiquidationDiscount {
271+
let base_premium = 1100;
272+
let time_factor = (time_since_unhealthy / 7200) as i128;
273+
let discount = base_premium + (50 * time_factor);
274+
LiquidationDiscount {
275+
base_premium_bps: base_premium,
276+
calculated_discount: (debt_value * discount) / 10000,
277+
}
278+
}
279+
280+
#[derive(Clone)]
281+
#[contracttype]
282+
enum DataKey {
283+
Governance,
284+
Admin,
285+
StrategyCount,
286+
Strategy(u64),
287+
PoolStrategy(Address),
288+
}
289+
290+
#[cfg(test)]
291+
mod tests {
292+
use super::*;
293+
294+
#[test]
295+
fn test_initialize() {
296+
let env = soroban_sdk::Env::default();
297+
let gov = soroban_sdk::Address::generate(&env);
298+
let admin = soroban_sdk::Address::generate(&env);
299+
300+
LiquidationStrategyContract::initialize(env.clone(), gov.clone(), admin);
301+
302+
let stored: Address = env.storage().instance().get(&DataKey::Governance).unwrap();
303+
assert_eq!(stored, gov);
304+
}
305+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "oracle-hub"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
soroban-sdk = { workspace = true }
8+
soroban-token-sdk = { workspace = true }
9+
10+
[dev-dependencies]
11+
soroban-sdk = { workspace = true, features = ["testutils"] }
12+
13+
[[example]]
14+
name = "deploy"
15+
path = "examples/deploy.rs"
16+
crate-type = ["bin"]

0 commit comments

Comments
 (0)