Skip to content

Commit 50650fc

Browse files
authored
Merge pull request #38 from Lansa-18/fix/issue-9-lending-pool-deposits-count-drift
Fix: LendingPool MaxPoolSize cap and DepositorCount drifting
2 parents aabf56b + 5fceb0e commit 50650fc

6 files changed

Lines changed: 6019 additions & 4 deletions

lending_pool/src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,12 @@ pub struct PoolStats {
9595
pub total_shares: i128,
9696
pub pool_token_balance: i128,
9797
pub depositor_count: u32,
98-
/// Fraction of tracked principal currently out on loan, in basis points.
99-
/// Only positive when active loans have reduced pool_balance below
100-
/// total_deposits.
98+
/// `(total_deposits − pool_token_balance) / total_deposits × 10 000`.
99+
/// Positive only when outstanding loans have reduced pool_balance below
100+
/// tracked principal. Note: accrued yield increases pool_balance, which
101+
/// partially offsets outstanding loans in this formula — utilisation may
102+
/// therefore understate the true loan fraction when significant yield has
103+
/// accumulated relative to outstanding principal.
101104
pub utilization_bps: u32,
102105
}
103106

@@ -383,7 +386,7 @@ impl LendingPool {
383386
let count = Self::read_depositor_count(env, token);
384387
env.storage().instance().set(
385388
&DataKey::DepositorCount(token.clone()),
386-
&count.saturating_sub(1),
389+
&count.checked_sub(1).expect("depositor_count underflow"),
387390
);
388391
} else {
389392
env.storage().persistent().set(&share_key, &remaining);

lending_pool/src/test.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,3 +1791,144 @@ fn test_set_loan_manager_requires_admin() {
17911791
.try_set_loan_manager(&token_id, &reporter)
17921792
.is_err());
17931793
}
1794+
1795+
// ── Issue #9: DepositorCount correctness ─────────────────────────────────────
1796+
// Note: TotalDeposits principal-tracking is already covered by #36's
1797+
// test_total_deposits_tracks_principal_after_yield_then_redeem.
1798+
// These tests focus on the DepositorCount hardening (checked_sub underflow
1799+
// guard) and cap enforcement after yield-bearing withdrawals.
1800+
1801+
#[test]
1802+
fn test_cap_enforced_after_yield_bearing_withdrawal() {
1803+
// Two providers each deposit 1000 (cap = 2000). After yield arrives,
1804+
// A fully withdraws. TotalDeposits must correctly reflect only B's
1805+
// remaining principal (1000), leaving exactly 1000 room under the cap.
1806+
let env = Env::default();
1807+
env.mock_all_auths();
1808+
1809+
let token_admin = Address::generate(&env);
1810+
let (token_id, stellar_asset_client, _) = create_token_contract(&env, &token_admin);
1811+
1812+
let pool_id = env.register(LendingPool, ());
1813+
let pool_client = LendingPoolClient::new(&env, &pool_id);
1814+
pool_client.initialize(&token_admin);
1815+
pool_client.set_withdrawal_cooldown(&0);
1816+
pool_client.set_max_pool_size(&token_id, &2_000);
1817+
1818+
let provider_a = Address::generate(&env);
1819+
let provider_b = Address::generate(&env);
1820+
let provider_c = Address::generate(&env);
1821+
stellar_asset_client.mint(&provider_a, &1_000);
1822+
stellar_asset_client.mint(&provider_b, &1_000);
1823+
stellar_asset_client.mint(&provider_c, &1_000);
1824+
1825+
pool_client.deposit(&provider_a, &token_id, &1_000);
1826+
pool_client.deposit(&provider_b, &token_id, &1_000);
1827+
assert_eq!(pool_client.get_total_deposits(&token_id), 2_000);
1828+
1829+
// 1000 tokens of yield arrive; record_yield updates TotalManagedAssets.
1830+
stellar_asset_client.mint(&pool_id, &1_000);
1831+
pool_client.record_yield(&token_id, &1_000);
1832+
// managed = 3000, total_shares = 2000.
1833+
1834+
// A redeems all 1000 shares → gets 1500 assets (principal + yield).
1835+
// TotalDeposits must become 1000 (B's principal only).
1836+
pool_client.withdraw(&provider_a, &token_id, &1_000);
1837+
assert_eq!(pool_client.get_total_deposits(&token_id), 1_000);
1838+
1839+
// Cap = 2000, TotalDeposits = 1000 → room = 1000.
1840+
// Depositing 1001 must be rejected.
1841+
let res = pool_client.try_deposit(&provider_c, &token_id, &1_001);
1842+
assert_eq!(res, Err(Ok(crate::PoolError::PoolSizeExceeded)));
1843+
1844+
// Depositing exactly 1000 fills the remaining cap room and must succeed.
1845+
pool_client.deposit(&provider_c, &token_id, &1_000);
1846+
assert_eq!(pool_client.get_total_deposits(&token_id), 2_000);
1847+
}
1848+
1849+
#[test]
1850+
fn test_depositor_count_across_deposit_partial_full_withdraw_redeposit() {
1851+
// DepositorCount must stay consistent across every state transition:
1852+
// first deposit (+1), partial withdraw (unchanged), full withdraw (-1),
1853+
// and re-deposit from zero shares (+1 again).
1854+
let env = Env::default();
1855+
env.mock_all_auths();
1856+
1857+
let token_admin = Address::generate(&env);
1858+
let (token_id, stellar_asset_client, _) = create_token_contract(&env, &token_admin);
1859+
1860+
let pool_id = env.register(LendingPool, ());
1861+
let pool_client = LendingPoolClient::new(&env, &pool_id);
1862+
pool_client.initialize(&token_admin);
1863+
pool_client.set_withdrawal_cooldown(&0);
1864+
1865+
let provider_a = Address::generate(&env);
1866+
let provider_b = Address::generate(&env);
1867+
stellar_asset_client.mint(&provider_a, &2_000);
1868+
stellar_asset_client.mint(&provider_b, &2_000);
1869+
1870+
// Two deposits → count = 2.
1871+
pool_client.deposit(&provider_a, &token_id, &2_000);
1872+
pool_client.deposit(&provider_b, &token_id, &2_000);
1873+
assert_eq!(pool_client.get_pool_stats(&token_id).depositor_count, 2);
1874+
1875+
// Partial withdrawal by A (remaining > 0) → count unchanged.
1876+
pool_client.withdraw(&provider_a, &token_id, &1_000);
1877+
assert_eq!(pool_client.get_pool_stats(&token_id).depositor_count, 2);
1878+
1879+
// Full withdrawal by A (remaining == 0) → count decrements.
1880+
pool_client.withdraw(&provider_a, &token_id, &1_000);
1881+
assert_eq!(pool_client.get_pool_stats(&token_id).depositor_count, 1);
1882+
1883+
// A re-deposits (existing_shares == 0, key was removed) → count increments.
1884+
pool_client.deposit(&provider_a, &token_id, &1_000);
1885+
assert_eq!(pool_client.get_pool_stats(&token_id).depositor_count, 2);
1886+
1887+
// Full withdrawal by B → count decrements.
1888+
pool_client.withdraw(&provider_b, &token_id, &2_000);
1889+
assert_eq!(pool_client.get_pool_stats(&token_id).depositor_count, 1);
1890+
1891+
// Full withdrawal by A → count reaches 0.
1892+
pool_client.withdraw(&provider_a, &token_id, &1_000);
1893+
assert_eq!(pool_client.get_pool_stats(&token_id).depositor_count, 0);
1894+
}
1895+
1896+
#[test]
1897+
fn test_total_deposits_zero_after_all_shares_redeemed_with_yield() {
1898+
// When all providers fully withdraw after yield has accumulated,
1899+
// TotalDeposits and total_shares must both reach zero.
1900+
let env = Env::default();
1901+
env.mock_all_auths();
1902+
1903+
let token_admin = Address::generate(&env);
1904+
let (token_id, stellar_asset_client, _) = create_token_contract(&env, &token_admin);
1905+
1906+
let pool_id = env.register(LendingPool, ());
1907+
let pool_client = LendingPoolClient::new(&env, &pool_id);
1908+
pool_client.initialize(&token_admin);
1909+
pool_client.set_withdrawal_cooldown(&0);
1910+
1911+
let provider_a = Address::generate(&env);
1912+
let provider_b = Address::generate(&env);
1913+
stellar_asset_client.mint(&provider_a, &1_000);
1914+
stellar_asset_client.mint(&provider_b, &1_000);
1915+
1916+
pool_client.deposit(&provider_a, &token_id, &1_000); // 1000 shares
1917+
pool_client.deposit(&provider_b, &token_id, &1_000); // 1000 shares
1918+
// managed = 2000, total_shares = 2000, TotalDeposits = 2000.
1919+
1920+
// 1000 tokens of yield arrive; record_yield updates TotalManagedAssets.
1921+
stellar_asset_client.mint(&pool_id, &1_000);
1922+
pool_client.record_yield(&token_id, &1_000);
1923+
// managed = 3000, total_shares = 2000.
1924+
1925+
// A redeems 1000 shares → 1500 assets. TotalDeposits → 1000.
1926+
pool_client.withdraw(&provider_a, &token_id, &1_000);
1927+
assert_eq!(pool_client.get_total_deposits(&token_id), 1_000);
1928+
1929+
// B redeems 1000 shares → 1500 assets. TotalDeposits → 0.
1930+
pool_client.withdraw(&provider_b, &token_id, &1_000);
1931+
assert_eq!(pool_client.get_total_deposits(&token_id), 0);
1932+
assert_eq!(pool_client.get_total_shares(&token_id), 0);
1933+
assert_eq!(pool_client.get_pool_stats(&token_id).depositor_count, 0);
1934+
}

0 commit comments

Comments
 (0)