Skip to content
Open
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
21 changes: 17 additions & 4 deletions contracts/maintenance-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -160,7 +173,7 @@ mod contract {
&dkey,
&Deposit {
sponsor,
amount,
amount: actual_received,
timestamp: env.ledger().timestamp(),
},
);
Expand Down
71 changes: 71 additions & 0 deletions contracts/maintenance-pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down