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
6 changes: 6 additions & 0 deletions contracts/invoice_liquidity/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub struct Config {
pub decay_period_ledgers: u64, // Ledger count between decay applications
pub dispute_timeout_ledgers: u64, // Ledger count after which a dispute can be auto-resolved
pub xlm_sac_address: Address, // Stellar Asset Contract address for native XLM wrapper
pub usdc_sac_address: Address, // USDC contract address
pub eurc_sac_address: Address, // EURC contract address
pub price_oracle: Option<Address>, // Optional price oracle for USD normalisation
}

Expand All @@ -34,6 +36,8 @@ pub fn update_config(
decay_period_ledgers: u64,
dispute_timeout_ledgers: u64,
xlm_sac_address: Address,
usdc_sac_address: Address,
eurc_sac_address: Address,
) -> Result<(), ConfigError> {
let admin = crate::storage::get_admin(env).ok_or(ConfigError::Unauthorized)?;
let old_config = crate::storage::get_config(env).ok_or(ConfigError::Unauthorized)?;
Expand All @@ -57,6 +61,8 @@ pub fn update_config(
decay_period_ledgers,
dispute_timeout_ledgers,
xlm_sac_address,
usdc_sac_address,
eurc_sac_address,
price_oracle: old_config.price_oracle,
};

Expand Down
9 changes: 9 additions & 0 deletions contracts/invoice_liquidity/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,12 @@ pub struct ReputationUpdated {
pub invoices_paid: u32,
pub invoices_defaulted: u32,
}

#[contractevent(topics = ["token_changed"])]
#[derive(Clone, Debug, PartialEq)]
pub struct InvoiceTokenChanged {
#[topic]
pub invoice_id: u64,
pub old_token: Address,
pub new_token: Address,
}
138 changes: 123 additions & 15 deletions contracts/invoice_liquidity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use events::{
AdminChanged, AppealResolved, ContractPaused, ContractUnpaused, ContractUpgraded,
DefaultAppealed, DisputeResolved, FundQueueResolved, FundRequested, InvoiceCancelled,
InvoiceDefaulted, InvoiceDisputed, InvoiceExpired, InvoiceFunded, InvoicePaid, InvoicePartiallyPaid,
InvoiceSubmitted, InvoiceTransferred, InvoiceUpdated, ParameterUpdated, TokenAdded,
InvoiceSubmitted, InvoiceTokenChanged, InvoiceTransferred, InvoiceUpdated, ParameterUpdated, TokenAdded,
TokenRemoved,
};
use invoice::{
Expand Down Expand Up @@ -92,7 +92,8 @@ impl InvoiceLiquidityContract {
pub fn initialize(
env: Env,
admin: Address,
token: Address,
usdc_token: Address,
eurc_token: Address,
xlm_token: Address,
) -> Result<(), ContractError> {
if env
Expand All @@ -119,7 +120,7 @@ impl InvoiceLiquidityContract {
.set(&StorageKey::NextInvoiceId, &1_u64);
}

// Initialize config with XLM SAC address
// Initialize config with token addresses
let initial_config = crate::config::Config {
high_rep_threshold: 70,
bonus_bps: 100,
Expand All @@ -128,13 +129,20 @@ impl InvoiceLiquidityContract {
decay_period_ledgers: 10000,
dispute_timeout_ledgers: 10000,
xlm_sac_address: xlm_token.clone(),
usdc_sac_address: usdc_token.clone(),
eurc_sac_address: eurc_token.clone(),
price_oracle: None,
};
crate::storage::set_config(&env, &initial_config);

// approve first token (USDC or default)
// approve initial tokens
env.storage().persistent().set(
&crate::storage::DataKey::ApprovedToken(token.clone()),
&crate::storage::DataKey::ApprovedToken(usdc_token.clone()),
&true,
);

env.storage().persistent().set(
&crate::storage::DataKey::ApprovedToken(eurc_token.clone()),
&true,
);

Expand All @@ -145,8 +153,9 @@ impl InvoiceLiquidityContract {
);

let mut list: Vec<Address> = Vec::new(&env);
list.push_back(token.clone());
list.push_back(xlm_token.clone());
list.push_back(usdc_token);
list.push_back(xlm_token);
list.push_back(eurc_token);

env.storage()
.persistent()
Expand Down Expand Up @@ -567,6 +576,64 @@ impl InvoiceLiquidityContract {
Ok(())
}

// ------------------------------------------------------------
// convert_invoice_token
// ------------------------------------------------------------
/// Access: Submitter only
pub fn convert_invoice_token(
env: Env,
freelancer: Address,
invoice_id: u64,
new_token: Address,
) -> Result<(), ContractError> {
if is_paused(&env) {
return Err(ContractError::ContractPaused);
}

if !invoice_exists(&env, invoice_id) {
return Err(ContractError::InvoiceNotFound);
}

let mut invoice = load_invoice(&env, invoice_id);
require_submitter_by_id(&env, &freelancer, invoice_id)?;

// Only allowed in Pending state
if invoice.status != InvoiceStatus::Pending {
match invoice.status {
InvoiceStatus::PartiallyFunded | InvoiceStatus::Funded => {
return Err(ContractError::AlreadyFunded)
}
InvoiceStatus::Paid => return Err(ContractError::AlreadyPaid),
_ => return Err(ContractError::Unauthorized), // Generic unauthorized for other states
}
}

// Check if invoice is expired (mirroring update_invoice logic)
if env.ledger().timestamp() >= u64::from(invoice.due_date) {
invoice.status = InvoiceStatus::Expired;
save_invoice(&env, &invoice);
return Err(ContractError::InvoiceExpired);
}

// New token must be in the allowlist
if !is_approved_token(&env, &new_token) {
return Err(ContractError::Unauthorized);
}

let old_token = invoice.token.clone();
invoice.token = new_token.clone();

save_invoice(&env, &invoice);

env.events().publish_event(&InvoiceTokenChanged {
invoice_id,
old_token,
new_token,
});

Ok(())
}

// ------------------------------------------------------------
// submit_invoices_batch
// ------------------------------------------------------------
Expand Down Expand Up @@ -826,9 +893,11 @@ impl InvoiceLiquidityContract {
let token = token_client(&env, &invoice.token);
let contract_address = env.current_contract_address();

// Handle XLM precision if needed (SAC wrapper handles conversion internally)
// Handle token precision if needed
let normalized_fund_amount = if is_xlm_token(&env, &invoice.token) {
normalize_xlm_amount(fund_amount)
} else if is_eurc_token(&env, &invoice.token) {
normalize_eurc_amount(fund_amount)
} else {
normalize_usdc_amount(fund_amount)
};
Expand Down Expand Up @@ -1122,9 +1191,11 @@ impl InvoiceLiquidityContract {
let token = token_client(&env, &invoice.token);
let contract_address = env.current_contract_address();

// Handle XLM precision if needed (SAC wrapper handles conversion internally)
// Handle token precision if needed
let normalized_amount = if is_xlm_token(&env, &invoice.token) {
normalize_xlm_amount(amount)
} else if is_eurc_token(&env, &invoice.token) {
normalize_eurc_amount(amount)
} else {
normalize_usdc_amount(amount)
};
Expand Down Expand Up @@ -1690,6 +1761,8 @@ impl InvoiceLiquidityContract {
decay_period_ledgers: u64,
dispute_timeout_ledgers: u64,
xlm_sac_address: Address,
usdc_sac_address: Address,
eurc_sac_address: Address,
) -> Result<(), ContractError> {
crate::config::update_config(
&env,
Expand All @@ -1701,6 +1774,8 @@ impl InvoiceLiquidityContract {
decay_period_ledgers,
dispute_timeout_ledgers,
xlm_sac_address,
usdc_sac_address,
eurc_sac_address,
)
.map_err(|_| ContractError::Unauthorized)
}
Expand Down Expand Up @@ -1830,19 +1905,38 @@ fn is_xlm_token(env: &Env, token: &Address) -> bool {
}

/// Convert amount from XLM precision (7 decimals) to contract precision
/// This is a no-op for now since we store amounts in their native token precision,
/// but provides a hook for future precision normalization if needed
fn normalize_xlm_amount(amount: i128) -> i128 {
amount
}

/// Check if a token address is the USDC address
fn is_usdc_token(env: &Env, token: &Address) -> bool {
if let Some(config) = crate::storage::get_config(env) {
token == &config.usdc_sac_address
} else {
false
}
}

/// Convert amount from USDC precision (6 decimals) to contract precision
/// This is a no-op for now since we store amounts in their native token precision,
/// but provides a hook for future precision normalization if needed
fn normalize_usdc_amount(amount: i128) -> i128 {
amount
}

/// Check if a token address is the EURC address
fn is_eurc_token(env: &Env, token: &Address) -> bool {
if let Some(config) = crate::storage::get_config(env) {
token == &config.eurc_sac_address
} else {
false
}
}

/// Convert amount from EURC precision (6 decimals) to contract precision
fn normalize_eurc_amount(amount: i128) -> i128 {
amount
}

fn validate_invoice_terms(
env: &Env,
amount: i128,
Expand Down Expand Up @@ -1886,10 +1980,22 @@ fn validate_invoice_terms(
}

fn is_approved_token(env: &Env, token: &Address) -> bool {
env.storage()
// First check the explicit allowlist in storage
if env.storage()
.persistent()
.get(&crate::storage::DataKey::ApprovedToken(token.clone()))
.unwrap_or(false)
.unwrap_or(false) {
return true;
}

// Then check the wired tokens in Config
if let Some(config) = crate::storage::get_config(env) {
if token == &config.usdc_sac_address || token == &config.eurc_sac_address || token == &config.xlm_sac_address {
return true;
}
}

false
}

fn notify_distribution_funding(env: &Env, lp: &Address, amount_usdc_equivalent: i128) {
Expand Down Expand Up @@ -1970,3 +2076,5 @@ mod tests_lazy_storage;
mod tests_reputation_events;
#[cfg(test)]
mod tests_discount_invariants;
#[cfg(test)]
mod tests_token_switch;
8 changes: 6 additions & 2 deletions contracts/invoice_liquidity/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub fn setup() -> TestEnv {
let usdc_contract_id = env.register_stellar_asset_contract_v2(usdc_admin.clone());
let usdc_address = usdc_contract_id.address();

let eurc_admin = Address::generate(&env);
let eurc_contract_id = env.register_stellar_asset_contract_v2(eurc_admin.clone());
let eurc_address = eurc_contract_id.address();

let token = TokenClient::new(&env, &usdc_address);
let token_admin = StellarAssetClient::new(&env, &usdc_address);

Expand All @@ -61,8 +65,8 @@ pub fn setup() -> TestEnv {
let xlm_contract_id = env.register_stellar_asset_contract_v2(xlm_admin);
let xlm_address = xlm_contract_id.address();

// Initialize with mock USDC and mock XLM SAC addresses
contract.initialize(&usdc_admin, &usdc_address, &xlm_address);
// Initialize with mock USDC, EURC and mock XLM SAC addresses
contract.initialize(&usdc_admin, &usdc_address, &eurc_address, &xlm_address);

// ---- Set ledger timestamp to a known baseline ----
let mut ledger_info = env.ledger().get();
Expand Down
3 changes: 2 additions & 1 deletion contracts/invoice_liquidity/src/tests_appeal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ fn setup_appeal() -> AppealTestEnv {
let xlm_addr = xlm_id.address();

// usdc_admin acts as the contract admin.
contract.initialize(&usdc_admin, &usdc_addr, &xlm_addr);
let eurc_addr = Address::generate(&env);
contract.initialize(&usdc_admin, &usdc_addr, &eurc_addr, &xlm_addr);

let mut ledger = env.ledger().get();
ledger.timestamp = 1_700_000_000;
Expand Down
3 changes: 2 additions & 1 deletion contracts/invoice_liquidity/src/tests_benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ fn setup_benchmark_env() -> BaseBenchEnv {

let contract_id = env.register(InvoiceLiquidityContract, ());
let contract = InvoiceLiquidityContractClient::new(&env, &contract_id);
contract.initialize(&usdc_admin, &usdc.address(), &xlm.address());
let eurc_address = Address::generate(&env);
contract.initialize(&usdc_admin, &usdc.address(), &eurc_address, &xlm.address());

let freelancer = Address::generate(&env);
let payer = Address::generate(&env);
Expand Down
3 changes: 2 additions & 1 deletion contracts/invoice_liquidity/src/tests_concurrency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ fn setup_env() -> ConcurrencyEnv {

let contract_id = env.register(InvoiceLiquidityContract, ());
let contract = InvoiceLiquidityContractClient::new(&env, &contract_id);
contract.initialize(&usdc_admin, &usdc.address(), &xlm.address());
let eurc_addr = Address::generate(&env);
contract.initialize(&usdc_admin, &usdc.address(), &eurc_addr, &xlm.address());

let freelancer = Address::generate(&env);
let payer = Address::generate(&env);
Expand Down
3 changes: 2 additions & 1 deletion contracts/invoice_liquidity/src/tests_discount_invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ fn setup_invariant(invoice_amount: i128) -> InvariantEnv {

let contract_id = env.register(InvoiceLiquidityContract, ());
let contract = InvoiceLiquidityContractClient::new(&env, &contract_id);
contract.initialize(&usdc_admin, &usdc_address, &xlm_address);
let eurc_address = Address::generate(&env);
contract.initialize(&usdc_admin, &usdc_address, &eurc_address, &xlm_address);

let mut ledger = env.ledger().get();
ledger.timestamp = LEDGER_TIMESTAMP;
Expand Down
3 changes: 2 additions & 1 deletion contracts/invoice_liquidity/src/tests_dispute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ fn setup_dispute() -> DisputeTestEnv {
let xlm_addr = xlm_id.address();

// usdc_admin acts as the contract admin.
contract.initialize(&usdc_admin, &usdc_addr, &xlm_addr);
let eurc_addr = Address::generate(&env);
contract.initialize(&usdc_admin, &usdc_addr, &eurc_addr, &xlm_addr);

let mut ledger = env.ledger().get();
ledger.timestamp = 1_700_000_000;
Expand Down
3 changes: 2 additions & 1 deletion contracts/invoice_liquidity/src/tests_fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ fn setup_fuzz() -> FuzzEnv {
let xlm_contract_id = env.register_stellar_asset_contract_v2(xlm_admin);
let xlm_address = xlm_contract_id.address();

contract.initialize(&usdc_admin, &usdc_address, &xlm_address);
let eurc_address = Address::generate(&env);
contract.initialize(&usdc_admin, &usdc_address, &eurc_address, &xlm_address);

// Fix ledger timestamp to a known baseline
let mut ledger_info = env.ledger().get();
Expand Down
3 changes: 2 additions & 1 deletion contracts/invoice_liquidity/src/tests_governance_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ fn setup() -> TestEnv {

let contract_id = env.register(InvoiceLiquidityContract, ());
let contract = InvoiceLiquidityContractClient::new(&env, &contract_id);
contract.initialize(&admin, &usdc.address, &xlm.address);
let eurc_address = Address::generate(&env);
contract.initialize(&admin, &usdc.address, &eurc_address, &xlm.address);

let mut ledger_info = env.ledger().get();
ledger_info.timestamp = 1_700_000_000;
Expand Down
3 changes: 2 additions & 1 deletion contracts/invoice_liquidity/src/tests_invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ fn setup() -> TestEnv {
let xlm_id = env.register_stellar_asset_contract_v2(xlm_admin);
let xlm_address = xlm_id.address();

contract.initialize(&admin, &usdc_address, &xlm_address);
let eurc_address = Address::generate(&env);
contract.initialize(&admin, &usdc_address, &eurc_address, &xlm_address);

let mut info = env.ledger().get();
info.timestamp = 1_700_000_000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ fn setup() -> LifecycleTestEnv {

let contract_id = env.register(InvoiceLiquidityContract, ());
let contract = InvoiceLiquidityContractClient::new(&env, &contract_id);
contract.initialize(&admin, &token.address, &xlm.address);
let eurc_address = Address::generate(&env);
contract.initialize(&admin, &token.address, &eurc_address, &xlm.address);

let mut ledger_info = env.ledger().get();
ledger_info.timestamp = 1_700_000_000;
Expand Down
Loading