Skip to content

Commit dc1554c

Browse files
author
AI Assistant
committed
feat: harden batch trade ops (parity, fees, duplicate IDs)
Align batch_create_trades with single-trade side effects and batch fee writes. Add expiry and timeline to batch_fund_trades; reject duplicate trade IDs in fund and confirm. Call users/admin completion hooks in batch_confirm_trades. Add DuplicateTradeInBatch error and regression test. Made-with: Cursor
1 parent 8c3e363 commit dc1554c

3 files changed

Lines changed: 56 additions & 1 deletion

File tree

src/errors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ pub enum ContractError {
3333
InsufficientAllowance = 27,
3434
TradeExpired = 28,
3535
NotExpiredYet = 29,
36+
DuplicateTradeInBatch = 30,
3637
}

src/lib.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,9 @@ impl StellarEscrowContract {
539539
let fee_bps = tiers::effective_fee_bps(&env, &seller, base_fee_bps);
540540
let mut trade_ids = soroban_sdk::Vec::new(&env);
541541
let mut total_amount: u64 = 0;
542+
let mut total_fees: u64 = 0;
542543
let now = env.ledger().sequence();
544+
let currency = types::Currency::Usdc;
543545
for (buyer, amount, arbitrator) in trades.iter() {
544546
if amount == 0 { return Err(ContractError::InvalidAmount); }
545547
if let Some(ref arb) = arbitrator {
@@ -552,14 +554,21 @@ impl StellarEscrowContract {
552554
id: trade_id, seller: seller.clone(), buyer: buyer.clone(),
553555
amount, fee, arbitrator, status: TradeStatus::Created,
554556
created_at: now, updated_at: now, metadata: None,
555-
currency: types::Currency::Usdc, expiry_time: None,
557+
currency: currency.clone(), expiry_time: None,
556558
};
557559
save_trade(&env, trade_id, &trade);
558560
index_trade_for_address(&env, &seller, trade_id);
559561
index_trade_for_address(&env, &buyer, trade_id);
562+
append_timeline_entry(&env, trade_id, TimelineEntry { status: TradeStatus::Created, ledger: now });
563+
users::record_trade_created(&env, &seller, &buyer, amount);
564+
admin::on_trade_created(&env, amount);
560565
trade_ids.push_back(trade_id);
561566
total_amount = total_amount.checked_add(amount).ok_or(ContractError::Overflow)?;
567+
total_fees = total_fees.checked_add(fee).ok_or(ContractError::Overflow)?;
562568
}
569+
let current_fees = get_accumulated_fees(&env)?;
570+
set_accumulated_fees(&env, current_fees.checked_add(total_fees).ok_or(ContractError::Overflow)?);
571+
add_currency_fees(&env, &currency, total_fees)?;
563572
events::emit_batch_trades_created(&env, trade_ids.len() as u32, total_amount);
564573
Ok(trade_ids)
565574
}
@@ -577,10 +586,21 @@ impl StellarEscrowContract {
577586
let token = get_usdc_token(&env)?;
578587
let token_client = TokenClient::new(&env, &token);
579588
let mut total_amount: u64 = 0;
589+
let mut seen: soroban_sdk::Map<u64, bool> = soroban_sdk::Map::new(&env);
590+
let now_ts = env.ledger().timestamp();
580591
for trade_id in trade_ids.iter() {
592+
if seen.get(trade_id).is_some() {
593+
return Err(ContractError::DuplicateTradeInBatch);
594+
}
595+
seen.set(trade_id, true);
581596
let trade = get_trade(&env, trade_id)?;
582597
if trade.status != TradeStatus::Created { return Err(ContractError::InvalidStatus); }
583598
if trade.buyer != buyer { return Err(ContractError::Unauthorized); }
599+
if let Some(expiry) = trade.expiry_time {
600+
if now_ts >= expiry {
601+
return Err(ContractError::TradeExpired);
602+
}
603+
}
584604
total_amount = total_amount.checked_add(trade.amount).ok_or(ContractError::Overflow)?;
585605
}
586606
token_client.transfer(&buyer, &env.current_contract_address(), &(total_amount as i128));
@@ -590,6 +610,7 @@ impl StellarEscrowContract {
590610
trade.status = TradeStatus::Funded;
591611
trade.updated_at = now;
592612
save_trade(&env, trade_id, &trade);
613+
append_timeline_entry(&env, trade_id, TimelineEntry { status: TradeStatus::Funded, ledger: now });
593614
}
594615
events::emit_batch_trades_funded(&env, trade_ids.len() as u32, total_amount);
595616
Ok(())
@@ -610,7 +631,12 @@ impl StellarEscrowContract {
610631
let mut total_payout: u64 = 0;
611632
let mut total_fees: u64 = 0;
612633
let mut seller_payouts: soroban_sdk::Map<Address, u64> = soroban_sdk::Map::new(&env);
634+
let mut seen: soroban_sdk::Map<u64, bool> = soroban_sdk::Map::new(&env);
613635
for trade_id in trade_ids.iter() {
636+
if seen.get(trade_id).is_some() {
637+
return Err(ContractError::DuplicateTradeInBatch);
638+
}
639+
seen.set(trade_id, true);
614640
let trade = get_trade(&env, trade_id)?;
615641
if trade.status != TradeStatus::Completed { return Err(ContractError::InvalidStatus); }
616642
if trade.buyer != buyer { return Err(ContractError::Unauthorized); }
@@ -629,6 +655,8 @@ impl StellarEscrowContract {
629655
let trade = get_trade(&env, trade_id)?;
630656
tiers::record_volume(&env, &trade.seller, trade.amount)?;
631657
tiers::record_volume(&env, &trade.buyer, trade.amount)?;
658+
users::record_trade_completed(&env, &trade.seller, &trade.buyer);
659+
admin::on_trade_completed(&env, trade.fee);
632660
}
633661
events::emit_batch_trades_confirmed(&env, trade_ids.len() as u32, total_payout, total_fees);
634662
Ok(())

src/test.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,32 @@ fn test_batch_create_rejects_empty_and_oversized_batches() {
977977
assert_eq!(oversized_result, Err(Ok(ContractError::BatchLimitExceeded)));
978978
}
979979

980+
#[test]
981+
fn test_batch_fund_and_confirm_reject_duplicate_trade_ids() {
982+
let (env, client, _, seller, buyer) = setup();
983+
let mut batch = soroban_sdk::Vec::new(&env);
984+
batch.push_back((buyer.clone(), 1_000u64, None));
985+
let trade_ids = client.batch_create_trades(&seller, &batch);
986+
let id = trade_ids.get(0).unwrap();
987+
988+
let mut dup_fund = soroban_sdk::Vec::new(&env);
989+
dup_fund.push_back(id);
990+
dup_fund.push_back(id);
991+
let fund_err = client.try_batch_fund_trades(&buyer, &dup_fund);
992+
assert_eq!(fund_err, Err(Ok(ContractError::DuplicateTradeInBatch)));
993+
994+
let mut single = soroban_sdk::Vec::new(&env);
995+
single.push_back(id);
996+
client.batch_fund_trades(&buyer, &single);
997+
client.complete_trade(&id);
998+
999+
let mut dup_confirm = soroban_sdk::Vec::new(&env);
1000+
dup_confirm.push_back(id);
1001+
dup_confirm.push_back(id);
1002+
let confirm_err = client.try_batch_confirm_trades(&buyer, &dup_confirm);
1003+
assert_eq!(confirm_err, Err(Ok(ContractError::DuplicateTradeInBatch)));
1004+
}
1005+
9801006
#[test]
9811007
fn test_custom_fee_configuration_changes_trade_fee_and_tier_state() {
9821008
let (_env, client, _, seller, buyer) = setup();

0 commit comments

Comments
 (0)