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 is active; stream creation is blocked | create_stream, create_streams, withdraw, cancel_stream, top_up_stream, update_rate_per_second |
StartTimeInPast |
5 | start_time is before the current ledger timestamp |
create_stream, create_streams |
ArithmeticOverflow |
6 | Arithmetic overflow in stream calculations | create_stream, create_streams, update_rate_per_second, top_up_stream, batch_create_streams |
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, 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 |
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: Global emergency pause is active; non-admin mutations are blocked.
Trigger Conditions:
- Admin called
set_global_emergency_pause(true) - Contract is in emergency pause mode
Affected Roles:
| Role | Can Trigger | Notes |
|---|---|---|
| Sender | Yes | Create/modify streams blocked |
| Recipient | Yes | withdraw blocked (use calculate_accrued to check balance) |
| Admin | No | Admin operations exempt |
Client Action:
match client.try_create_stream(...) {
Ok(stream_id) => { /* success */ }
Err(ContractError::ContractPaused) => {
// Notify user: "Contract temporarily paused"
// Check `get_config` for pause status
// Retry later or contact admin
}
Err(e) => { /* handle other errors */ }
}Success Semantics: Returns u64 stream_id (when unpaused).
Integrator Note: During pause, calculate_accrued and get_stream_state remain functional.
Recipients can check their balance but cannot withdraw.
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 ().
These are runtime panics that should not occur in normal operation:
| Panic Message | Cause | Client Action |
|---|---|---|
can only close completed streams |
close_completed_stream on non-completed stream |
Use get_stream_state to check status |
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 | - | - | - |
top_up_stream |
- | StreamNotFound, Unauthorized, InvalidParams, InvalidState, ArithmeticOverflow | 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) |
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 |
- All 13
ContractErrorvariants - Role-based error mapping
- Success/failure semantics for each operation
- Time-driven edge cases
- Client action recommendations
| 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 |