Skip to content
Closed
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
52 changes: 39 additions & 13 deletions contracts/loan_manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ impl LoanManager {
const PERSISTENT_TTL_BUMP: u32 = 518400;
const DEFAULT_INTEREST_RATE_BPS: u32 = 1200;
const DEFAULT_TERM_LEDGERS: u32 = 17280;
const CURRENT_VERSION: u32 = 4;
const CURRENT_VERSION: u32 = 5;
const DEFAULT_LATE_FEE_RATE_BPS: u32 = 500;
const MAX_LATE_FEE_CAP_BPS: u32 = 2500;
const DEFAULT_MAX_LOAN_AMOUNT: i128 = 50_000;
Expand Down Expand Up @@ -1034,6 +1034,21 @@ impl LoanManager {
.instance()
.set(&DataKey::LateFeeRateBps, &late_fee_rate);

// NOTE: BorrowerLoans storage migration
// In v5, BorrowerLoans(Address) moved from instance to persistent storage
// to prevent unbounded instance storage inflation. Existing instance-stored
// lists cannot be automatically migrated because Soroban does not support
// iterating over instance storage keys. However, this is safe because:
// 1. BorrowerLoanCount (used for cap enforcement) was already in persistent
// storage and remains unaffected.
// 2. Individual Loan records were already in persistent storage.
// 3. The get_borrower_loans function will return an empty vec for borrowers
// whose data was only in instance storage; their active loans are still
// tracked correctly via BorrowerLoanCount.
// If deployed instance already has BorrowerLoans in instance, those entries
// will be ignored by the new persistent-storage reads. Users should re-request
// any loans that were in-flight during migration.

// Update contract version and mark migration as complete
env.storage()
.instance()
Expand Down Expand Up @@ -1162,18 +1177,19 @@ impl LoanManager {
Self::bump_instance_ttl(&env);
Self::bump_persistent_ttl(&env, &DataKey::Loan(loan_counter));

// Add loan ID to borrower's loan list
// Add loan ID to borrower's loan list (persistent storage to avoid
// inflating instance storage which is loaded on every call)
let borrower_loans_key = DataKey::BorrowerLoans(borrower.clone());
let mut borrower_loans: Vec<u32> = env
.storage()
.instance()
.persistent()
.get(&borrower_loans_key)
.unwrap_or(Vec::new(&env));
borrower_loans.push_back(loan_counter);
env.storage()
.instance()
.persistent()
.set(&borrower_loans_key, &borrower_loans);
Self::bump_instance_ttl(&env);
Self::bump_persistent_ttl(&env, &borrower_loans_key);

events::loan_requested(&env, loan_counter, borrower.clone(), amount);
Ok(loan_counter)
Expand Down Expand Up @@ -1969,7 +1985,7 @@ impl LoanManager {
let borrower_loans_key = DataKey::BorrowerLoans(loan.borrower.clone());
if let Some(existing) = env
.storage()
.instance()
.persistent()
.get::<_, Vec<u32>>(&borrower_loans_key)
{
let mut updated: Vec<u32> = Vec::new(&env);
Expand All @@ -1979,9 +1995,12 @@ impl LoanManager {
}
}
if updated.is_empty() {
env.storage().instance().remove(&borrower_loans_key);
env.storage().persistent().remove(&borrower_loans_key);
} else {
env.storage().instance().set(&borrower_loans_key, &updated);
env.storage()
.persistent()
.set(&borrower_loans_key, &updated);
Self::bump_persistent_ttl(&env, &borrower_loans_key);
}
}

Expand Down Expand Up @@ -2444,11 +2463,18 @@ impl LoanManager {
}

pub fn get_borrower_loans(env: Env, borrower: Address) -> Vec<u32> {
Self::bump_instance_ttl(&env);
env.storage()
.instance()
.get(&DataKey::BorrowerLoans(borrower))
.unwrap_or(Vec::new(&env))
let key = DataKey::BorrowerLoans(borrower);
if env.storage().persistent().has(&key) {
let loans = env
.storage()
.persistent()
.get(&key)
.unwrap_or(Vec::new(&env));
Self::bump_persistent_ttl(&env, &key);
loans
} else {
Vec::new(&env)
}
}

pub fn get_min_score(env: Env) -> u32 {
Expand Down
6 changes: 3 additions & 3 deletions contracts/loan_manager/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ fn test_migration_guard_prevents_double_execution() {
// First migration should succeed
manager.migrate();
let version1 = manager.version();
assert_eq!(version1, 4);
assert_eq!(version1, 5);

// Verify data is still readable after migration
let loan = manager.get_loan(&loan_id);
Expand All @@ -257,7 +257,7 @@ fn test_migration_guard_prevents_double_execution() {
// Second migration should be idempotent (not error, just return early)
manager.migrate();
let version2 = manager.version();
assert_eq!(version2, 4);
assert_eq!(version2, 5);

// Data should still be readable
let loan_after = manager.get_loan(&loan_id);
Expand All @@ -272,7 +272,7 @@ fn test_loan_request_success() {

let (manager, nft_client, _pool, _token, _token_admin) = setup_test(&env);
let borrower = Address::generate(&env);
assert_eq!(manager.version(), 4);
assert_eq!(manager.version(), 5);

// Give borrower a score high enough to pass (>= 500)
let history_hash = soroban_sdk::BytesN::from_array(&env, &[0u8; 32]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@
]
},
"val": {
"u32": 4
"u32": 5
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,55 @@
518400
]
],
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4",
"key": {
"vec": [
{
"symbol": "BorrowerLoans"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM"
}
]
},
"durability": "persistent"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4",
"key": {
"vec": [
{
"symbol": "BorrowerLoans"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM"
}
]
},
"durability": "persistent",
"val": {
"vec": [
{
"u32": 1
}
]
}
}
},
"ext": "v0"
},
518400
]
],
[
{
"contract_data": {
Expand Down Expand Up @@ -1173,25 +1222,6 @@
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
}
},
{
"key": {
"vec": [
{
"symbol": "BorrowerLoans"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM"
}
]
},
"val": {
"vec": [
{
"u32": 1
}
]
}
},
{
"key": {
"vec": [
Expand Down Expand Up @@ -1390,7 +1420,7 @@
]
},
"val": {
"u32": 4
"u32": 5
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,55 @@
518400
]
],
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4",
"key": {
"vec": [
{
"symbol": "BorrowerLoans"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM"
}
]
},
"durability": "persistent"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4",
"key": {
"vec": [
{
"symbol": "BorrowerLoans"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM"
}
]
},
"durability": "persistent",
"val": {
"vec": [
{
"u32": 1
}
]
}
}
},
"ext": "v0"
},
518400
]
],
[
{
"contract_data": {
Expand Down Expand Up @@ -1005,25 +1054,6 @@
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
}
},
{
"key": {
"vec": [
{
"symbol": "BorrowerLoans"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM"
}
]
},
"val": {
"vec": [
{
"u32": 1
}
]
}
},
{
"key": {
"vec": [
Expand Down Expand Up @@ -1204,7 +1234,7 @@
]
},
"val": {
"u32": 4
"u32": 5
}
}
]
Expand Down
Loading
Loading