Skip to content

Commit 7858c6f

Browse files
authored
Merge pull request #358 from CollinsC1O/Oracle-Integration
feat: Oracle Integration
2 parents 8e64472 + 86fd6b8 commit 7858c6f

5 files changed

Lines changed: 109 additions & 60 deletions

File tree

contract/src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,6 @@ pub enum ContractError {
225225
// Social feature errors (70-74)
226226
CannotFollowSelf = 70,
227227
NotFollowing = 71,
228+
NoTrigger = 113,
229+
PriceConditionNotMet = 114,
228230
}

contract/src/events.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@ pub struct EvOracleRemoved { pub v: u32, pub base: Address, pub quote: Addres
375375
pub struct EvOraclePriceFetched { pub v: u32, pub base: Address, pub quote: Address, pub price: i128, pub decimals: u32 }
376376
#[contracttype] #[derive(Clone, Debug)]
377377
pub struct EvOracleUnavailable { pub v: u32, pub base: Address, pub quote: Address }
378+
#[contracttype] #[derive(Clone, Debug)]
379+
pub struct EvTriggerExecuted { pub v: u32, pub trade_id: u64, pub action: crate::types::TriggerAction }
378380

379381
pub fn emit_oracle_registered(env: &Env, base: Address, quote: Address, oracle: Address, priority: u32) {
380382
env.events().publish((cat_oracle(), symbol_short!("orc_reg")), EvOracleRegistered { v: EVENT_VERSION, base, quote, oracle, priority });
@@ -388,6 +390,9 @@ pub fn emit_oracle_price_fetched(env: &Env, base: Address, quote: Address, price
388390
pub fn emit_oracle_unavailable(env: &Env, base: Address, quote: Address) {
389391
env.events().publish((cat_oracle(), symbol_short!("orc_err")), EvOracleUnavailable { v: EVENT_VERSION, base, quote });
390392
}
393+
pub fn emit_trigger_executed(env: &Env, trade_id: u64, action: &crate::types::TriggerAction) {
394+
env.events().publish((cat_oracle(), symbol_short!("trig_ex")), EvTriggerExecuted { v: EVENT_VERSION, trade_id, action: action.clone() });
395+
}
391396

392397
// ---------------------------------------------------------------------------
393398
// Upgrade system events

contract/src/lib.rs

Lines changed: 66 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,10 @@ use soroban_sdk::{contract, contractimpl, token::TokenClient, Address, BytesN, E
2525

2626
pub use errors::ContractError;
2727
pub use types::{
28-
ArbitrationConfig, DisclosureGrant, DisputeResolution, Proposal, ProposalAction,
29-
ArbitrationConfig, ArbitratorVote, DisclosureGrant, DisputeResolution, MultiSigConfig,
30-
Proposal, ProposalAction, ProposalStatus, Subscription, SubscriptionTier, TierConfig,
31-
TemplateTerms, TemplateVersion, Trade, TradePrivacy, TradeStatus, TradeTemplate,
32-
UserTier, UserTierInfo, VotingSummary,
33-
ArbitratorReputation, DisclosureGrant, DisputeResolution, Proposal, ProposalAction,
34-
ProposalStatus, Subscription, SubscriptionTier, TierConfig, TemplateTerms, TemplateVersion,
35-
Trade, TradePrivacy, TradeStatus, TradeTemplate, UserTier, UserTierInfo,
28+
ArbitrationConfig, ArbitratorReputation, ArbitratorVote, DisclosureGrant, DisputeResolution,
29+
MultiSigConfig, PriceTrigger, Proposal, ProposalAction, ProposalStatus, Subscription,
30+
SubscriptionTier, TemplateTerms, TemplateVersion, Trade, TradePrivacy, TradeStatus,
31+
TradeTemplate, TriggerAction, UserTier, UserTierInfo, VotingSummary,
3632
};
3733
pub use queries::{PageParams, SortDirection, TradeFilter, TradeSortField, TradeStats};
3834
pub use oracle::{OracleEntry, PriceData, PriceValidation};
@@ -244,53 +240,6 @@ impl StellarEscrowContract {
244240
/// Create a trade with multi-signature arbitration.
245241
/// All arbitrators in `config` must be registered; threshold must be > 0
246242
/// and ≤ arbitrators count.
247-
pub fn create_multisig_trade(
248-
env: Env,
249-
seller: Address,
250-
buyer: Address,
251-
amount: u64,
252-
config: MultiSigConfig,
253-
expiry_time: Option<u64>,
254-
currency: Option<Address>,
255-
) -> Result<u64, ContractError> {
256-
require_initialized(&env)?;
257-
require_not_paused(&env)?;
258-
if amount == 0 {
259-
return Err(ContractError::InvalidAmount);
260-
}
261-
if config.threshold == 0 || config.threshold > config.arbitrators.len() {
262-
return Err(ContractError::InvalidMultiSigConfig);
263-
}
264-
for i in 0..config.arbitrators.len() {
265-
if !has_arbitrator(&env, &config.arbitrators.get(i).unwrap()) {
266-
return Err(ContractError::ArbitratorNotRegistered);
267-
}
268-
}
269-
if let Some(expiry) = expiry_time {
270-
if expiry <= env.ledger().timestamp() {
271-
return Err(ContractError::InvalidExpiry);
272-
}
273-
}
274-
seller.require_auth();
275-
let token = currency.unwrap_or(get_usdc_token(&env)?);
276-
let trade_id = increment_trade_counter(&env)?;
277-
let fee = calc_fee(&env, &seller, amount)?;
278-
let trade = Trade {
279-
id: trade_id,
280-
seller: seller.clone(),
281-
buyer: buyer.clone(),
282-
amount,
283-
fee,
284-
arbitrator: Some(ArbitrationConfig::MultiSig(config)),
285-
status: TradeStatus::Created,
286-
expiry_time,
287-
currency: token,
288-
metadata: None,
289-
};
290-
save_trade(&env, trade_id, &trade);
291-
events::emit_trade_created(&env, trade_id, seller, buyer, amount, trade.currency);
292-
Ok(trade_id)
293-
}
294243
295244
/// Cast a vote on a disputed multi-sig trade.
296245
pub fn cast_vote(
@@ -566,8 +515,8 @@ impl StellarEscrowContract {
566515
arbitrator: Option<Address>,
567516
expiry_time: Option<u64>,
568517
currency: Option<Address>,
569-
metadata: Option<TradeMetadata>,
570-
metadata: OptionalMetadata,
518+
metadata: Option<soroban_sdk::String>,
519+
trigger: Option<PriceTrigger>,
571520
) -> Result<u64, ContractError> {
572521
require_initialized(&env)?;
573522
require_not_paused(&env)?;
@@ -619,6 +568,7 @@ impl StellarEscrowContract {
619568
expiry_time,
620569
currency: token,
621570
metadata,
571+
trigger,
622572
};
623573
save_trade(&env, trade_id, &trade);
624574
events::emit_trade_created(&env, trade_id, seller.clone(), buyer.clone(), amount);
@@ -637,7 +587,8 @@ impl StellarEscrowContract {
637587
multisig_config: MultiSigConfig,
638588
expiry_time: Option<u64>,
639589
currency: Option<Address>,
640-
metadata: OptionalMetadata,
590+
metadata: Option<soroban_sdk::String>,
591+
trigger: Option<PriceTrigger>,
641592
) -> Result<u64, ContractError> {
642593
require_initialized(&env)?;
643594
require_not_paused(&env)?;
@@ -673,7 +624,7 @@ impl StellarEscrowContract {
673624
validate_user_compliance(&env, &seller, amount)?;
674625
validate_user_compliance(&env, &buyer, amount)?;
675626

676-
if let OptionalMetadata::Some(ref meta) = metadata {
627+
if let Some(ref meta) = metadata {
677628
validate_metadata(meta)?;
678629
}
679630

@@ -694,6 +645,7 @@ impl StellarEscrowContract {
694645
expiry_time,
695646
currency: token,
696647
metadata,
648+
trigger,
697649
};
698650
save_trade(&env, trade_id, &trade);
699651
events::emit_trade_created(&env, trade_id, seller.clone(), buyer.clone(), amount);
@@ -1154,6 +1106,59 @@ impl StellarEscrowContract {
11541106
oracle::validate_trade_price(&env, &base, &quote, trade_amount, min_usd, max_usd)
11551107
}
11561108

1109+
/// Check and execute a price trigger for a trade.
1110+
/// Can be called by anyone; trigger logic is automated based on oracle price.
1111+
pub fn execute_price_trigger(env: Env, trade_id: u64) -> Result<(), ContractError> {
1112+
require_initialized(&env)?;
1113+
require_not_paused(&env)?;
1114+
let mut trade = get_trade(&env, trade_id)?;
1115+
let trigger = match &trade.trigger {
1116+
Some(t) => t.clone(),
1117+
None => return Err(ContractError::NoTrigger),
1118+
};
1119+
// Trigger can only execute for funded trades
1120+
if trade.status != TradeStatus::Funded {
1121+
return Err(ContractError::InvalidStatus);
1122+
}
1123+
1124+
if oracle::check_trigger(&env, &trigger)? {
1125+
match trigger.action {
1126+
TriggerAction::Cancel => {
1127+
// Refund entire escrowed amount to buyer
1128+
let token_client = token::Client::new(&env, &trade.currency);
1129+
token_client.transfer(
1130+
&env.current_contract_address(),
1131+
&trade.buyer,
1132+
&(trade.amount as i128),
1133+
);
1134+
trade.status = TradeStatus::Cancelled;
1135+
}
1136+
TriggerAction::Release => {
1137+
// Release to seller, minus platform fee
1138+
let token_client = token::Client::new(&env, &trade.currency);
1139+
let payout = trade.amount.checked_sub(trade.fee).ok_or(ContractError::Overflow)?;
1140+
token_client.transfer(
1141+
&env.current_contract_address(),
1142+
&trade.seller,
1143+
&(payout as i128),
1144+
);
1145+
// Add fee to contract's accumulated revenue
1146+
let current_fees = storage::get_currency_fees(&env, &trade.currency);
1147+
let new_fees = current_fees.checked_add(trade.fee).ok_or(ContractError::Overflow)?;
1148+
storage::set_currency_fees(&env, &trade.currency, new_fees);
1149+
storage::add_accumulated_fees(&env, trade.fee)?;
1150+
trade.status = TradeStatus::Triggered;
1151+
}
1152+
}
1153+
save_trade(&env, trade_id, &trade);
1154+
events::emit_trigger_executed(&env, trade_id, &trigger.action);
1155+
} else {
1156+
return Err(ContractError::PriceConditionNotMet);
1157+
}
1158+
1159+
Ok(())
1160+
}
1161+
11571162
// -------------------------------------------------------------------------
11581163
// Emergency Pause
11591164
// -------------------------------------------------------------------------
@@ -1682,7 +1687,8 @@ impl StellarEscrowContract {
16821687
status: TradeStatus::AwaitingBridge,
16831688
expiry_time: None,
16841689
currency: get_usdc_token(&env)?,
1685-
metadata: OptionalMetadata::None,
1690+
metadata: None,
1691+
trigger: None,
16861692
};
16871693
save_trade(&env, trade_id, &trade);
16881694
save_cross_chain_info(&env, trade_id, &CrossChainInfo {

contract/src/oracle.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use soroban_sdk::{contractclient, contracttype, symbol_short, Address, Env, Vec};
1212

1313
use crate::errors::ContractError;
14+
use crate::types::{PriceTrigger, TriggerAction};
1415

1516
// ---------------------------------------------------------------------------
1617
// Constants
@@ -210,3 +211,13 @@ pub fn validate_trade_price(
210211
usd_value,
211212
})
212213
}
214+
215+
/// Check if a price trigger's condition is currently met.
216+
pub fn check_trigger(env: &Env, trigger: &PriceTrigger) -> Result<bool, ContractError> {
217+
let pd = get_price(env, &trigger.base, &trigger.quote)?;
218+
if trigger.trigger_above {
219+
Ok(pd.price >= trigger.target_price)
220+
} else {
221+
Ok(pd.price <= trigger.target_price)
222+
}
223+
}

contract/src/types.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub enum TradeStatus {
4747
Cancelled,
4848
AwaitingBridge, // cross-chain: waiting for bridge oracle confirmation
4949
BridgeFailed, // cross-chain: bridge attestation failed
50+
Triggered, // price-based trigger executed
5051
}
5152

5253
#[contracttype]
@@ -109,6 +110,28 @@ pub enum ArbitrationConfig {
109110
MultiSig(MultiSigConfig),
110111
}
111112

113+
// ---------------------------------------------------------------------------
114+
// Price Triggers
115+
// ---------------------------------------------------------------------------
116+
117+
#[contracttype]
118+
#[derive(Clone, Debug, Eq, PartialEq)]
119+
pub enum TriggerAction {
120+
Cancel,
121+
Release,
122+
}
123+
124+
#[contracttype]
125+
#[derive(Clone, Debug, Eq, PartialEq)]
126+
pub struct PriceTrigger {
127+
pub base: Address,
128+
pub quote: Address,
129+
pub target_price: i128,
130+
/// If true, trigger when price >= target_price. If false, trigger when price <= target_price.
131+
pub trigger_above: bool,
132+
pub action: TriggerAction,
133+
}
134+
112135

113136
#[contracttype]
114137
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -127,6 +150,8 @@ pub struct Trade {
127150
pub currency: Address,
128151
/// Optional JSON-like string metadata (product info, shipping details, etc.)
129152
pub metadata: Option<String>,
153+
/// Optional price-based trigger
154+
pub trigger: Option<PriceTrigger>,
130155
}
131156

132157
#[contracttype]

0 commit comments

Comments
 (0)