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
1 change: 1 addition & 0 deletions COMEBACKHERE-contracts/Cargo.lock

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

92 changes: 69 additions & 23 deletions COMEBACKHERE-contracts/contracts/invoice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ const MAX_REFERENCE_LEN: u32 = 64;
/// Minimum invoice amount, in stroops (10,000,000 stroops == 1 USDC given 7 decimals).
const MIN_AMOUNT_USDC: i128 = 10_000_000;

// At five seconds per ledger, these keep state alive for roughly 335 days before
// renewal and extend it to roughly 359 days after an access or mutation.
const INSTANCE_TTL_THRESHOLD: u32 = 5_800_000;
const INSTANCE_TTL_EXTEND_TO: u32 = 6_200_000;
const INVOICE_TTL_THRESHOLD: u32 = 5_800_000;
const INVOICE_TTL_EXTEND_TO: u32 = 6_200_000;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ContractError {
Expand Down Expand Up @@ -94,10 +101,32 @@ fn check_not_paused(env: &Env) -> Result<(), ContractError> {
if is_paused(env) {
Err(ContractError::ContractPaused)
} else {
extend_instance_ttl(env);
Ok(())
}
}

fn extend_instance_ttl(env: &Env) {
env.storage()
.instance()
.extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO);
}

fn extend_invoice_ttl(env: &Env, invoice_id: u64) {
env.storage().persistent().extend_ttl(
&DataKey::Invoice(invoice_id),
INVOICE_TTL_THRESHOLD,
INVOICE_TTL_EXTEND_TO,
);
}

fn store_invoice(env: &Env, invoice_id: u64, invoice: &Invoice) {
env.storage()
.persistent()
.set(&DataKey::Invoice(invoice_id), invoice);
extend_invoice_ttl(env, invoice_id);
}

