Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["utility_contracts", "price_oracle", "resource-token", "common"]
members = ["utility_contracts", "price_oracle", "resource-token", "common", "settlement"]

[workspace.dependencies]
soroban-sdk = "23.2.4"
15 changes: 15 additions & 0 deletions contracts/settlement/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,18 @@ pub const MAX_SETTLEMENT: i128 = 1_000_000_000_000_000_000; // 1e18

/// Denominator for basis points calculations
pub const BPS_DENOMINATOR: u32 = 10000;

/// Maximum allowed slippage in basis points (100 bps = 1%)
/// Range: [1, 500] (0.01% to 5%)
pub const MAX_SLIPPAGE_BPS: u32 = 100;

/// Maximum settlement volume per call (1M tokens with 7 decimals)
#[allow(dead_code)]
pub const MAX_VOLUME: i128 = 10_000_000_000_000; // 1_000_000 * 1e7

/// Maximum oracle rate with 7 decimals
#[allow(dead_code)]
pub const MAX_RATE: i128 = 10_000_000_000_000; // 1_000_000 * 1e7

/// Denominator for fixed-point operations (7 decimal places)
pub const DECIMAL_DENOMINATOR: i128 = 10_000_000; // 1e7
64 changes: 64 additions & 0 deletions contracts/settlement/src/conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use soroban_sdk::{panic_with_error, Address, Env};

use crate::constants::{BPS_DENOMINATOR, DECIMAL_DENOMINATOR, MAX_SLIPPAGE_BPS};
use crate::rate_application::get_rate;
use crate::SettlementError;

/// Convert resource token volume to settlement currency using the oracle exchange rate.
///
/// Flow:
/// 1. Fetches the current oracle rate
/// 2. Computes the settlement amount = volume * rate / 1e7
/// 3. Checks actual amount against slippage tolerance and user's minimum
///
/// # Returns
/// The settlement amount computed from the oracle rate
///
/// # Panics
/// * `SlippageExceeded` if slippage exceeds MAX_SLIPPAGE_BPS or actual < min_expected_amount
pub fn convert_to_settlement_currency(
env: &Env,
oracle: &Address,
volume: i128,
min_expected_amount: Option<i128>,
) -> i128 {
let rate = get_rate(env, oracle);

let expected_amount = volume
.checked_mul(rate)
.expect("conversion overflow")
.checked_div(DECIMAL_DENOMINATOR)
.expect("conversion underflow");

let actual_amount = expected_amount;

let slippage_bps = if expected_amount > 0 {
let diff = expected_amount.saturating_sub(actual_amount);
(diff.checked_mul(BPS_DENOMINATOR as i128)
.expect("slippage overflow"))
.checked_div(expected_amount)
.expect("slippage underflow") as u32
} else {
0
};

if slippage_bps > MAX_SLIPPAGE_BPS {
env.events().publish(
(soroban_sdk::symbol_short!("SlpSlipp"),),
(expected_amount, actual_amount, slippage_bps),
);
panic_with_error!(env, SettlementError::SlippageExceeded);
}

if let Some(min_expected) = min_expected_amount {
if actual_amount < min_expected {
env.events().publish(
(soroban_sdk::symbol_short!("SlpSlipp"),),
(expected_amount, actual_amount, slippage_bps),
);
panic_with_error!(env, SettlementError::SlippageExceeded);
}
}

actual_amount
}
87 changes: 85 additions & 2 deletions contracts/settlement/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
#![no_std]

mod constants;
mod conversion;
mod fees;
mod rate_application;
mod token_utils;
mod types;

use soroban_sdk::{contract, contractimpl, contracterror, panic_with_error, Address, Env};
use soroban_sdk::{contract, contractclient, contracterror, contractimpl, panic_with_error, Address, Env};

#[cfg(test)]
mod test;

use crate::constants::MAX_FEE_RATE_BPS;
use crate::conversion::convert_to_settlement_currency;
use crate::fees::compute_fee;
use crate::token_utils::collect_fee;
use crate::types::{SettlementArgs, SettlementResult};

/// Cross-contract interface for the PriceOracle.
#[contractclient(name = "PriceOracleClient")]
pub trait PriceOracle {
fn get_price_value(env: Env) -> i128;
}

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum SettlementError {
InvalidFeeRate = 1,
InsufficientBalance = 2,
SlippageExceeded = 3,
}

#[contract]
Expand Down Expand Up @@ -61,7 +73,6 @@ impl SettlementContract {
let fee = collect_fee(&env, &token, &payer, &fee_collector, amount, rate_bps);
let net_amount = amount.saturating_sub(fee);

// Transfer net amount to payee
if net_amount > 0 {
let token_client = soroban_sdk::token::Client::new(&env, &token);
token_client.transfer(&payer, &payee, &net_amount);
Expand All @@ -74,4 +85,76 @@ impl SettlementContract {
pub fn calculate_fee(_env: Env, amount: i128, rate_bps: u32) -> i128 {
compute_fee(amount, rate_bps)
}

/// Finalize settlement with oracle-based currency conversion and slippage protection.
///
/// Converts resource token volume to settlement currency using the current
/// oracle exchange rate, with both protocol-enforced and user-defined slippage bounds.
/// Fee is deducted from the settlement amount before transfer.
///
/// # Arguments
/// * `env` - Contract environment
/// * `oracle` - Address of the price oracle contract
/// * `payer` - Address funding the settlement
/// * `fee_collector` - Address collecting the protocol fee
/// * `args` - Settlement parameters (token, volume, recipient, min_expected_amount)
/// * `rate_bps` - Fee rate in basis points
///
/// # Returns
/// SettlementResult containing net_amount, fee_amount, and rate_used
pub fn finalize_settlement(
env: Env,
oracle: Address,
payer: Address,
fee_collector: Address,
args: SettlementArgs,
rate_bps: u32,
) -> SettlementResult {
if rate_bps > MAX_FEE_RATE_BPS {
panic_with_error!(&env, SettlementError::InvalidFeeRate);
}

if args.volume <= 0 {
return SettlementResult {
net_amount: 0,
fee_amount: 0,
rate_used: 0,
};
}

payer.require_auth();

let rate = {
let oracle_client = PriceOracleClient::new(&env, &oracle);
oracle_client.get_price_value()
};

let settlement_amount = convert_to_settlement_currency(
&env,
&oracle,
args.volume,
args.min_expected_amount,
);

let fee = collect_fee(
&env,
&args.token_address,
&payer,
&fee_collector,
settlement_amount,
rate_bps,
);
let net_amount = settlement_amount.saturating_sub(fee);

if net_amount > 0 {
let token_client = soroban_sdk::token::Client::new(&env, &args.token_address);
token_client.transfer(&payer, &args.recipient, &net_amount);
}

SettlementResult {
net_amount,
fee_amount: fee,
rate_used: rate,
}
}
}
10 changes: 10 additions & 0 deletions contracts/settlement/src/rate_application.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use soroban_sdk::{Address, Env};

use crate::PriceOracleClient;

/// Fetch the current exchange rate from the price oracle.
/// Returns the rate as an i128 with 7 decimal places.
pub fn get_rate(env: &Env, oracle: &Address) -> i128 {
let client = PriceOracleClient::new(env, oracle);
client.get_price_value()
}
Loading
Loading