Skip to content

Commit 4423ab6

Browse files
authored
Merge pull request #27 from jotel-dev/loan-manager-deposit-collateral-cei-fix
Loan manager deposit collateral cei fix
2 parents 2011656 + 0f9f82e commit 4423ab6

2 files changed

Lines changed: 87 additions & 13 deletions

File tree

loan_manager/src/lib.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,7 @@ impl LoanManager {
13921392
}
13931393

13941394
let loan_key = DataKey::Loan(loan_id);
1395-
let loan: Loan = env
1395+
let mut loan: Loan = env
13961396
.storage()
13971397
.persistent()
13981398
.get(&loan_key)
@@ -1419,27 +1419,32 @@ impl LoanManager {
14191419
.storage()
14201420
.instance()
14211421
.get(&DataKey::Token)
1422-
.expect("token not set");
1422+
.ok_or(LoanError::NotInitialized)?;
14231423
let token_client = TokenClient::new(&env, &token);
1424-
token_client.transfer(&loan.borrower, &env.current_contract_address(), &amount);
1425-
1426-
let loan_key = DataKey::Loan(loan_id);
1427-
let mut loan: Loan = env
1428-
.storage()
1429-
.persistent()
1430-
.get(&loan_key)
1431-
.expect("loan not found");
14321424

14331425
let updated_collateral = loan
14341426
.collateral_amount
14351427
.checked_add(amount)
1436-
.expect("collateral overflow");
1428+
.ok_or(LoanError::AmountTooLarge)?;
14371429
loan.collateral_amount = updated_collateral;
14381430
env.storage().persistent().set(&loan_key, &loan);
14391431
Self::bump_persistent_ttl(&env, &loan_key);
14401432

1433+
// CEI: commit the updated loan state before the external token transfer.
1434+
token_client.transfer(&loan.borrower, &env.current_contract_address(), &amount);
1435+
1436+
// Re-validate after the external interaction with typed errors rather than panicking.
1437+
let stored_loan: Loan = env
1438+
.storage()
1439+
.persistent()
1440+
.get(&loan_key)
1441+
.ok_or(LoanError::LoanNotFound)?;
1442+
if stored_loan.status != LoanStatus::Approved {
1443+
return Err(LoanError::LoanNotActive);
1444+
}
1445+
14411446
env.events().publish(
1442-
(symbol_short!("ColDep"), loan_id, loan.borrower),
1447+
(symbol_short!("ColDep"), loan_id, stored_loan.borrower),
14431448
updated_collateral,
14441449
);
14451450

loan_manager/src/test.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,40 @@ use lending_pool::{LendingPool, LendingPoolClient};
33
use remittance_nft::{RemittanceNFT, RemittanceNFTClient};
44
use soroban_sdk::testutils::Ledger as _;
55
use soroban_sdk::token::{Client as TokenClient, StellarAssetClient};
6-
use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String};
6+
use soroban_sdk::{
7+
contract, contractimpl, testutils::Address as _, Address, BytesN, Env, String, Symbol,
8+
};
9+
10+
#[contract]
11+
pub struct MaliciousToken;
12+
13+
#[contractimpl]
14+
impl MaliciousToken {
15+
pub fn set_attack_target(env: Env, manager: Address, loan_id: u32) {
16+
env.storage()
17+
.persistent()
18+
.set(&Symbol::new(&env, "manager"), &manager);
19+
env.storage()
20+
.persistent()
21+
.set(&Symbol::new(&env, "loan_id"), &loan_id);
22+
}
23+
24+
pub fn transfer(env: Env, _from: Address, _to: Address, _amount: i128) {
25+
let manager: Address = env
26+
.storage()
27+
.persistent()
28+
.get(&Symbol::new(&env, "manager"))
29+
.unwrap();
30+
let loan_id: u32 = env
31+
.storage()
32+
.persistent()
33+
.get(&Symbol::new(&env, "loan_id"))
34+
.unwrap();
35+
env.as_contract(&manager, || {
36+
env.storage().persistent().remove(&DataKey::Loan(loan_id));
37+
});
38+
}
39+
}
740

841
fn setup_test<'a>(
942
env: &Env,
@@ -1335,6 +1368,42 @@ fn test_deposit_collateral_and_auto_release_on_full_repayment() {
13351368
);
13361369
}
13371370

1371+
#[test]
1372+
fn test_deposit_collateral_rejects_loan_removed_during_token_transfer() {
1373+
let env = Env::default();
1374+
env.mock_all_auths_allowing_non_root_auth();
1375+
1376+
let (manager, nft_client, pool_client, token_id, _token_admin) = setup_test(&env);
1377+
let borrower = Address::generate(&env);
1378+
1379+
let history_hash = BytesN::from_array(&env, &[0u8; 32]);
1380+
nft_client.mint(
1381+
&borrower,
1382+
&650,
1383+
&history_hash,
1384+
&String::from_str(&env, "ipfs://QmTest"),
1385+
&None,
1386+
);
1387+
1388+
let stellar_token = StellarAssetClient::new(&env, &token_id);
1389+
stellar_token.mint(&pool_client, &20_000);
1390+
stellar_token.mint(&borrower, &20_000);
1391+
1392+
let loan_id = manager.request_loan(&borrower, &1_000, &17280);
1393+
manager.approve_loan(&loan_id);
1394+
1395+
let malicious = env.register(MaliciousToken, ());
1396+
let malicious_client = MaliciousTokenClient::new(&env, &malicious);
1397+
malicious_client.set_attack_target(&manager.address, &loan_id);
1398+
1399+
env.as_contract(&manager.address, || {
1400+
env.storage().instance().set(&DataKey::Token, &malicious);
1401+
});
1402+
1403+
let result = manager.try_deposit_collateral(&loan_id, &300);
1404+
assert_eq!(result, Err(Ok(LoanError::LoanNotFound)));
1405+
}
1406+
13381407
#[test]
13391408
fn test_collateral_is_seized_on_default() {
13401409
let env = Env::default();

0 commit comments

Comments
 (0)