|
20 | 20 | //! `InsufficientDeveloperBalance` rather than wrapping. |
21 | 21 | //! |
22 | 22 | //! 6. **Normal (non-overflow) paths** still produce correct arithmetic results. |
| 23 | +//! |
| 24 | +//! 7. **`withdraw_developer_balance`** (daily accumulator) — `checked_add` raises |
| 25 | +//! `DailyWithdrawCapExceeded` rather than silently saturating on overflow. |
23 | 26 |
|
24 | 27 | extern crate std; |
25 | 28 |
|
@@ -82,6 +85,23 @@ fn poke_dev_balance( |
82 | 85 | }); |
83 | 86 | } |
84 | 87 |
|
| 88 | +/// Directly write a developer's `WithdrawalToday` state in persistent storage. |
| 89 | +fn poke_withdrawal_today( |
| 90 | + env: &Env, |
| 91 | + contract_id: &Address, |
| 92 | + developer: &Address, |
| 93 | + amount: i128, |
| 94 | + day: u64, |
| 95 | +) { |
| 96 | + use crate::types::DailyWithdrawState; |
| 97 | + env.as_contract(contract_id, || { |
| 98 | + env.storage().persistent().set( |
| 99 | + &StorageKey::WithdrawalToday(developer.clone()), |
| 100 | + &DailyWithdrawState { day, amount }, |
| 101 | + ); |
| 102 | + }); |
| 103 | +} |
| 104 | + |
85 | 105 | /// Directly write the global pool's `total_balance` in instance storage. |
86 | 106 | fn poke_global_pool_balance(env: &Env, contract_id: &Address, total_balance: i128) { |
87 | 107 | use crate::types::GlobalPool; |
@@ -304,6 +324,45 @@ fn withdraw_more_than_balance_returns_error() { |
304 | 324 | ); |
305 | 325 | } |
306 | 326 |
|
| 327 | +// --------------------------------------------------------------------------- |
| 328 | +// 6. withdraw_developer_balance — daily accumulator overflow -> DailyWithdrawCapExceeded |
| 329 | +// --------------------------------------------------------------------------- |
| 330 | + |
| 331 | +/// When the developer has no cap set (0 = unlimited), the daily withdrawal |
| 332 | +/// accumulator still uses `checked_add` so that an i128 overflow fails |
| 333 | +/// loudly instead of silently saturating. |
| 334 | +#[test] |
| 335 | +fn withdraw_daily_amount_overflow_raises_error() { |
| 336 | + let (env, contract_id, _admin, vault, stored_usdc) = setup_with_usdc(); |
| 337 | + let client = CalloraSettlementClient::new(&env, &contract_id); |
| 338 | + let developer = Address::generate(&env); |
| 339 | + |
| 340 | + // Seed developer balance with a large amount so the withdrawal itself |
| 341 | + // does not fail with InsufficientDeveloperBalance. |
| 342 | + poke_dev_balance(&env, &contract_id, &developer, &stored_usdc, 1_000_000i128); |
| 343 | + |
| 344 | + // Seed the same-day withdrawal accumulator to i128::MAX - 1 so the |
| 345 | + // next withdrawal's checked_add will overflow. |
| 346 | + let today = env.ledger().timestamp() / 86400; |
| 347 | + poke_withdrawal_today(&env, &contract_id, &developer, i128::MAX - 1, today); |
| 348 | + |
| 349 | + // Mint enough USDC to the contract so the transfer can proceed |
| 350 | + // past the liquidity check. |
| 351 | + let token_admin_client = soroban_sdk::token::StellarAssetClient::new(&env, &stored_usdc); |
| 352 | + token_admin_client.mint(&contract_id, &1_000i128); |
| 353 | + |
| 354 | + // Withdraw 1 micro-unit — must fail because daily.amount would overflow. |
| 355 | + let result = client.try_withdraw_developer_balance(&developer, &1i128, &None); |
| 356 | + assert!( |
| 357 | + result.is_err(), |
| 358 | + "daily accumulator overflow should fail, got {result:?}" |
| 359 | + ); |
| 360 | +} |
| 361 | + |
| 362 | +// --------------------------------------------------------------------------- |
| 363 | +// 7. Normal withdrawals still work correctly |
| 364 | +// --------------------------------------------------------------------------- |
| 365 | + |
307 | 366 | /// Exact-balance withdrawal succeeds and leaves the developer at zero. |
308 | 367 | #[test] |
309 | 368 | fn withdraw_exact_balance_succeeds() { |
|
0 commit comments