fn check_admin(env: &Env, addr: &Address) -> Result<(), ContractError> {
if addr != &admin(env) {
Err(ContractError::Unauthorized)
Expand Down Expand Up @@ -139,6 +168,7 @@ impl InvoiceContract {
.persistent()
.set(&DataKey::InvoiceCount, &0u64);
env.storage().persistent().set(&DataKey::Paused, &false);
extend_instance_ttl(&env);
Ok(())
}

Expand Down Expand Up @@ -222,9 +252,7 @@ impl InvoiceContract {
expires_at,
reference,
};
env.storage()
.persistent()
.set(&DataKey::Invoice(count), &invoice);
store_invoice(&env, count, &invoice);

events::invoice_created(&env, &merchant, &count);
Ok(count)
Expand All @@ -238,10 +266,13 @@ impl InvoiceContract {
/// # Errors
/// - [`ContractError::InvoiceNotFound`] if no invoice with that ID exists.
pub fn get_invoice(env: Env, invoice_id: u64) -> Result<Invoice, ContractError> {
env.storage()
let invoice = env
.storage()
.persistent()
.get(&DataKey::Invoice(invoice_id))
.ok_or(ContractError::InvoiceNotFound)
.ok_or(ContractError::InvoiceNotFound)?;
extend_invoice_ttl(&env, invoice_id);
Ok(invoice)
}

/// Returns only the [`InvoiceStatus`] for a given invoice ID, without fetching
Expand All @@ -258,6 +289,7 @@ impl InvoiceContract {
.persistent()
.get::<DataKey, Invoice>(&DataKey::Invoice(invoice_id))
.ok_or(ContractError::InvoiceNotFound)?;
extend_invoice_ttl(&env, invoice_id);
Ok(invoice.status)
}

Expand Down Expand Up @@ -393,9 +425,7 @@ impl InvoiceContract {
}

invoice.status = InvoiceStatus::Paid;
env.storage()
.persistent()
.set(&DataKey::Invoice(id), &invoice);
store_invoice(&env, id, &invoice);
events::invoice_paid(&env, &id);
}
Ok(())
Expand Down Expand Up @@ -431,9 +461,7 @@ impl InvoiceContract {
// No funds have moved yet — simple cancellation.
InvoiceStatus::Pending => {
invoice.status = InvoiceStatus::Cancelled;
env.storage()
.persistent()
.set(&DataKey::Invoice(invoice_id), &invoice);
store_invoice(&env, invoice_id, &invoice);
events::invoice_cancelled(&env, &invoice_id);
Ok(())
}
Expand All @@ -442,9 +470,7 @@ impl InvoiceContract {
// complete the refund without leaving funds stuck.
InvoiceStatus::Paid => {
invoice.status = InvoiceStatus::RefundRequested;
env.storage()
.persistent()
.set(&DataKey::Invoice(invoice_id), &invoice);
store_invoice(&env, invoice_id, &invoice);
events::invoice_refund_req(&env, &invoice_id);
Ok(())
}
Expand Down Expand Up @@ -497,9 +523,7 @@ impl InvoiceContract {
return Err(ContractError::AlreadyRefundRequested);
}
invoice.status = InvoiceStatus::RefundRequested;
env.storage()
.persistent()
.set(&DataKey::Invoice(invoice_id), &invoice);
store_invoice(&env, invoice_id, &invoice);
events::invoice_refund_req(&env, &invoice_id);
Ok(())
}
Expand Down Expand Up @@ -555,9 +579,7 @@ impl InvoiceContract {
return Err(ContractError::GraceWindowNotExpired);
}
invoice.status = InvoiceStatus::Released;
env.storage()
.persistent()
.set(&DataKey::Invoice(invoice_id), &invoice);
store_invoice(&env, invoice_id, &invoice);
events::escrow_released(&env, &invoice_id);
Ok(())
}
Expand Down Expand Up @@ -587,9 +609,7 @@ impl InvoiceContract {
.ok_or(ContractError::InvoiceNotFound)?;
if invoice.status == InvoiceStatus::Pending && now >= invoice.expires_at {
invoice.status = InvoiceStatus::Expired;
env.storage()
.persistent()
.set(&DataKey::Invoice(id), &invoice);
store_invoice(&env, id, &invoice);
events::invoice_expired(&env, &id);
}
}
Expand Down Expand Up @@ -698,6 +718,7 @@ impl InvoiceContract {
pub fn pause(env: Env, caller: Address) -> Result<(), ContractError> {
check_admin(&env, &caller)?;
env.storage().persistent().set(&DataKey::Paused, &true);
extend_instance_ttl(&env);
events::contract_paused(&env);
Ok(())
}
Expand All @@ -715,6 +736,7 @@ impl InvoiceContract {
pub fn unpause(env: Env, caller: Address) -> Result<(), ContractError> {
check_admin(&env, &caller)?;
env.storage().persistent().set(&DataKey::Paused, &false);
extend_instance_ttl(&env);
events::contract_unpaused(&env);
Ok(())
}
Expand Down Expand Up @@ -778,6 +800,30 @@ mod tests {
assert_eq!(invoice_id, 1);
}

#[test]
fn test_invoice_storage_remains_readable_after_many_ledgers() {
let (env, cid, _admin) = setup_contract(1000);
let client = InvoiceContractClient::new(&env, &cid);
let merchant = Address::generate(&env);
let customer = Address::generate(&env);
let token = Address::generate(&env);
let invoice_id = client.create_invoice(
&merchant,
&customer,
&10_000_000i128,
&token,
&5000,
&1,
&None,
);

env.ledger().with_mut(|li| {
li.sequence_number = INVOICE_TTL_EXTEND_TO - INVOICE_TTL_THRESHOLD + 1;
});
assert_eq!(client.get_invoice(&invoice_id).id, invoice_id);
assert_eq!(client.get_invoice_status(&invoice_id), InvoiceStatus::Pending);
}

#[test]
fn test_create_invoice_with_duplicate_nonce_returns_error() {
let (env, cid, _admin) = setup_contract(1000);
Expand Down
1 change: 1 addition & 0 deletions COMEBACKHERE-contracts/contracts/treasury/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ soroban-sdk = "28.0.0"
[dev-dependencies]
soroban-sdk = { version = "28.0.0", features = ["testutils"] }
proptest = "1.4"
serde_json = "1"

[profile.release]
opt-level = "z"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"test": "bench_approve_settlement",
"baseline_status": "provisional_budget_not_measured",
"cpu_instructions": 100000,
"memory_bytes": 50000,
"tolerance_percent": 20,
"timestamp": "2026-09-26",
"soroban_sdk": "28.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"test": "bench_execute_settlement",
"baseline_status": "provisional_budget_not_measured",
"cpu_instructions": 100000,
"memory_bytes": 50000,
"tolerance_percent": 20,
"timestamp": "2026-09-26",
"soroban_sdk": "28.0.0"
}
74 changes: 74 additions & 0 deletions COMEBACKHERE-contracts/contracts/treasury/src/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
use super::*;
use soroban_sdk::{testutils::Address as _, vec, Address, Env};

const APPROVE_BASELINE: &str = include_str!("../resources/approve_settlement_baseline.json");
const EXECUTE_BASELINE: &str = include_str!("../resources/execute_settlement_baseline.json");

fn assert_cost_within_baseline(baseline_json: &str, cpu: u64, memory: u64) {
let baseline: serde_json::Value =
serde_json::from_str(baseline_json).expect("benchmark baseline must be valid JSON");
let tolerance_percent = baseline["tolerance_percent"]
.as_u64()
.expect("benchmark baseline must define tolerance_percent");
let cpu_limit = baseline["cpu_instructions"]
.as_u64()
.expect("benchmark baseline must define cpu_instructions")
.saturating_mul(100 + tolerance_percent)
/ 100;
let memory_limit = baseline["memory_bytes"]
.as_u64()
.expect("benchmark baseline must define memory_bytes")
.saturating_mul(100 + tolerance_percent)
/ 100;

assert!(cpu <= cpu_limit, "CPU instructions ({cpu}) exceeded baseline tolerance ({cpu_limit})");
assert!(
memory <= memory_limit,
"Memory bytes ({memory}) exceeded baseline tolerance ({memory_limit})"
);
}

fn setup_bench_env() -> (Env, Address) {
let e = Env::default();
e.mock_all_auths();
Expand Down Expand Up @@ -117,3 +144,50 @@ fn bench_propose_settlement_rejected_token_cheaper_than_accepted() {
accepted proposal ({cpu_accept} cpu) now that the allowlist check runs first"
);
}

#[test]
fn bench_approve_settlement() {
let e = Env::default();
e.mock_all_auths();
let contract_id = e.register(TreasuryContract, ());
let client = TreasuryContractClient::new(&e, &contract_id);
let admin = Address::generate(&e);
let signer = Address::generate(&e);
let token = Address::generate(&e);
let merchant = Address::generate(&e);
client.initialize(&vec![&e, (signer.clone(), 1u64)], &1, &admin);
let settlement_id = client.propose_settlement(&signer, &token, &5_000_000u64, &merchant);

e.budget().reset_unlimited();
let cpu_before = e.budget().cpu_instruction_cost();
let memory_before = e.budget().memory_bytes_cost();
client.approve_settlement(&signer, &settlement_id);
let cpu = e.budget().cpu_instruction_cost() - cpu_before;
let memory = e.budget().memory_bytes_cost() - memory_before;

assert_cost_within_baseline(APPROVE_BASELINE, cpu, memory);
}

#[test]
fn bench_execute_settlement() {
let e = Env::default();
e.mock_all_auths();
let contract_id = e.register(TreasuryContract, ());
let client = TreasuryContractClient::new(&e, &contract_id);
let admin = Address::generate(&e);
let signer = Address::generate(&e);
let token = Address::generate(&e);
let merchant = Address::generate(&e);
client.initialize(&vec![&e, (signer.clone(), 1u64)], &1, &admin);
let settlement_id = client.propose_settlement(&signer, &token, &5_000_000u64, &merchant);
client.approve_settlement(&signer, &settlement_id);

e.budget().reset_unlimited();
let cpu_before = e.budget().cpu_instruction_cost();
let memory_before = e.budget().memory_bytes_cost();
client.execute_settlement(&signer, &settlement_id, &token);
let cpu = e.budget().cpu_instruction_cost() - cpu_before;
let memory = e.budget().memory_bytes_cost() - memory_before;

assert_cost_within_baseline(EXECUTE_BASELINE, cpu, memory);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![cfg(test)]

use super::*;
use soroban_sdk::{testutils::Address as _, vec, Address, Env};
use soroban_sdk::{testutils::{Address as _, Ledger}, vec, Address, Env};

fn setup_env() -> (Env, Address) {
let env = Env::default();
Expand Down Expand Up @@ -161,6 +161,34 @@ fn test_resolve_dispute_callable_by_signer() {
client.resolve_dispute(&signer_b, &settlement_id, &true);
}

#[test]
fn test_dispute_remains_resolvable_after_many_ledgers() {
let (env, contract_id) = setup_env();
let client = make_client(&env, &contract_id);

let admin = Address::generate(&env);
let signer_a = Address::generate(&env);
let signer_b = Address::generate(&env);
let token = Address::generate(&env);
let merchant = Address::generate(&env);
let signers = vec![&env, (signer_a.clone(), 1u64), (signer_b.clone(), 1u64)];
client.initialize(&signers, &2u64, &admin);

let settlement_id = client.propose_settlement(&signer_a, &token, &5_000_000u64, &merchant);
client.raise_dispute(&merchant, &settlement_id, &1u32);

env.ledger().with_mut(|li| {
li.sequence_number = INSTANCE_TTL_EXTEND_TO - INSTANCE_TTL_THRESHOLD + 1;
});
assert_eq!(
client.get_settlement(&settlement_id).unwrap().status,
SettlementStatus::OnHold
);

client.resolve_dispute(&signer_a, &settlement_id, &true);
assert!(client.get_settlement(&settlement_id).is_some());
}

#[test]
fn test_multiple_disputes_independent() {
let (env, contract_id) = setup_env();
Expand Down
Loading