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
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"contracts/auction",
"contracts/tests",
"benchmarks",
"contracts/tranche",
]
resolver = "2"

Expand Down
18 changes: 18 additions & 0 deletions contracts/tranche/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "tranche"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
proptest = "1.4"

[features]
testutils = []
78 changes: 78 additions & 0 deletions contracts/tranche/src/deposit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use soroban_sdk::{panic_with_error, Address, Env};

use crate::{
errors::TrancheError,
events::{DEPOSIT, EVT},
state::{
DataKey, InvestorPosition, TrancheAccounting, TrancheClass, TranchePool,
},
};

pub fn deposit(
env: &Env,
investor: Address,
token: Address,
tranche: TrancheClass,
amount: i128,
) {
if amount <= 0 {
panic_with_error!(env, TrancheError::InvalidAmount);
}

investor.require_auth();

let mut pool: TranchePool = env
.storage()
.instance()
.get(&DataKey::Pool(token.clone()))
.unwrap_or_else(|| panic_with_error!(env, TrancheError::PoolNotFound));

let key = DataKey::Investor(
investor.clone(),
token.clone(),
tranche,
);

let mut position: InvestorPosition = env
.storage()
.instance()
.get(&key)
.unwrap_or_default();

match tranche {
TrancheClass::Senior => {
// Enforce senior advance rate: seniors can't exceed their configured
// percentage of total pool value
let new_total_deposited = pool.junior.deposited + pool.senior.deposited + amount;
let senior_target = pool.config.senior_advance_rate_bps as i128 * new_total_deposited / 10_000;
if pool.senior.deposited + amount > senior_target {
panic_with_error!(env, TrancheError::AdvanceRateExceeded);
}
update_accounting(&mut pool.senior, amount);
}
TrancheClass::Junior => {
update_accounting(&mut pool.junior, amount);
}
}

position.deposited += amount;
position.shares += amount;

env.storage().instance().set(&key, &position);
env.storage()
.instance()
.set(&DataKey::Pool(token.clone()), &pool);

env.events().publish(
(EVT, DEPOSIT),
(investor, token, tranche, amount),
);
}

fn update_accounting(
accounting: &mut TrancheAccounting,
amount: i128,
) {
accounting.deposited += amount;
accounting.available += amount;
}
18 changes: 18 additions & 0 deletions contracts/tranche/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Eq, PartialEq)]
#[repr(u32)]
pub enum TrancheError {
AlreadyInitialized = 1,
Unauthorized = 2,
PoolNotFound = 3,
InvalidAmount = 4,
InsufficientBalance = 5,
AdvanceRateExceeded = 6,
InvalidTranche = 7,
ArithmeticOverflow = 8,
WaterfallError = 9,
LossAllocationError = 10,
ReentrancyDetected = 11,
}
10 changes: 10 additions & 0 deletions contracts/tranche/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use soroban_sdk::{symbol_short, Symbol};

pub const EVT: Symbol = symbol_short!("tranche");

pub const DEPOSIT: Symbol = symbol_short!("deposit");
pub const WITHDRAW: Symbol = symbol_short!("withdraw");
pub const FUND: Symbol = symbol_short!("fund");
pub const REPAY: Symbol = symbol_short!("repay");
pub const DEFAULT: Symbol = symbol_short!("default");
pub const CONFIG: Symbol = symbol_short!("config");
75 changes: 75 additions & 0 deletions contracts/tranche/src/funding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use soroban_sdk::{contracttype, panic_with_error, Address, Env};

use crate::{
errors::TrancheError,
events::{EVT, FUND},
state::{DataKey, InvoiceTrancheExposure, TranchePool},
};

#[contracttype]
#[derive(Clone)]
pub struct TrancheFundingSplit {
pub senior_amount: i128,
pub junior_amount: i128,
}

pub fn fund_invoice_from_tranches(
env: &Env,
token: Address,
invoice_id: u64,
total_amount: i128,
) -> TrancheFundingSplit {
let mut pool: TranchePool = env
.storage()
.instance()
.get(&DataKey::Pool(token.clone()))
.unwrap_or_else(|| panic_with_error!(env, TrancheError::PoolNotFound));

// Calculate proportional split based on senior advance rate
let senior_amount = total_amount * pool.config.senior_advance_rate_bps as i128 / 10_000;
let junior_amount = total_amount - senior_amount;

// Check availability
if pool.senior.available < senior_amount {
panic_with_error!(env, TrancheError::InsufficientBalance);
}
if pool.junior.available < junior_amount {
panic_with_error!(env, TrancheError::InsufficientBalance);
}

// Update accounting
pool.senior.available -= senior_amount;
pool.senior.deployed += senior_amount;
pool.junior.available -= junior_amount;
pool.junior.deployed += junior_amount;

// Record tranche exposure for this invoice
let exposure = InvoiceTrancheExposure {
invoice_id,
senior_deployed: senior_amount,
junior_deployed: junior_amount,
};
env.storage()
.instance()
.set(&DataKey::InvoiceExposure(invoice_id), &exposure);

env.storage()
.instance()
.set(&DataKey::Pool(token.clone()), &pool);

env.events().publish(
(EVT, FUND),
(invoice_id, token, senior_amount, junior_amount),
);

TrancheFundingSplit {
senior_amount,
junior_amount,
}
}

pub fn get_invoice_exposure(env: &Env, invoice_id: u64) -> Option<InvoiceTrancheExposure> {
env.storage()
.instance()
.get(&DataKey::InvoiceExposure(invoice_id))
}
Loading
Loading