This directory contains the Soroban smart contracts for the Azable, written in Rust. The contracts are organized as a Cargo workspace.
The contracts directory is a Cargo workspace with the following members:
payment-stream: A contract for creating and managing continuous token streams.distributor: A contract for distributing tokens to multiple recipients.
Shared dependencies, such as the soroban-sdk, are managed in the root Cargo.toml of this workspace.
The payment-stream contract allows for the creation of linear payment streams, where a specified amount of tokens is released to a recipient over a defined period. Streams can also be created with a cliff (lockup) period, during which nothing is withdrawable before linear vesting begins.
- Stream: A
Streamis the central data structure, containing details about the sender, recipient, token, total amount, withdrawn amount, start and end times, an optional cliff duration, and the stream's status. - Cliff Period: The number of seconds after
start_timeduring which the recipient cannot withdraw. Once elapsed, tokens vest linearly across the full stream window, so the pro-rata share accrued during the cliff is released at the cliff boundary. - Status: A stream can have one of the following statuses:
Active,Paused,Canceled, orCompleted.
initialize(admin: Address): Initializes the contract with an administrative address.create_stream(...): Creates a new payment stream with specified parameters (no cliff).create_stream_with_cliff(...): Creates a payment stream with a cliff (lockup) period.get_stream(stream_id: u64): Retrieves the details of a specific stream.withdrawable_amount(stream_id: u64): Calculates the amount that can be withdrawn from a stream at the current time.withdraw(stream_id: u64, amount: i128): Allows the recipient to withdraw available funds.pause_stream(stream_id: u64): Pauses an active stream (sender only).resume_stream(stream_id: u64): Resumes a paused stream (sender only).cancel_stream(stream_id: u64): Cancels a stream (sender only), allowing for the recovery of unvested funds.
The distributor contract provides functionality for sending tokens to multiple recipients in a single transaction.
The contract supports two modes of distribution:
- Equal Distribution: A total amount is divided equally among a list of recipients.
- Weighted Distribution: Each recipient in a list receives a corresponding specified amount.
initialize(admin: Address): Initializes the contract with an administrative address.distribute_equal(...): Distributes a total amount equally among a list of recipients.distribute_weighted(...): Distributes specified amounts to a list of recipients.get_admin(): Retrieves the admin address.
To build and test the contracts, you can use the following commands from the root of the project:
-
Build Contracts:
pnpm build:contracts
This command runs
cargo build --releasewithin thecontractsworkspace. -
Run Contract Tests:
pnpm test:contracts
This command runs
cargo testfor all contracts in the workspace.
54 tests · 15 distributor · 39 payment stream ·
| Test | Verifies |
|---|---|
test_initialize |
Contract initialises state correctly on first deploy. |
test_re_initialize_fails |
A second call to initialize panics — prevents overwriting live state. |
| Test | Verifies |
|---|---|
test_set_protocol_fee |
Fee can be updated to a new valid value after initialisation. |
test_zero_protocol_fee |
Setting the fee to zero is accepted; distributions pass through with no deduction. |
| Test | Verifies |
|---|---|
test_distribute_equal |
A given amount is split evenly across all recipients and balances match. |
test_distribute_equal_with_protocol_fee |
The protocol fee is deducted before splitting; every recipient receives (amount − fee) / n. |
test_distribute_equal_empty_recipients |
Passing an empty recipient list panics — no distribution is attempted. |
test_distribute_equal_amount_too_small |
An amount smaller than the number of recipients panics — avoids zero-token transfers. |
| Test | Verifies |
|---|---|
test_distribute_weighted |
Each recipient receives a share proportional to their declared weight. |
test_distribute_weighted_with_protocol_fee |
The fee is deducted first; weighted shares are then calculated on the net amount. |
test_distribute_weighted_zero_amount |
A zero-amount weighted call panics — guards against no-op distributions. |
| Test | Verifies |
|---|---|
test_record_history |
Each distribution is persisted to the on-chain history log. |
test_update_token_statistics |
Per-token running totals (volume, count) are incremented after a distribution. |
test_update_global_stats |
The contract-wide aggregate counters update on every distribution. |
test_update_user_statistics |
Per-user totals (sent/received) are updated correctly. |
| Test | Verifies |
|---|---|
test_create_stream |
A new stream is created with the correct sender, recipient, amount, and duration. |
test_get_nonexistent_stream |
Querying a stream ID that does not exist panics. |
| Test | Verifies |
|---|---|
test_withdraw |
The recipient can withdraw the currently vested portion of the stream. |
test_withdrawable_amount |
The view function returns the correct vested amount at a given point in time. |
test_withdraw_max |
Withdrawing the full vested amount at stream completion transfers the entire balance. |
test_withdraw_after_pause_and_resume |
Vested tokens accumulated before and after a pause/resume cycle are both withdrawable. |
test_unauthorized_withdraw |
A caller who is neither the recipient nor an authorised delegate is rejected. |
| Test | Verifies |
|---|---|
test_set_delegate |
A delegate address is stored and recognised for the stream. |
test_delegate_withdraw |
The delegate can withdraw vested tokens on behalf of the recipient. |
test_overwrite_delegate |
Setting a new delegate replaces the previous one without error. |
test_revoke_delegate |
After revocation the former delegate loses withdrawal access. |
test_revoke_nonexistent_delegate |
Revoking when no delegate is set completes without error. |
test_set_self_delegate |
Assigning the sender as their own delegate panics. |
test_recipient_can_still_withdraw_after_delegate_set |
The recipient retains direct withdrawal access even when a delegate exists. |
test_unauthorized_delegate_withdraw_after_revoke |
A revoked delegate's attempt to withdraw panics. |
| Test | Verifies |
|---|---|
test_pause_and_resume_stream |
A stream can be paused and later resumed, returning to an active state. |
test_pausing_stops_token_vesting |
While paused the withdrawable amount does not increase over time. |
test_resuming_continues_from_where_it_left_off |
After resume, vesting picks up exactly where it stopped — no time is lost or double-counted. |
test_withdrawable_amount_zero_for_paused_streams |
A freshly paused stream with no prior vesting reports zero withdrawable. |
test_only_sender_can_pause |
Any caller other than the stream sender is rejected when pausing. |
test_only_sender_can_resume |
Any caller other than the stream sender is rejected when resuming. |
| Test | Verifies |
|---|---|
test_cancel_stream |
The sender can cancel an active stream; unvested funds are returned. |
| Test | Verifies |
|---|---|
test_deposit |
Additional funds can be deposited into an existing stream. |
test_deposit_multiple |
Several successive deposits accumulate correctly in the stream balance. |
test_deposit_after_withdrawal |
A deposit after a partial withdrawal restores the available balance. |
test_deposit_updates_last_activity |
Each deposit advances the stream's last_activity timestamp. |
test_deposit_exceeds_total |
Depositing more than the remaining stream capacity panics. |
test_deposit_negative_amount |
A negative deposit amount panics. |
test_deposit_invalid_amount |
A non-numeric or otherwise malformed amount panics. |
| Test | Verifies |
|---|---|
test_protocol_metrics_initialization |
All protocol-level counters start at zero on deploy. |
test_multiple_streams_metrics |
Creating several streams increments the global stream counter correctly. |
test_withdrawal_updates_metrics |
A withdrawal increments the protocol's total-withdrawn counter. |
test_withdraw_max_updates_metrics |
A full withdrawal updates both the stream-level and protocol-level metrics. |
test_multiple_withdrawals_accumulate_metrics |
Successive partial withdrawals sum correctly in the metrics. |
test_pause_updates_metrics |
Pausing a stream increments the pause counter. |
test_resume_updates_metrics |
Resuming a stream increments the resume counter. |
test_revoke_delegate_updates_metrics |
Revoking a delegate increments the revocation counter. |
test_stream_paused_event_emitted |
A StreamPaused event is emitted with the correct stream ID on pause. |
test_stream_resumed_event_emitted |
A StreamResumed event is emitted with the correct stream ID on resume. |