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 context/progress-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ Update this file after every completed contract change, fix, or architectural de
- `test_repay_installment_zero_amount_rejected`: asserts `InvalidRepaymentAmount` (#13) for zero payment
- Total tests: 98 (93 existing + 5 new) — all passing

### Issue #6 — Typed Storage Errors
- Removed all `.expect(...)` and bare `.unwrap()` matches from `contracts/*/src/storage.rs`
- Converted storage getters/readers to typed `Result<T, ContractError>` paths while preserving intentional zero/false/default semantics
- Added TTL extension after persistent writes for creditline user indexes/active debt, liquidity-pool LP shares, and vendor-registry vendor/count records
- Added missing `NotInitialized` variants to creditline, parameters, and reputation errors without renumbering existing variants
- Added before-initialize regression coverage across all 5 active contracts using generated `try_*` clients
- Verified with `cargo check --offline`, `cargo build --offline`, `cargo test --offline`, and `cargo clippy --offline -- -D warnings` — 230 passed, 0 failed, 4 ignored

---

## In Progress
Expand Down
2 changes: 1 addition & 1 deletion contracts/creditline-contract/src/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::storage;

/// Require that the given address is the admin, otherwise panic with NotAdmin error
pub fn require_admin(env: &Env, caller: &Address) {
let admin = storage::get_admin(env);
let admin = storage::get_admin(env).unwrap_or_else(|err| panic_with_error!(env, err));

if caller != &admin {
panic_with_error!(env, CreditLineError::NotAdmin);
Expand Down
1 change: 1 addition & 0 deletions contracts/creditline-contract/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ pub enum CreditLineError {
InvalidInstallmentIndex = 23,
InstallmentAlreadyPaid = 24,
InvalidLoanStatus = 25,
NotInitialized = 26,
}
122 changes: 78 additions & 44 deletions contracts/creditline-contract/src/lib.rs

Large diffs are not rendered by default.

139 changes: 81 additions & 58 deletions contracts/creditline-contract/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol, Vec};

use crate::errors::CreditLineError;
use crate::types::Loan;

// Storage keys
Expand All @@ -24,11 +25,11 @@ enum DataKey {
}

/// Get the admin address from storage
pub fn get_admin(env: &Env) -> Address {
pub fn get_admin(env: &Env) -> Result<Address, CreditLineError> {
env.storage()
.instance()
.get(&ADMIN_KEY)
.unwrap_or_else(|| panic!("Admin not set"))
.ok_or(CreditLineError::NotInitialized)
}

/// Set the admin address in storage
Expand All @@ -37,24 +38,25 @@ pub fn set_admin(env: &Env, admin: &Address) {
}

/// Get the current loan counter (for generating unique loan IDs)
pub fn get_loan_counter(env: &Env) -> u64 {
env.storage().instance().get(&LOAN_COUNTER).unwrap_or(0)
pub fn get_loan_counter(env: &Env) -> Result<u64, CreditLineError> {
Ok(env.storage().instance().get(&LOAN_COUNTER).unwrap_or(0))
}

/// Increment and return the next loan ID
pub fn increment_loan_counter(env: &Env) -> u64 {
let current = get_loan_counter(env);
let next = current.checked_add(1).expect("Loan counter overflow");
pub fn increment_loan_counter(env: &Env) -> Result<u64, CreditLineError> {
let current = get_loan_counter(env)?;
let next = current.checked_add(1).ok_or(CreditLineError::Overflow)?;
env.storage().instance().set(&LOAN_COUNTER, &next);
next
Ok(next)
}

/// Read a loan from storage
pub fn read_loan(env: &Env, loan_id: u64) -> Option<Loan> {
pub fn read_loan(env: &Env, loan_id: u64) -> Result<Loan, CreditLineError> {
let shard = loan_shard(loan_id);
env.storage()
.persistent()
.get(&DataKey::Loan(shard, loan_id))
.ok_or(CreditLineError::LoanNotFound)
}

/// Write a loan to storage
Expand All @@ -70,24 +72,25 @@ pub fn write_loan(env: &Env, loan: &Loan) {
}
}

pub fn get_user_loan_count(env: &Env, borrower: &Address) -> u64 {
env.storage()
pub fn get_user_loan_count(env: &Env, borrower: &Address) -> Result<u64, CreditLineError> {
Ok(env
.storage()
.persistent()
.get(&DataKey::UserLoanCount(borrower.clone()))
.unwrap_or(0)
.unwrap_or(0))
}

pub fn get_user_loan_ids_paginated(
env: &Env,
borrower: &Address,
start: u64,
limit: u32,
) -> Vec<u64> {
let total = get_user_loan_count(env, borrower);
) -> Result<Vec<u64>, CreditLineError> {
let total = get_user_loan_count(env, borrower)?;
let mut result = Vec::new(env);

if limit == 0 || start >= total {
return result;
return Ok(result);
}

let end = start.saturating_add(limit as u64).min(total);
Expand All @@ -100,71 +103,85 @@ pub fn get_user_loan_ids_paginated(
idx += 1;
}

result
Ok(result)
}

pub fn get_user_loans_paginated(
env: &Env,
borrower: &Address,
start: u64,
limit: u32,
) -> Vec<Loan> {
let loan_ids = get_user_loan_ids_paginated(env, borrower, start, limit);
) -> Result<Vec<Loan>, CreditLineError> {
let loan_ids = get_user_loan_ids_paginated(env, borrower, start, limit)?;
let mut loans = Vec::new(env);

for loan_id in loan_ids.iter() {
if let Some(loan) = read_loan(env, loan_id) {
loans.push_back(loan);
}
loans.push_back(read_loan(env, loan_id)?);
}

loans
Ok(loans)
}

pub fn get_user_active_debt(env: &Env, borrower: &Address) -> i128 {
env.storage()
pub fn get_user_active_debt(env: &Env, borrower: &Address) -> Result<i128, CreditLineError> {
Ok(env
.storage()
.persistent()
.get(&DataKey::UserActiveDebt(borrower.clone()))
.unwrap_or(0)
.unwrap_or(0))
}

pub fn increase_user_active_debt(env: &Env, borrower: &Address, amount: i128) {
let current = get_user_active_debt(env, borrower);
pub fn increase_user_active_debt(
env: &Env,
borrower: &Address,
amount: i128,
) -> Result<(), CreditLineError> {
let current = get_user_active_debt(env, borrower)?;
let next = current
.checked_add(amount)
.expect("User active debt overflow");
env.storage()
.persistent()
.set(&DataKey::UserActiveDebt(borrower.clone()), &next);
.ok_or(CreditLineError::Overflow)?;
let key = DataKey::UserActiveDebt(borrower.clone());
env.storage().persistent().set(&key, &next);
extend_persistent_ttl(env, &key);
Ok(())
}

pub fn decrease_user_active_debt(env: &Env, borrower: &Address, amount: i128) {
let current = get_user_active_debt(env, borrower);
pub fn decrease_user_active_debt(
env: &Env,
borrower: &Address,
amount: i128,
) -> Result<(), CreditLineError> {
let current = get_user_active_debt(env, borrower)?;
let next = current
.checked_sub(amount)
.expect("User active debt underflow");
env.storage()
.persistent()
.set(&DataKey::UserActiveDebt(borrower.clone()), &next);
.ok_or(CreditLineError::Underflow)?;
let key = DataKey::UserActiveDebt(borrower.clone());
env.storage().persistent().set(&key, &next);
extend_persistent_ttl(env, &key);
Ok(())
}

fn append_user_loan_index(env: &Env, borrower: &Address, loan_id: u64) {
let count = get_user_loan_count(env, borrower);
env.storage()
.persistent()
.set(&DataKey::UserLoanAt(borrower.clone(), count), &loan_id);
env.storage()
.persistent()
.set(&DataKey::UserLoanCount(borrower.clone()), &(count + 1));
let count = get_user_loan_count(env, borrower)
.unwrap_or_else(|err| soroban_sdk::panic_with_error!(env, err));
let loan_at_key = DataKey::UserLoanAt(borrower.clone(), count);
env.storage().persistent().set(&loan_at_key, &loan_id);
extend_persistent_ttl(env, &loan_at_key);

let count_key = DataKey::UserLoanCount(borrower.clone());
let next_count = count
.checked_add(1)
.unwrap_or_else(|| soroban_sdk::panic_with_error!(env, CreditLineError::Overflow));
env.storage().persistent().set(&count_key, &next_count);
extend_persistent_ttl(env, &count_key);
}

fn loan_shard(loan_id: u64) -> u32 {
(loan_id % (LOAN_SHARD_COUNT as u64)) as u32
}

/// Get the Reputation Contract address
pub fn get_reputation_contract(env: &Env) -> Option<Address> {
env.storage().instance().get(&REPUTATION_CONTRACT)
pub fn get_reputation_contract(env: &Env) -> Result<Option<Address>, CreditLineError> {
Ok(env.storage().instance().get(&REPUTATION_CONTRACT))
}

/// Set the Reputation Contract address
Expand All @@ -173,8 +190,8 @@ pub fn set_reputation_contract(env: &Env, address: &Address) {
}

/// Get the Vendor Registry Contract address
pub fn get_vendor_registry(env: &Env) -> Option<Address> {
env.storage().instance().get(&MERCHANT_REGISTRY)
pub fn get_vendor_registry(env: &Env) -> Result<Option<Address>, CreditLineError> {
Ok(env.storage().instance().get(&MERCHANT_REGISTRY))
}

/// Set the Vendor Registry Contract address
Expand All @@ -183,8 +200,8 @@ pub fn set_vendor_registry(env: &Env, address: &Address) {
}

/// Get the Liquidity Pool Contract address
pub fn get_liquidity_pool(env: &Env) -> Option<Address> {
env.storage().instance().get(&LIQUIDITY_POOL)
pub fn get_liquidity_pool(env: &Env) -> Result<Option<Address>, CreditLineError> {
Ok(env.storage().instance().get(&LIQUIDITY_POOL))
}

/// Set the Liquidity Pool Contract address
Expand All @@ -193,8 +210,8 @@ pub fn set_liquidity_pool(env: &Env, address: &Address) {
}

/// Get the Token Contract address
pub fn get_token(env: &Env) -> Option<Address> {
env.storage().instance().get(&TOKEN)
pub fn get_token(env: &Env) -> Result<Option<Address>, CreditLineError> {
Ok(env.storage().instance().get(&TOKEN))
}

/// Set the Token Contract address
Expand All @@ -203,20 +220,21 @@ pub fn set_token(env: &Env, address: &Address) {
}

/// Get the Parameters Contract address
pub fn get_parameters_contract(env: &Env) -> Option<Address> {
env.storage().instance().get(&PARAMETERS_CONTRACT)
pub fn get_parameters_contract(env: &Env) -> Result<Option<Address>, CreditLineError> {
Ok(env.storage().instance().get(&PARAMETERS_CONTRACT))
}

/// Set the Parameters Contract address
pub fn set_parameters_contract(env: &Env, address: &Address) {
env.storage().instance().set(&PARAMETERS_CONTRACT, address);
}

pub fn is_reentrancy_locked(env: &Env) -> bool {
env.storage()
pub fn is_reentrancy_locked(env: &Env) -> Result<bool, CreditLineError> {
Ok(env
.storage()
.instance()
.get(&REENTRANCY_LOCK)
.unwrap_or(false)
.unwrap_or(false))
}

pub fn set_reentrancy_locked(env: &Env, locked: bool) {
Expand All @@ -229,8 +247,13 @@ pub const PERSISTENT_TTL_THRESHOLD: u32 = 1_036_800;
pub const PERSISTENT_TTL_EXTEND_TO: u32 = 2_073_600;

/// Extend TTL for a persistent storage entry
pub fn extend_persistent_ttl_loan(env: &Env, key: &DataKey) {
fn extend_persistent_ttl(env: &Env, key: &DataKey) {
env.storage()
.persistent()
.extend_ttl(key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO);
}

/// Extend TTL for a loan storage entry
fn extend_persistent_ttl_loan(env: &Env, key: &DataKey) {
extend_persistent_ttl(env, key);
}
Loading
Loading