diff --git a/contracts/maintenance-pool/src/lib.rs b/contracts/maintenance-pool/src/lib.rs index e51a66b..723047a 100644 --- a/contracts/maintenance-pool/src/lib.rs +++ b/contracts/maintenance-pool/src/lib.rs @@ -135,16 +135,29 @@ mod contract { return Err(Error::TooManyDeposits); } + // Measure the actual received delta — fee-on-transfer or rebasing + // tokens deliver less than `amount`, so the books must credit what + // arrived, not what was requested. let token_client = token::Client::new(&env, &token); - token_client.transfer(&sponsor, env.current_contract_address(), &amount); + let actual_received = mergefi_common::measure_transfer_delta( + &env, + &token, + &env.current_contract_address(), + || { + token_client.transfer(&sponsor, env.current_contract_address(), &amount); + }, + ); + if actual_received <= 0 { + return Err(Error::InvalidAmount); + } pool.balance = pool .balance - .checked_add(amount) + .checked_add(actual_received) .ok_or(Error::ArithmeticOverflow)?; pool.total_deposited = pool .total_deposited - .checked_add(amount) + .checked_add(actual_received) .ok_or(Error::ArithmeticOverflow)?; let index = pool.deposit_count; pool.deposit_count = pool @@ -160,7 +173,7 @@ mod contract { &dkey, &Deposit { sponsor, - amount, + amount: actual_received, timestamp: env.ledger().timestamp(), }, ); diff --git a/contracts/maintenance-pool/src/test.rs b/contracts/maintenance-pool/src/test.rs index 1b0c34c..886bb40 100644 --- a/contracts/maintenance-pool/src/test.rs +++ b/contracts/maintenance-pool/src/test.rs @@ -147,6 +147,77 @@ fn test_withdraw_with_zero_fee_pays_full_amount() { assert_eq!(pool.total_withdrawn, 200_0000000i128); } +/// Fee-on-transfer mock: `transfer` credits the recipient 10% less than +/// requested — the shape `deposit`'s `measure_transfer_delta` path must +/// credit (`pool.balance` reflects what arrived, not what was asked). +#[contract] +pub struct MockFeeToken; + +#[contractimpl] +impl MockFeeToken { + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { + let bal_key = soroban_sdk::Symbol::new(&env, "bal"); + let from_bal: i128 = env + .storage() + .persistent() + .get(&(bal_key.clone(), from.clone())) + .unwrap_or(0); + let to_bal: i128 = env + .storage() + .persistent() + .get(&(bal_key.clone(), to.clone())) + .unwrap_or(0); + env.storage() + .persistent() + .set(&(bal_key.clone(), from), &(from_bal - amount)); + // 10% burns off in transit — the recipient only sees 90%. + env.storage() + .persistent() + .set(&(bal_key, to), &(to_bal + amount - amount / 10)); + } + + pub fn balance(env: Env, id: Address) -> i128 { + let bal_key = soroban_sdk::Symbol::new(&env, "bal"); + env.storage() + .persistent() + .get(&(bal_key, id)) + .unwrap_or(0) + } + + pub fn mint(env: Env, to: Address, amount: i128) { + let bal_key = soroban_sdk::Symbol::new(&env, "bal"); + let bal: i128 = env + .storage() + .persistent() + .get(&(bal_key.clone(), to.clone())) + .unwrap_or(0); + env.storage() + .persistent() + .set(&(bal_key, to), &(bal + amount)); + } +} + +#[test] +fn test_deposit_credits_received_delta_not_requested_amount() { + // The AC demands this case with the fee-on-transfer mock: deposit + // requests 100 but only 90 arrives — the ledger must say 90. + let env = Env::default(); + env.mock_all_auths(); + let (_admin, _treasury, client) = setup(&env); + + let token_addr = env.register(MockFeeToken, ()); + let fee_client = MockFeeTokenClient::new(&env, &token_addr); + let sponsor = Address::generate(&env); + fee_client.mint(&sponsor, &1_000i128); + + client.deposit(&7u64, &sponsor, &token_addr, &100i128); + + let pool = client.get_pool(&7u64); + assert_eq!(pool.balance, 90i128, "credits the received delta"); + assert_eq!(pool.total_deposited, 90i128); + assert_eq!(client.get_deposit(&7u64, &0u32).amount, 90i128); +} + #[test] fn test_deposit_rejects_invalid_amount() { let env = Env::default();