Skip to content
Merged
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
66 changes: 33 additions & 33 deletions contracts/common/src/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,39 +207,6 @@ pub fn sort_remainders_desc(order: &mut Vec<(u32, i128, Address)>) {
}
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{contract, contractimpl};

#[contract]
struct SplitTestContract;

#[contractimpl]
impl SplitTestContract {}

#[test]
fn compute_split_enforces_recipient_limit() {
let env = Env::default();
let contract_id = env.register(SplitTestContract, ());
let mut recipients = Vec::new(&env);
for _ in 0..MAX_RECIPIENTS {
recipients.push_back((Address::generate(&env), 500u32));
}

let payouts = env.as_contract(&contract_id, || {
compute_split(&env, 1_000, 0, &recipients).unwrap()
});
assert_eq!(payouts.shares.len(), MAX_RECIPIENTS);

recipients.push_back((Address::generate(&env), 0));
let result = env.as_contract(&contract_id, || {
compute_split(&env, 1_000, 0, &recipients)
});
assert!(matches!(result, Err(SplitError::TooManyRecipients)));
}
}

// Repeatedly move the largest remaining element to the end of the array,
// shrinking the heap until the array is sorted ascending by `less`.
let mut end = n;
Expand All @@ -252,3 +219,36 @@ pub fn sort_remainders_desc(order: &mut Vec<(u32, i128, Address)>) {
sift_down_remainder_order(order, 0, end);
}
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{contract, contractimpl, testutils::Address as _};

#[contract]
struct SplitTestContract;

#[contractimpl]
impl SplitTestContract {}

#[test]
fn compute_split_enforces_recipient_limit() {
let env = Env::default();
let contract_id = env.register(SplitTestContract, ());
let mut recipients = Vec::new(&env);
for _ in 0..MAX_RECIPIENTS {
recipients.push_back((Address::generate(&env), 500u32));
}

let payouts = env.as_contract(&contract_id, || {
compute_split(&env, 1_000, 0, &recipients).unwrap()
});
assert_eq!(payouts.shares.len(), MAX_RECIPIENTS);

recipients.push_back((Address::generate(&env), 0));
let result = env.as_contract(&contract_id, || {
compute_split(&env, 1_000, 0, &recipients)
});
assert!(matches!(result, Err(SplitError::TooManyRecipients)));
}
}
16 changes: 7 additions & 9 deletions contracts/escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3009,10 +3009,10 @@ fn test_extend_deadline_returns_contribution_not_found_when_archived() {
fn test_release_with_duplicate_recipient_addresses() {
let env = Env::default();
env.mock_all_auths();
let (_, _admin, treasury, client) = setup(&env);
let (_, _admin, _treasury, client) = setup(&env);

let token_admin = Address::generate(&env);
let (token_addr, asset_client, token_client) = create_token(&env, &token_admin);
let (token_addr, asset_client, _token_client) = create_token(&env, &token_admin);
let sponsor = Address::generate(&env);
asset_client.mint(&sponsor, &10_000_000_000i128);

Expand All @@ -3027,14 +3027,12 @@ fn test_release_with_duplicate_recipient_addresses() {
&None,
);

// Same address twice with 5000 bps each — must produce two separate
// transfers summing to the full 10000-bps entitlement.
// compute_split rejects duplicate addresses — `InvalidSplit` is the correct
// outcome because allowing it would silently double-pay alice at the expense
// of every other recipient's share, which is never intentional.
let recipients = vec![&env, (alice.clone(), 5_000u32), (alice.clone(), 5_000u32)];
client.release(&200u64, &recipients);

let distributable = 950_0000000i128; // after 5% fee
assert_eq!(token_client.balance(&alice), distributable);
assert_eq!(token_client.balance(&treasury), 50_0000000i128);
let err = client.try_release(&200u64, &recipients);
assert_eq!(err, Err(Ok(Error::InvalidSplit)));
}

#[test]
Expand Down
82 changes: 61 additions & 21 deletions contracts/maintenance-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,41 +274,81 @@ mod contract {
return Err(Error::TokenMismatch);
}

if pool.deposit_count >= MAX_DEPOSITS {
let mut existing_index = None;
let search_limit = pool.deposit_count.min(MAX_DEPOSITS);
for i in 0..search_limit {
let dkey = DataKey::Deposit(pool_id, i);
if let Some(deposit) = env.storage().persistent().get::<_, Deposit>(&dkey) {
if deposit.sponsor == sponsor {
existing_index = Some(i);
break;
}
}
}

if existing_index.is_none() && pool.deposit_count >= MAX_DEPOSITS {
return Err(Error::TooManyDeposits);
}

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
.deposit_count
.checked_add(1)
.ok_or(Error::DepositCountOverflow)?;

if let Some(index) = existing_index {
let dkey = DataKey::Deposit(pool_id, index);
let mut deposit: Deposit = env
.storage()
.persistent()
.get(&dkey)
.ok_or(Error::DepositNotFound)?;
deposit.amount = deposit
.amount
.checked_add(actual_received)
.ok_or(Error::ArithmeticOverflow)?;
deposit.timestamp = env.ledger().timestamp();
env.storage().persistent().set(&dkey, &deposit);
extend_ttl(&env, &dkey);
} else {
let index = pool.deposit_count;
pool.deposit_count = pool
.deposit_count
.checked_add(1)
.ok_or(Error::DepositCountOverflow)?;

let dkey = DataKey::Deposit(pool_id, index);
env.storage().persistent().set(
&dkey,
&Deposit {
sponsor: sponsor.clone(),
amount: actual_received,
timestamp: env.ledger().timestamp(),
},
);
extend_ttl(&env, &dkey);
}

env.storage().persistent().set(&pkey, &pool);
extend_ttl(&env, &pkey);

let dkey = DataKey::Deposit(pool_id, index);
env.storage().persistent().set(
&dkey,
&Deposit {
sponsor: sponsor.clone(),
amount,
timestamp: env.ledger().timestamp(),
},
);
extend_ttl(&env, &dkey);

// Refresh every prior deposit sub-record so older entries don't
// archive while the pool stays active with recurring new deposits.
// The newly-written record at `index` was already extended above;
Expand All @@ -321,7 +361,7 @@ mod contract {
pool_id,
sponsor,
token,
amount,
amount: actual_received,
balance: pool.balance,
}
.publish(&env);
Expand Down
Loading