This document provides a comprehensive mapping of ContractError variants to their semantic meaning,
trigger conditions, affected roles, and recommended client actions. Integrators (wallets, indexers,
treasury tooling) can use this reference to handle protocol exceptions correctly.
| Error Code | Value | Description | Functions Returning It |
|---|---|---|---|
StreamNotFound |
1 | The specified stream does not exist | pause_stream, resume_stream, cancel_stream, withdraw, calculate_accrued, get_stream_state, admin overrides |
InvalidState |
2 | Operation attempted in an invalid state | cancel_stream, withdraw, withdraw_to, batch_withdraw, get_claimable_at, admin overrides |
InvalidParams |
3 | Function input parameters are invalid | create_stream, withdraw_to, update_rate_per_second, top_up_stream, extend_stream_end_time, shorten_stream_end_time, batch_create_streams |
ContractPaused |
4 | Global emergency pause or creation pause is active | create_stream, create_streams, create_streams_partial, withdraw, withdraw_to, batch_withdraw, cancel_stream, top_up_stream, update_rate_per_second, shorten_stream_end_time, extend_stream_end_time, update_recipient, trigger_auto_claim |
StartTimeInPast |
5 | start_time is before the current ledger timestamp |
create_stream, create_streams, create_streams_partial |
ArithmeticOverflow |
6 | Arithmetic overflow in stream calculations | create_stream, create_streams, create_streams_partial, update_rate_per_second, top_up_stream, shorten_stream_end_time, extend_stream_end_time |
Unauthorized |
7 | Caller is not authorized to perform this operation | init, set_admin, cancel_stream, top_up_stream, withdraw (recipient check) |
AlreadyInitialised |
8 | Contract has already been initialized | init |
InsufficientBalance |
9 | Token transfer failed due to insufficient balance or allowance | create_stream, create_streams_partial, cancel_stream, withdraw, top_up_stream |
InsufficientDeposit |
10 | Deposit amount does not cover the planned duration at the specified rate | create_stream, create_streams, update_rate_per_second, extend_stream_end_time |
StreamAlreadyPaused |
11 | Stream is already in Paused state |
pause_stream, pause_stream_as_admin |
StreamNotPaused |
12 | Stream is not Paused; cannot resume an Active stream |
resume_stream, resume_stream_as_admin |
StreamTerminalState |
13 | Stream is Completed or Cancelled; modification blocked |
pause_stream, resume_stream, admin overrides |
DuplicateStreamId |
14 | Duplicate stream IDs supplied to a batch operation | batch_withdraw |
InvalidSignature |
15 | Delegated withdrawal signature is invalid, expired, or nonce mismatch | delegated_withdraw |
BelowMinimumAmount |
16 | Withdrawable amount is below the expected_minimum_amount committed in the signature |
delegated_withdraw |
ClockRegression |
17 | Ledger-backed accrual observed a timestamp lower than the previous accrual timestamp | calculate_accrued, get_withdrawable, withdraw, withdraw_to, batch_withdraw, batch_withdraw_to, rate changes, cancel_stream, auto-claim paths |
ReservationCountZero |
17 | ID reservation count is zero | reserve_stream_ids |
ReservationLimitExceeded |
18 | ID reservation count exceeds MAX_ID_RESERVATION |
reserve_stream_ids |
SignatureDeadlineExpired |
19 | Delegated withdrawal signature deadline has passed | delegated_withdraw |
TemplateNotFound |
20 | Requested stream template does not exist | get_stream_template, create_stream_from_template, delete_stream_template |
TemplateLimitExceeded |
21 | Per-owner or global template limit would be exceeded | register_stream_template |
TemplateUnauthorized |
22 | Caller is not authorized to delete a template | delete_stream_template |
TokenVerificationFailed |
23 | Token contract does not expose the expected SEP-41 interface during init | init |
PauseReasonTooLong |
23 | Pause reason string exceeds MAX_PAUSE_REASON_BYTES |
pause_protocol |
Non-error enum values used by stream creation and accrual:
| Enum | Value | Meaning |
|---|---|---|
Linear |
0 | A StreamKind that accrues continuously over time after the start time. |
CliffOnly |
1 | A StreamKind that unlocks the full deposit at the cliff time in one step. |
Definition: The requested stream ID does not exist in contract storage.
Trigger Conditions:
stream_idis 0 or exceeds the current stream counter- Stream was never created
- Stream ID was invalidated (rare, for admin interventions)
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Anyone | Yes | Permissionless read functions return this error |
| Recipient | Yes | withdraw, get_stream_state |
| Sender | Yes | pause_stream, cancel_stream, top_up_stream |
| Admin | Yes | pause_stream_as_admin, cancel_stream_as_admin |
Client Action:
match client.try_get_stream_state(&stream_id) {
Ok(state) => { /* stream exists, use state */ }
Err(ContractError::StreamNotFound) => {
// Stream doesn't exist - check stream_id validity
// Notify user or refresh stream list
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns StreamState with valid fields.
Definition: Operation attempted in a state where it is not allowed.
Trigger Conditions:
| Scenario | Description |
|---|---|
| Withdraw from Completed stream | All funds already withdrawn |
| Withdraw from non-terminal Paused stream | Must resume first |
| Cancel Completed stream | Already terminal |
| Top-up Completed/Cancelled stream | Cannot modify terminal streams |
| Admin resume when not globally paused | Emergency pause not active |
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Recipient | Yes | withdraw on wrong status |
| Sender | Yes | cancel on terminal stream |
| Admin | Yes | resume_global_emergency_pause when not paused |
| Anyone | No | Permissionless reads don't trigger |
Client Action:
match client.try_withdraw(&stream_id) {
Ok(amount) => { /* success, update UI */ }
Err(ContractError::InvalidState) => {
let state = client.get_stream_state(&stream_id)?;
match state.status {
StreamStatus::Completed => "All funds withdrawn",
StreamStatus::Paused => "Resume stream first",
_ => "Contact support"
}
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns positive i128 amount (withdrawable balance).
Definition: One or more input parameters are invalid.
Trigger Conditions:
| Parameter | Invalid When |
|---|---|
sender == recipient |
Sender and recipient addresses are identical |
deposit_amount <= 0 |
Deposit must be positive |
rate_per_second <= 0 |
Rate must be positive |
start_time >= end_time |
Start must be before end |
cliff_time < start_time |
Cliff cannot precede start |
cliff_time > end_time |
Cliff cannot follow end |
destination == contract_address |
Cannot withdraw to contract |
new_rate_per_second <= old_rate |
Rate can only increase |
new_rate_per_second <= 0 |
Rate must be positive |
top_up_amount <= 0 |
Top-up must be positive |
extend_end_time <= current_end_time |
New end must be later |
shorten_end_time >= current_end_time |
New end must be earlier |
shorten_end_time < current_ledger_timestamp |
Cannot shorten to past |
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | create_stream, update_rate_per_second, top_up_stream |
| Admin | Yes | set_admin, init (wrong config) |
| Anyone | Yes | Invalid addresses |
Client Action:
match client.try_create_stream(&sender, &recipient, &deposit, &rate, &start, &cliff, &end) {
Ok(stream_id) => { /* success */ }
Err(ContractError::InvalidParams) => {
// Validate inputs locally before retrying
// Check: sender != recipient, deposit > 0, rate > 0, start < end
// cliff >= start, cliff <= end
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns u64 stream_id for create operations, () for updates.
Definition: The protocol is globally paused. No new streams may be created.
Trigger Conditions:
- Admin called
set_global_emergency_paused(true)orset_contract_paused(true) - Contract is in global emergency pause or creation pause mode
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | create_stream blocked if EITHER pause mode is active. cancel/update blocked ONLY if Global Emergency Pause is active. |
| Recipient | Yes | withdraw blocked ONLY if Global Emergency Pause is active. |
| Admin | No | Admin operations (pause/resume/init) are never blocked by the pause flag. |
Client Action:
match client.try_create_stream(...) {
Ok(stream_id) => { /* success */ }
Err(ContractError::ContractPaused) => {
// Notify user: "Protocol temporarily paused"
// Check `is_paused()` for current status
// Check `get_pause_info()` for reason and timestamp
// Retry later or contact admin
let info = client.get_pause_info();
if let Some(ref reason) = info.reason {
println!("Pause reason: {}", reason);
}
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns u64 stream_id (when unpaused).
Integrator Note: During any pause, calculate_accrued and get_stream_state remain functional.
Recipients can always check their balance.
- If
is_creation_paused()is true: Only NEW stream creation is blocked. - If
is_global_emergency_paused()is true: All mutations (creation, withdrawal, cancellation) are blocked. Useis_paused()(checks both) or inspectget_pause_info()for full details.
Definition: start_time is before the current ledger timestamp.
Trigger Conditions:
start_time < env.ledger().timestamp()at creation time- Stream cannot retroactively start
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | create_stream, create_streams |
Client Action:
let current_time = env.ledger().timestamp();
let start_time = calculate_future_start(current_time, delay_seconds);
match client.try_create_stream(..., &start_time, ...) {
Ok(stream_id) => { /* success */ }
Err(ContractError::StartTimeInPast) => {
// Use current_time + 1 as start_time
// Or schedule for future
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns u64 stream_id with future start_time.
Definition: Arithmetic overflow in stream calculations.
Trigger Conditions:
| Calculation | Overflow Condition |
|---|---|
rate * duration |
Result exceeds i128::MAX |
deposit + amount (top-up) |
Result exceeds i128::MAX |
duration calculation |
Overflow in u64 arithmetic |
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | Large deposit/rate combinations |
| Admin | Yes | Parameter adjustments |
Client Action:
match client.try_create_stream(..., &deposit, &rate, ...) {
Ok(stream_id) => { /* success */ }
Err(ContractError::ArithmeticOverflow) => {
// Reduce deposit or rate
// Break into multiple streams
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns u64 stream_id.
Integrator Note: The contract caps at i128::MAX which is ~1.7×10³⁸ for 18-decimal tokens.
This is effectively unlimited for any realistic token amount.
Definition: Caller is not authorized to perform this operation.
Trigger Conditions:
| Operation | Authorization Requirement |
|---|---|
cancel_stream |
Caller is sender or admin |
top_up_stream |
Caller is sender or admin |
withdraw |
Caller is recipient |
init |
First caller only |
set_admin |
Current admin only |
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Recipient | Yes | withdraw when not recipient |
| Sender | Yes | cancel when not sender/admin |
| Third Party | Yes | Any unauthorized call |
| Admin | Yes (by others) | Wrong admin calling |
Client Action:
match client.try_withdraw(&stream_id) {
Ok(amount) => { /* success */ }
Err(ContractError::Unauthorized) => {
// User is not the recipient
// Check `get_stream_state` to verify recipient address
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns positive i128 amount.
Definition: Contract has already been initialized.
Trigger Conditions:
initcalled whenConfigalready exists in storage- Second initialization attempt
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Anyone | Yes | Only first init succeeds |
Client Action:
match client.try_init(&token, &admin) {
Ok(()) => { /* success */ }
Err(ContractError::AlreadyInitialised) => {
// Contract already initialized - this is expected if already set up
// Call `get_config` to verify configuration
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns () on first initialization.
Definition: Token transfer failed due to insufficient balance or allowance.
Trigger Conditions:
- Sender's token balance < deposit_amount
- Sender's token allowance < deposit_amount (if not unlimited)
- Insufficient balance during
cancel_streamrefund - Insufficient balance during
top_up_stream
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | Primary case |
| Admin | Yes | If admin funds streams |
Client Action:
match client.try_create_stream(...) {
Ok(stream_id) => { /* success */ }
Err(ContractError::InsufficientBalance) => {
// Check token balance and allowance
// Fund account or increase allowance
let balance = token_client.balance(&sender);
let allowance = token_client.allowance(&sender, &contract_address);
// Notify user to fund account
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns u64 stream_id.
Definition: Deposit amount does not cover the planned duration at the specified rate.
Trigger Conditions:
| Condition | Formula |
|---|---|
| New stream | deposit < rate * (end - start) |
| Rate update | deposit < new_rate * remaining_duration |
| Extend end time | deposit < rate * new_total_duration |
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | create_stream, update_rate_per_second, extend_stream_end_time |
Client Action:
let duration = end_time - start_time;
let minimum_deposit = rate_per_second * duration as i128;
match client.try_create_stream(..., &(minimum_deposit + 1), ...) {
Ok(stream_id) => { /* success */ }
Err(ContractError::InsufficientDeposit) => {
// Increase deposit to minimum_deposit or higher
// Or reduce rate or duration
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns u64 stream_id.
Definition: Stream is already in Paused state.
Trigger Conditions:
pause_streamcalled on already-paused streampause_stream_as_admincalled on already-paused stream
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | pause_stream |
| Admin | Yes | pause_stream_as_admin |
Client Action:
match client.try_pause_stream(&stream_id) {
Ok(()) => { /* success */ }
Err(ContractError::StreamAlreadyPaused) => {
// Stream already paused - this is idempotent
// Check `get_stream_state` to confirm status
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns ().
Definition: Stream is not in Paused state.
Trigger Conditions:
resume_streamcalled onActivestream (not paused)resume_stream_as_admincalled on non-paused stream
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | resume_stream on active stream |
| Admin | Yes | resume_stream_as_admin on active stream |
Client Action:
match client.try_resume_stream(&stream_id) {
Ok(()) => { /* success */ }
Err(ContractError::StreamNotPaused) => {
// Stream not paused - check status
let state = client.get_stream_state(&stream_id)?;
if state.status == StreamStatus::Active {
// Already active, no action needed
}
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns ().
Definition: Stream is in a terminal state (Completed or Cancelled).
Trigger Conditions:
| Status | Blocked Operations |
|---|---|
| Completed | pause_stream, cancel_stream, top_up_stream, update_rate_per_second |
| Cancelled | pause_stream, resume_stream, cancel_stream, top_up_stream, update_rate_per_second |
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | Attempting to modify terminal stream |
| Recipient | No | Read operations still work |
| Admin | Yes | Admin overrides also blocked |
Client Action:
match client.try_pause_stream(&stream_id) {
Ok(()) => { /* success */ }
Err(ContractError::StreamTerminalState) => {
let state = client.get_stream_state(&stream_id)?;
match state.status {
StreamStatus::Completed => "Stream fully vested",
StreamStatus::Cancelled => "Stream cancelled",
_ => "Unexpected state"
}
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns ().
Definition: Duplicate stream IDs were supplied to a batch operation.
Trigger Conditions:
batch_withdrawcalled with astream_idsvector containing the same ID more than once
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Recipient | Yes | batch_withdraw with repeated IDs |
Client Action:
match client.try_batch_withdraw(&recipient, &stream_ids) {
Ok(results) => { /* success */ }
Err(ContractError::DuplicateStreamId) => {
// Deduplicate stream_ids before retrying
// Use a set to ensure uniqueness
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns Vec<BatchWithdrawResult> with unique entries.
Definition: Delegated withdrawal signature is invalid, expired, or nonce mismatch.
Trigger Conditions:
delegated_withdrawcalled with an invalid ed25519 signature- Signature has expired (timestamp check failed)
- Nonce mismatch (replay protection)
- Signature does not match the expected payload structure
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Relayer | Yes | Invalid signature from recipient |
| Recipient | Yes | Expired or replayed signature |
Client Action:
match client.try_delegated_withdraw(&relayer, &stream_id, &signature, &nonce, &expected_minimum) {
Ok(amount) => { /* success */ }
Err(ContractError::InvalidSignature) => {
// Signature validation failed
// Check: signature is valid ed25519, nonce is current, not expired
// Request new signature from recipient
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns positive i128 amount withdrawn.
Definition: Withdrawable amount is below the expected_minimum_amount committed in the signature.
Trigger Conditions:
delegated_withdrawcalled when accrued amount is less than theexpected_minimum_amountspecified in the signed payload- Protects recipient from relayer front-running or timing issues
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Relayer | Yes | Attempting withdrawal before sufficient accrual |
| Recipient | No | Recipient sets the minimum in signature |
Client Action:
match client.try_delegated_withdraw(&relayer, &stream_id, &signature, &nonce, &expected_minimum) {
Ok(amount) => { /* success */ }
Err(ContractError::BelowMinimumAmount) => {
// Accrued amount is below expected minimum
// Wait for more accrual or request new signature with lower minimum
let current_accrued = client.calculate_accrued(&stream_id)?;
// Retry when current_accrued >= expected_minimum
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns positive i128 amount withdrawn (>= expected_minimum).
Definition: Ledger-backed accrual observed a ledger timestamp lower than the previous accrual timestamp stored for the contract instance.
Trigger Conditions:
- Test harness sets
ledger().timestamp()backwards after a prior accrual calculation - Migration or environment change provides retrograde timestamps to accrual paths
Client Action: Treat as an infrastructure or test-environment failure. Do not retry at the lower timestamp; restore monotonic ledger time and rerun the transaction.
Success Semantics: No stream state is changed when the guard returns this error before withdrawable math.
Definition: reserve_stream_ids was called with count = 0.
Client Action: Request at least one ID before reserving, or skip the reservation call when there are no streams to pre-allocate.
Definition: reserve_stream_ids was called with count > MAX_ID_RESERVATION.
Client Action: Split large batches into reservations of at most MAX_ID_RESERVATION IDs.
Definition: A delegated withdrawal signature is structurally valid but its signed deadline has passed.
Client Action: Ask the recipient to sign a fresh delegated withdrawal payload with a later deadline.
Definition: The requested stream template is not present in storage.
Client Action: Refresh the template list before retrying, or register the template before creating streams from it.
Definition: Registering a template would exceed either the per-owner or global template limit.
Client Action: Delete unused templates or reuse an existing template instead of registering another one.
Definition: A caller attempted to delete or manage a template they do not own.
Client Action: Switch to the template owner account or leave the template unchanged.
Definition: During initialization, the configured token contract did not expose the expected SEP-41 interface.
Client Action: Verify the token address and deploy/init against a compatible token contract before retrying.
Definition: pause_protocol received a reason string longer than MAX_PAUSE_REASON_BYTES.
Client Action: Shorten the operator-facing pause reason and retry the pause transaction.
The following input-error paths previously caused a host-level panic. They now return
structured ContractError variants so clients can handle them programmatically:
| Former Panic | Now Returns | Functions |
|---|---|---|
panic_with_error!(ContractPaused) in require_not_globally_paused |
ContractError::ContractPaused |
withdraw, withdraw_to, batch_withdraw, cancel_stream, update_rate_per_second, shorten_stream_end_time, extend_stream_end_time |
panic_with_error!(ArithmeticOverflow) in batch deposit sum |
ContractError::ArithmeticOverflow |
create_streams |
panic_with_error!(ArithmeticOverflow) in rate × duration |
ContractError::ArithmeticOverflow |
update_rate_per_second, shorten_stream_end_time, extend_stream_end_time |
assert!("batch_withdraw stream_ids must be unique") |
ContractError::DuplicateStreamId |
batch_withdraw |
These are runtime panics that should not occur in normal operation and represent infrastructure-level failures (not user input errors):
| Panic Message | Cause | Client Action |
|---|---|---|
contract not initialised: missing config |
Storage access before init |
Call init first |
| Operation | Recipient | Sender | Admin | Anyone |
|---|---|---|---|---|
create_stream |
- | InvalidParams, InsufficientBalance, InsufficientDeposit | - | - |
pause_stream |
- | StreamNotFound, Unauthorized, StreamAlreadyPaused, StreamTerminalState | Same + StreamNotFound | StreamNotFound |
resume_stream |
- | StreamNotFound, Unauthorized, StreamNotPaused, StreamTerminalState | Same + StreamNotFound | StreamNotFound |
cancel_stream |
- | StreamNotFound, Unauthorized, InvalidState | StreamNotFound, Unauthorized | - |
withdraw |
StreamNotFound, Unauthorized, InvalidState | - | - | - |
delegated_withdraw |
- | - | - | InvalidSignature, BelowMinimumAmount, StreamNotFound, InvalidState |
top_up_stream |
- | StreamNotFound, Unauthorized, InvalidParams, InvalidState, ArithmeticOverflow, [UnsupportedStreamKind](#unsupportedstreamkind-17) |
StreamNotFound | - |
calculate_accrued |
StreamNotFound | StreamNotFound | StreamNotFound | StreamNotFound |
get_stream_state |
StreamNotFound | StreamNotFound | StreamNotFound | StreamNotFound |
| Edge Case | Error | Condition |
|---|---|---|
| Stream past end_time | InvalidState | withdraw on completed stream |
| Stream at exact end_time | Success | Full withdrawal allowed |
| Stream before cliff | InvalidState | withdraw returns 0 |
| Stream at exact cliff | Success | Accrual begins (from start_time) |
| Future start_time | Success | Stream created but no accrual yet |
| Cancel before cliff | Success | Full refund (accrued = 0) |
| Cancel after end_time | InvalidState | No refund (accrued = deposit) |
| Retrograde ledger timestamp | ClockRegression | ledger().timestamp() < previous accrual timestamp in test/debug builds |
Error handling is verified by tests in contracts/stream/src/test.rs:
| Error | Test Pattern |
|---|---|
| StreamNotFound | try_get_stream_state with invalid ID |
| InvalidParams | try_create_stream with sender == recipient, deposit <= 0, etc. |
| ContractPaused | Global pause then create |
| Unauthorized | Wrong recipient try_withdraw |
| InsufficientBalance | Sender with no tokens |
| InsufficientDeposit | deposit < rate * duration |
| StreamTerminalState | Pause/complete then modify |
| DuplicateStreamId | batch_withdraw with repeated stream IDs |
| InvalidSignature | delegated_withdraw with invalid or expired signature |
| BelowMinimumAmount | delegated_withdraw when accrued < expected_minimum |
| ClockRegression | clock_monotonicity.rs seeds non-monotonic ledger timestamps |
Discriminant stability is verified by test_contract_error_discriminants_are_stable in contracts/stream/src/test.rs, which asserts the exact u32 value of every ContractError variant and will fail at compile time if any value is changed.
The factory contract (contracts/factory) uses a separate FactoryError enum.
| Error | Description |
|---|---|
AlreadyInitialized |
Factory has already been initialized; init may only be called once |
NotInitialized |
Factory has not been initialized; call init first |
Unauthorized |
Caller is not the factory admin |
RecipientNotAllowlisted |
Recipient address is not on the factory allowlist |
DepositExceedsCap |
Requested deposit exceeds the per-stream cap configured in the factory |
DurationTooShort |
Stream duration is below the factory-enforced minimum |
InvalidTimeRange |
Requested stream end time is not strictly after its start time |
InvalidCliff |
Requested cliff time is outside the inclusive start/end window |
InvalidCap |
init or set_cap received a non-positive max_deposit; accepted range is 1..=i128::MAX |
InvalidMinDuration |
init or set_min_duration received a min_duration above MAX_MIN_DURATION_SECONDS; accepted range is 0..=3_153_600_000 seconds |
- All 14
ContractErrorvariants - Role-based error mapping
- Success/failure semantics for each operation
- Time-driven edge cases
- Client action recommendations
- Dust-attack prevention guidance
| Exclusion | Rationale | Residual Risk |
|---|---|---|
| Token-specific errors | Delegated to token contract | Low - caught by InsufficientBalance |
| Gas budget errors | Soroban runtime errors | Low - indicates contract size issues |
| Storage serialization errors | Runtime infrastructure | Very Low |
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Error code changes | Low | High | Versioning in client SDKs |
| Missing error cases | Low | Medium | Comprehensive test coverage |
| Client mishandling | Medium | Medium | This documentation |