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
15 changes: 9 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,7 @@ impl AnchornetContract {
if amount <= 0 {
return Err(Error::InvalidAmount);
}
Self::calculate_fee(
amount,
Self::effective_fee_bps(&env, &asset),
)
Self::calculate_fee(amount, Self::effective_fee_bps(&env, &asset))
}

/// Grants or revokes a protocol fee waiver for `anchor`. While waived,
Expand Down Expand Up @@ -725,7 +722,10 @@ impl AnchornetContract {
}

let mut pool = storage::get_pool(&env, &settlement.asset);
pool.total = pool.total.checked_add(settlement.amount).ok_or(Error::Overflow)?;
pool.total = pool
.total
.checked_add(settlement.amount)
.ok_or(Error::Overflow)?;
storage::set_pool(&env, &settlement.asset, &pool);

settlement.status = SettlementStatus::Cancelled;
Expand Down Expand Up @@ -776,7 +776,10 @@ impl AnchornetContract {
}

let mut pool = storage::get_pool(&env, &settlement.asset);
pool.total = pool.total.checked_add(settlement.amount).ok_or(Error::Overflow)?;
pool.total = pool
.total
.checked_add(settlement.amount)
.ok_or(Error::Overflow)?;
storage::set_pool(&env, &settlement.asset, &pool);

settlement.status = SettlementStatus::Expired;
Expand Down
255 changes: 241 additions & 14 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1227,11 +1227,20 @@ fn test_list_settlements_by_anchor_and_asset_empty_for_unknown() {
let stranger = Address::generate(&env);
let other_asset = symbol_short!("EURC");

assert_eq!(client.list_settlements_by_anchor_and_asset(&stranger, &asset, &1, &10).len(), 0);
assert_eq!(client.list_settlements_by_anchor_and_asset(&anchor, &other_asset, &1, &10).len(), 0);
assert_eq!(
client
.list_settlements_by_anchor_and_asset(&stranger, &asset, &1, &10)
.len(),
0
);
assert_eq!(
client
.list_settlements_by_anchor_and_asset(&anchor, &other_asset, &1, &10)
.len(),
0
);
}


