diff --git a/contracts/settlement/src/errors.rs b/contracts/settlement/src/errors.rs index d8b29f7d..b7826fc3 100644 --- a/contracts/settlement/src/errors.rs +++ b/contracts/settlement/src/errors.rs @@ -34,6 +34,9 @@ use soroban_sdk::contracterror; /// | 24 | InvalidClaimWindow | Claim window parameters are invalid | /// | 25 | ClaimWindowClosed | Developer claim window is not currently open | /// | 26 | MinBalanceViolation | Withdrawal would leave balance below the minimum | +/// | 27 | ReplayDetected | Settlement claim was replayed or out of order | +/// | 28 | BatchEmpty | Batch operation received an empty vector | +/// | 29 | BatchTooLarge | Batch operation exceeded the maximum allowed size | #[contracterror] #[derive(Clone, Copy, Debug, PartialEq)] #[repr(u32)] @@ -65,4 +68,6 @@ pub enum SettlementError { ClaimWindowClosed = 25, MinBalanceViolation = 26, ReplayDetected = 27, + BatchEmpty = 28, + BatchTooLarge = 29, } diff --git a/contracts/settlement/src/lib.rs b/contracts/settlement/src/lib.rs index 2f96d95d..bf3a2cad 100644 --- a/contracts/settlement/src/lib.rs +++ b/contracts/settlement/src/lib.rs @@ -196,6 +196,8 @@ impl CalloraSettlement { /// /// # Validation /// All amounts must be `> 0`. Empty and oversized batches are rejected before any state change. + /// The contract returns typed errors for empty batches (`BatchEmpty`), oversized batches + /// (`BatchTooLarge`), and non-positive amounts (`AmountNotPositive`). /// /// # Atomicity /// All validation runs before any state is written. A failure on any item leaves the @@ -214,13 +216,19 @@ impl CalloraSettlement { Self::require_authorized_caller(env.clone(), caller.clone()); let n = items.len(); - assert!(n > 0, "batch_receive_payment requires at least one item"); - assert!(n <= MAX_BATCH_SIZE, "batch too large"); + if n == 0 { + env.panic_with_error(SettlementError::BatchEmpty); + } + if n > MAX_BATCH_SIZE { + env.panic_with_error(SettlementError::BatchTooLarge); + } // Validate all amounts before touching state. for item in items.iter() { let (_, amount) = item; - assert!(amount > 0, "amount must be positive"); + if amount <= 0 { + env.panic_with_error(SettlementError::AmountNotPositive); + } } // Replay guard: validate ALL developer HWMs before any state change. diff --git a/contracts/settlement/src/settlement_tests.rs b/contracts/settlement/src/settlement_tests.rs index 5ac0ab7b..a284127f 100644 --- a/contracts/settlement/src/settlement_tests.rs +++ b/contracts/settlement/src/settlement_tests.rs @@ -2100,7 +2100,7 @@ mod settlement_tests { let items: soroban_sdk::Vec<(Address, i128)> = soroban_sdk::Vec::new(&env); let result = client.try_batch_receive_payment(&vault, &items, &token, &1u32); - assert!(result.is_err()); + assert!(is_error(result, SettlementError::BatchEmpty)); } #[test] @@ -2115,7 +2115,7 @@ mod settlement_tests { items.push_back((dev.clone(), 1i128)); } let result = client.try_batch_receive_payment(&vault, &items, &token, &1u32); - assert!(result.is_err()); + assert!(is_error(result, SettlementError::BatchTooLarge)); } #[test] @@ -2127,7 +2127,7 @@ mod settlement_tests { let mut items = soroban_sdk::Vec::new(&env); items.push_back((dev.clone(), 0i128)); let result = client.try_batch_receive_payment(&vault, &items, &token, &1u32); - assert!(result.is_err()); + assert!(is_error(result, SettlementError::AmountNotPositive)); } #[test] @@ -2139,7 +2139,7 @@ mod settlement_tests { let mut items = soroban_sdk::Vec::new(&env); items.push_back((dev.clone(), -1i128)); let result = client.try_batch_receive_payment(&vault, &items, &token, &1u32); - assert!(result.is_err()); + assert!(is_error(result, SettlementError::AmountNotPositive)); } #[test] diff --git a/contracts/settlement/src/test_error_codes.rs b/contracts/settlement/src/test_error_codes.rs index 14ce3251..ad06db70 100644 --- a/contracts/settlement/src/test_error_codes.rs +++ b/contracts/settlement/src/test_error_codes.rs @@ -31,6 +31,10 @@ fn settlement_error_codes_are_stable_and_unique() { (23, SettlementError::OverDraft), (24, SettlementError::InvalidClaimWindow), (25, SettlementError::ClaimWindowClosed), + (26, SettlementError::MinBalanceViolation), + (27, SettlementError::ReplayDetected), + (28, SettlementError::BatchEmpty), + (29, SettlementError::BatchTooLarge), ]; let mut seen = BTreeSet::new(); @@ -42,7 +46,7 @@ fn settlement_error_codes_are_stable_and_unique() { ); } - assert_eq!(seen.len(), 25); + assert_eq!(seen.len(), 29); } #[test] @@ -74,6 +78,10 @@ fn error_code_docs_list_every_settlement_code() { "| 23 | `OverDraft` | Settlement | Withdrawal amount exceeds the developer's balance |", "| 24 | `InvalidClaimWindow` | Settlement | Claim window end timestamp is before the start timestamp |", "| 25 | `ClaimWindowClosed` | Settlement | Developer attempted to claim outside their configured claim window |", + "| 26 | `MinBalanceViolation` | Settlement | Withdrawal would leave balance below the minimum |", + "| 27 | `ReplayDetected` | Settlement | Settlement claim was replayed or out of order |", + "| 28 | `BatchEmpty` | Settlement | Batch operation received an empty vector |", + "| 29 | `BatchTooLarge` | Settlement | Batch operation exceeded the maximum allowed size |", ]; for line in expected_lines { diff --git a/docs/ERROR_CODES.md b/docs/ERROR_CODES.md index b1c29daa..7c80498d 100644 --- a/docs/ERROR_CODES.md +++ b/docs/ERROR_CODES.md @@ -82,6 +82,10 @@ must not be reassigned once released. | 23 | `OverDraft` | Settlement | Withdrawal amount exceeds the developer's balance | | 24 | `InvalidClaimWindow` | Settlement | Claim window end timestamp is before the start timestamp | | 25 | `ClaimWindowClosed` | Settlement | Developer attempted to claim outside their configured claim window | +| 26 | `MinBalanceViolation` | Settlement | Withdrawal would leave balance below the minimum | +| 27 | `ReplayDetected` | Settlement | Settlement claim was replayed or out of order | +| 28 | `BatchEmpty` | Settlement | Batch operation received an empty vector | +| 29 | `BatchTooLarge` | Settlement | Batch operation exceeded the maximum allowed size | ## Revenue Pool