#[test]
fn test_version() {
let env = Env::default();
Expand Down Expand Up @@ -1502,6 +1511,87 @@ fn test_list_anchors_reflects_reregistration() {
assert_eq!(anchors.get(0).unwrap(), anchor);
}

/// Regression test for deregister/re-register cycle preserving balance and
/// pool.providers state (no double-count, no reset, funds remain withdrawable).
///
/// Per `deregister_anchor` doc: "Existing pool liquidity is unaffected; the
/// anchor simply cannot open new positions".
///
/// Covers acceptance criteria:
/// - balances / anchor_balances / pool.providers unchanged across deregister
/// - re-register does not reset or double-count
/// - immediate withdraw after re-register succeeds
/// - provide_liquidity and open_settlement blocked while deregistered
#[test]
fn test_deregister_re_register_preserves_balances_and_provider_count() {
let env = Env::default();
env.mock_all_auths();
let (client, admin) = setup(&env);
let anchor = Address::generate(&env);
let asset = symbol_short!("USDC");

client.initialize(&admin);
client.register_anchor(&anchor);
client.provide_liquidity(&anchor, &asset, &1_000);

// Initial state
assert_eq!(client.balance(&anchor, &asset), 1_000);
let pool = client.pool(&asset);
assert_eq!(pool.total, 1_000);
assert_eq!(pool.providers, 1);
let bals = client.anchor_balances(&anchor, &0, &10);
assert_eq!(bals.len(), 1);
assert_eq!(bals.get(0).unwrap(), (asset.clone(), 1_000));

// Deregister — balances and provider count unaffected
client.deregister_anchor(&anchor);
assert!(!client.is_anchor(&anchor));

assert_eq!(client.balance(&anchor, &asset), 1_000);
let pool = client.pool(&asset);
assert_eq!(pool.total, 1_000);
assert_eq!(pool.providers, 1);
let bals = client.anchor_balances(&anchor, &0, &10);
assert_eq!(bals.len(), 1);
assert_eq!(bals.get(0).unwrap(), (asset.clone(), 1_000));

// Cannot provide or open settlement while deregistered (AnchorNotRegistered)
let err_provide = client
.try_provide_liquidity(&anchor, &asset, &100)
.err()
.unwrap()
.unwrap();
assert_eq!(err_provide, Error::AnchorNotRegistered);

let err_settle = client
.try_open_settlement(&anchor, &asset, &100)
.err()
.unwrap()
.unwrap();
assert_eq!(err_settle, Error::AnchorNotRegistered);

// Re-register the same anchor
client.register_anchor(&anchor);
assert!(client.is_anchor(&anchor));

// State must be exactly as before deregistration (no reset, no double-count)
assert_eq!(client.balance(&anchor, &asset), 1_000);
let pool = client.pool(&asset);
assert_eq!(pool.total, 1_000);
assert_eq!(pool.providers, 1);
let bals = client.anchor_balances(&anchor, &0, &10);
assert_eq!(bals.len(), 1);
assert_eq!(bals.get(0).unwrap(), (asset.clone(), 1_000));

// Anchor can immediately withdraw its preserved balance (no need to re-provide)
let withdrawn = client.withdraw_all_liquidity(&anchor, &asset);
assert_eq!(withdrawn, 1_000);
assert_eq!(client.balance(&anchor, &asset), 0);
let pool = client.pool(&asset);
assert_eq!(pool.total, 0);
assert_eq!(pool.providers, 0);
}

#[test]
fn test_list_anchors_pagination() {
let env = Env::default();
Expand Down Expand Up @@ -2331,8 +2421,22 @@ fn test_clear_operator() {
assert_eq!(err, Error::NoOperator);
assert!(!client.is_operator(&operator));

assert_operator_rejected!(env, client, operator, "pause", (), client.try_pause(&operator));
assert_operator_rejected!(env, client, operator, "unpause", (), client.try_unpause(&operator));
assert_operator_rejected!(
env,
client,
operator,
"pause",
(),
client.try_pause(&operator)
);
assert_operator_rejected!(
env,
client,
operator,
"unpause",
(),
client.try_unpause(&operator)
);
assert_operator_rejected!(
env,
client,
Expand All @@ -2341,7 +2445,7 @@ fn test_clear_operator() {
(),
client.try_extend_instance_ttl(&operator)
);

// Admin can still act
client.pause(&admin);
assert!(client.is_paused());
Expand Down Expand Up @@ -2888,11 +2992,7 @@ fn test_settlement_age_rejects_unknown_id() {
let env = Env::default();
let (client, _admin, _anchor, _asset) = funded(&env, 1_000);

let err = client
.try_settlement_age(&99)
.err()
.unwrap()
.unwrap();
let err = client.try_settlement_age(&99).err().unwrap().unwrap();
assert_eq!(err, Error::SettlementNotFound);
}

Expand Down Expand Up @@ -3760,6 +3860,127 @@ fn test_provide_liquidity_multi_blocked_while_paused() {
assert_eq!(err, Error::Paused);
}

// ---------------------------------------------------------------------------
// provide_liquidity_multi atomicity regression tests
//
// The existing test_provide_liquidity_multi_rejects_duplicate_asset test only
// covers the case where the duplicate is at the front of the batch (both
// entries are the same asset). These regression tests verify that an invalid
// entry appearing *later* in the requests vector — after several valid distinct
// assets — causes zero mutations across the entire batch, including the valid
// entries that appeared before the invalid one. This enforces the all-or-nothing
// atomicity guarantee that provide_liquidity_multi's doc comment promises.
// ---------------------------------------------------------------------------

/// Regression test: an invalid entry (duplicate asset) appearing *later* in the
/// batch — after several valid distinct assets — must cause zero mutations across
/// the entire batch, including the valid entries that appeared before the invalid
/// one. Without the two-pass validate-then-apply design, the first two legs could
/// be partially applied before the duplicate is detected on the third.
#[test]
fn test_provide_liquidity_multi_zero_mutations_on_late_duplicate_failure() {
let env = Env::default();
env.mock_all_auths();
let (client, admin) = setup(&env);
let anchor = Address::generate(&env);
let asset1 = symbol_short!("AST1");
let asset2 = symbol_short!("AST2");

client.initialize(&admin);
client.register_anchor(&anchor);

// Snapshot balances and pool totals for every affected asset before the
// call. All are zero since no liquidity has been provided yet.
let bal1_before = client.balance(&anchor, &asset1);
let bal2_before = client.balance(&anchor, &asset2);
let total1_before = client.total_liquidity(&asset1);
let total2_before = client.total_liquidity(&asset2);
let providers1_before = client.pool(&asset1).providers;
let providers2_before = client.pool(&asset2).providers;

// First two entries are valid distinct assets; third is a duplicate of the
// first — the invalid entry appears *after* valid ones.
let requests = vec![
&env,
(asset1.clone(), 100),
(asset2.clone(), 200),
(asset1.clone(), 300), // duplicate of asset1
];
let err = client
.try_provide_liquidity_multi(&anchor, &requests)
.err()
.unwrap()
.unwrap();
assert_eq!(err, Error::DuplicateAssetInBatch);

// Verify state unchanged for every asset in the batch — including the valid
// ones (asset1, asset2) that appeared before the invalid entry.
assert_eq!(client.balance(&anchor, &asset1), bal1_before);
assert_eq!(client.balance(&anchor, &asset2), bal2_before);
assert_eq!(client.total_liquidity(&asset1), total1_before);
assert_eq!(client.total_liquidity(&asset2), total2_before);
assert_eq!(client.pool(&asset1).providers, providers1_before);
assert_eq!(client.pool(&asset2).providers, providers2_before);
}

/// Regression test: an invalid entry (non-positive amount) appearing *later* in
/// the batch — after several valid distinct assets — must cause zero mutations
/// across the entire batch, including the valid entries that appeared before the
/// invalid one. The non-positive amount is detected at a different point in the
/// validation loop than the duplicate-asset check, so it is covered separately.
#[test]
fn test_provide_liquidity_multi_zero_mutations_on_late_nonpositive_failure() {
let env = Env::default();
env.mock_all_auths();
let (client, admin) = setup(&env);
let anchor = Address::generate(&env);
let asset1 = symbol_short!("AST1");
let asset2 = symbol_short!("AST2");
let asset3 = symbol_short!("AST3");

client.initialize(&admin);
client.register_anchor(&anchor);

// Snapshot balances and pool totals for every affected asset before the
// call. All are zero since no liquidity has been provided yet.
let bal1_before = client.balance(&anchor, &asset1);
let bal2_before = client.balance(&anchor, &asset2);
let bal3_before = client.balance(&anchor, &asset3);
let total1_before = client.total_liquidity(&asset1);
let total2_before = client.total_liquidity(&asset2);
let total3_before = client.total_liquidity(&asset3);
let providers1_before = client.pool(&asset1).providers;
let providers2_before = client.pool(&asset2).providers;
let providers3_before = client.pool(&asset3).providers;

// First two entries are valid distinct assets; third has a non-positive
// amount — the invalid entry appears *after* valid ones.
let requests = vec![
&env,
(asset1.clone(), 100),
(asset2.clone(), 200),
(asset3.clone(), 0), // non-positive amount
];
let err = client
.try_provide_liquidity_multi(&anchor, &requests)
.err()
.unwrap()
.unwrap();
assert_eq!(err, Error::InvalidAmount);

// Verify state unchanged for every asset in the batch — including the valid
// ones (asset1, asset2) that appeared before the invalid entry.
assert_eq!(client.balance(&anchor, &asset1), bal1_before);
assert_eq!(client.balance(&anchor, &asset2), bal2_before);
assert_eq!(client.balance(&anchor, &asset3), bal3_before);
assert_eq!(client.total_liquidity(&asset1), total1_before);
assert_eq!(client.total_liquidity(&asset2), total2_before);
assert_eq!(client.total_liquidity(&asset3), total3_before);
assert_eq!(client.pool(&asset1).providers, providers1_before);
assert_eq!(client.pool(&asset2).providers, providers2_before);
assert_eq!(client.pool(&asset3).providers, providers3_before);
}

#[test]
fn test_total_settled_amount_sums_by_status() {
let env = Env::default();
Expand Down Expand Up @@ -4325,7 +4546,9 @@ fn test_list_settlements_by_anchor_and_asset_start_past_end_returns_empty() {
client.open_settlement(&anchor, &asset, &100);

assert_eq!(
client.list_settlements_by_anchor_and_asset(&anchor, &asset, &3, &10).len(),
client
.list_settlements_by_anchor_and_asset(&anchor, &asset, &3, &10)
.len(),
0
);
assert_eq!(
Expand All @@ -4343,11 +4566,15 @@ fn test_list_settlements_by_anchor_and_asset_limit_zero_returns_empty() {
client.open_settlement(&anchor, &asset, &100);

assert_eq!(
client.list_settlements_by_anchor_and_asset(&anchor, &asset, &1, &0).len(),
client
.list_settlements_by_anchor_and_asset(&anchor, &asset, &1, &0)
.len(),
0
);
assert_eq!(
client.list_settlements_by_anchor_and_asset(&anchor, &asset, &0, &0).len(),
client
.list_settlements_by_anchor_and_asset(&anchor, &asset, &0, &0)
.len(),
0
);
}
Expand Down