All amounts are in the token's smallest unit (stroops for XLM-derived tokens). All deadlines are Unix timestamps (seconds since epoch).
All amount fields in function parameters and event data use the token's smallest unit. The table below lists the conventions for common tokens:
| Token | Decimals | Smallest Unit | Conversion Formula |
|---|---|---|---|
| XLM | 7 | stroop | human_amount × 10⁷ = on_chain_amount |
| USDC | 7 | micro-dollar | human_amount × 10⁷ = on_chain_amount |
| EURC | 7 | micro-euro | human_amount × 10⁷ = on_chain_amount |
To convert a human-readable amount to the value passed to the contract: multiply by 10^decimals. Every amount field — both in function parameters and event data — uses this smallest-unit representation (see the Event Payloads section for per-field annotations).
fn initialize(env: Env, owners: Vec<Address>, threshold: u32) -> Result<(), ContractError>One-shot initializer. Must be called before any other function. All owners must authorize this call.
| Parameter | Type | Constraints |
|---|---|---|
owners |
Vec<Address> |
1–20 unique addresses |
threshold |
u32 |
1 ≤ threshold ≤ owners.len() |
Errors: AlreadyInitialized, InvalidOwners, InvalidThreshold, DuplicateOwner
fn create_proposal(
env: Env,
proposer: Address,
to: Address,
amount: i128,
token: Address,
description: String,
deadline: u64,
) -> Result<u64, ContractError>Creates a transfer proposal. Returns the new proposal ID.
| Parameter | Type | Constraints |
|---|---|---|
proposer |
Address |
Must be an owner. Must authorize. |
to |
Address |
Recipient address |
amount |
i128 |
≥ 1 |
token |
Address |
Must implement Soroban token interface (decimals() + symbol()) |
description |
String |
1–300 characters |
deadline |
u64 |
> current ledger timestamp, ≤ now + 90 days |
Emits: ("created",) → ProposalCreatedEvent
Errors: Unauthorized, InvalidAmount, EmptyDescription, DescriptionTooLong, InvalidDeadline, InvalidDuration, InvalidToken, TooManyActiveProposals
fn approve(env: Env, approver: Address, proposal_id: u64) -> Result<(), ContractError>Records an approval for proposal_id from approver. Transitions status to Ready when threshold is reached.
| Parameter | Type | Constraints |
|---|---|---|
approver |
Address |
Must be an owner. Must authorize. |
proposal_id |
u64 |
Must refer to an existing Pending or Ready proposal |
Emits: ("approved",) → ProposalApprovedEvent
Errors: Unauthorized, ProposalNotFound, ProposalNotActive, AlreadyApproved
fn revoke(env: Env, approver: Address, proposal_id: u64) -> Result<(), ContractError>Withdraws the caller's approval. Transitions status back to Pending if approvals drop below threshold.
| Parameter | Type | Constraints |
|---|---|---|
approver |
Address |
Must be an owner with an existing approval. Must authorize. |
proposal_id |
u64 |
Must refer to an existing Pending or Ready proposal |
Emits: ("revoked",) → ProposalRevokedEvent
Errors: Unauthorized, ProposalNotFound, ProposalNotActive, NotApproved
fn execute(env: Env, executor: Address, proposal_id: u64) -> Result<(), ContractError>Executes a Ready proposal. Transfers amount of token from the contract to proposal.to. The contract must hold sufficient token balance.
| Parameter | Type | Constraints |
|---|---|---|
executor |
Address |
Must be an owner. Must authorize. |
proposal_id |
u64 |
Must refer to a Ready proposal whose deadline has not passed |
Emits: ("executed",) → ProposalExecutedEvent
Errors: Unauthorized, ProposalNotFound, ProposalNotActive, ThresholdNotMet, ProposalExpired, TransferFailed
fn get_proposal(env: Env, proposal_id: u64) -> Result<Proposal, ContractError>Returns the current proposal state with a freshly derived status (Expired status is derived from the current ledger timestamp without requiring a write).
Errors: NotInitialized, ProposalNotFound
fn get_proposals_paged(env: Env, offset: u64, limit: u32) -> Vec<Proposal>Returns a page of proposals. offset is 0-based (first proposal is at offset 0). limit is capped at 20. Proposals are returned in creation order.
fn get_owners(env: Env) -> Result<Vec<Address>, ContractError>Returns the current owner list.
fn get_threshold(env: Env) -> Result<u32, ContractError>Returns the approval threshold.
fn get_total_proposals(env: Env) -> u64Returns the total number of proposals ever created (including expired and executed).
fn is_owner(env: Env, address: Address) -> boolReturns true if address is a current owner.
fn has_approved(env: Env, proposal_id: u64, owner: Address) -> boolReturns true if owner has approved proposal_id.
| Code | Value | Meaning |
|---|---|---|
AlreadyInitialized |
1 | initialize was already called |
NotInitialized |
2 | Contract has not been initialized |
Unauthorized |
3 | Caller is not an owner |
InvalidThreshold |
4 | threshold = 0 or threshold > owner count |
InvalidOwners |
5 | Empty or oversized owner list |
ProposalNotFound |
6 | No proposal exists with that ID |
ProposalNotActive |
7 | Proposal is Executed, Expired, or Revoked |
AlreadyApproved |
8 | Owner already approved this proposal |
NotApproved |
9 | Owner has not approved — cannot revoke |
ThresholdNotMet |
10 | Approvals < threshold — cannot execute |
ProposalExpired |
11 | Deadline has passed |
InvalidAmount |
12 | Amount < 1 |
InvalidDeadline |
13 | Deadline ≤ current ledger timestamp |
InvalidToken |
14 | Token address does not implement Soroban token interface |
TransferFailed |
15 | On-chain token transfer failed (check contract balance) |
EmptyDescription |
16 | Description is empty |
DescriptionTooLong |
17 | Description exceeds 300 characters |
TooManyActiveProposals |
18 | Active proposal count reached limit (50) |
DuplicateOwner |
19 | Duplicate address in owner list |
ArithmeticError |
20 | Integer overflow |
InvalidDuration |
21 | Deadline more than 90 days in the future |
When calling contract functions from JavaScript, each parameter must be converted to the XDR SCVal format that the Soroban RPC expects. The Stellar SDK provides nativeToScVal for encoding and scValToNative for decoding.
Important:
u64andi128values exceed JavaScript's safe integer range (Number.MAX_SAFE_INTEGER= 2⁵³ − 1). They must be passed as JavaScriptBigInt— notNumber. UsingNumbersilently truncates the value.
| Rust Type | SCVal Variant | Build with nativeToScVal |
Decode with scValToNative |
|---|---|---|---|
Address |
ScVal::Address |
nativeToScVal(address, { type: 'address' }) |
scValToNative(scval) → "G…" string |
Vec<T> |
ScVal::Vec |
nativeToScVal(array, { type: 'vec' }) |
scValToNative(scval) → JavaScript Array |
u32 |
ScVal::U32 |
nativeToScVal(n, { type: 'u32' }) |
scValToNative(scval) → JavaScript Number |
u64 |
ScVal::U64 |
nativeToScVal(BigInt(n), { type: 'u64' }) |
scValToNative(scval) → JavaScript BigInt |
i128 |
ScVal::I128 |
nativeToScVal(BigInt(n), { type: 'i128' }) |
scValToNative(scval) → JavaScript BigInt |
String |
ScVal::String |
nativeToScVal(s, { type: 'string' }) |
scValToNative(scval) → JavaScript String |
bool |
ScVal::Bool |
nativeToScVal(b, { type: 'bool' }) |
scValToNative(scval) → JavaScript Boolean |
Proposal |
ScVal::Map |
N/A (output only) | scValToNative(scval) → plain JavaScript object whose field names match the Proposal struct in ARCHITECTURE.md §3 |
() (unit) |
ScVal::Void |
N/A (no input) | scValToNative(scval) → undefined |
Each Soroban event has an ordered topics array followed by a data payload. The contract address is implicitly prepended as the first element of the topics array by the network. The remainder is published explicitly by the contract via env.events().publish((symbol,), data).
Topics:
| Index | Value | XDR Type |
|---|---|---|
| 0 | Contract address (implicit) | ScVal::Address |
| 1 | "created" |
ScVal::Symbol |
Data fields:
| Field | Rust Type | XDR SCVal Type | Description |
|---|---|---|---|
id |
u64 |
ScVal::U64 |
Unique proposal ID assigned by the counter |
proposer |
Address |
ScVal::Address |
Owner who created the proposal |
to |
Address |
ScVal::Address |
Recipient address of the transfer |
amount |
i128 |
ScVal::I128 |
Transfer amount (see Token Amounts) |
threshold |
u32 |
ScVal::U32 |
Approval threshold in effect at creation |
struct ProposalCreatedEvent {
id: u64,
proposer: Address,
to: Address,
amount: i128,
threshold: u32,
}Topics:
| Index | Value | XDR Type |
|---|---|---|
| 0 | Contract address (implicit) | ScVal::Address |
| 1 | "approved" |
ScVal::Symbol |
Data fields:
| Field | Rust Type | XDR SCVal Type | Description |
|---|---|---|---|
id |
u64 |
ScVal::U64 |
Proposal ID that received the approval |
approver |
Address |
ScVal::Address |
Owner who approved |
approvals |
u32 |
ScVal::U32 |
Running total of approvals after this vote |
threshold |
u32 |
ScVal::U32 |
Approval threshold at vote time |
struct ProposalApprovedEvent {
id: u64,
approver: Address,
approvals: u32,
threshold: u32,
}Topics:
| Index | Value | XDR Type |
|---|---|---|
| 0 | Contract address (implicit) | ScVal::Address |
| 1 | "revoked" |
ScVal::Symbol |
Data fields:
| Field | Rust Type | XDR SCVal Type | Description |
|---|---|---|---|
id |
u64 |
ScVal::U64 |
Proposal ID the approval was revoked from |
approver |
Address |
ScVal::Address |
Owner who revoked their approval |
approvals |
u32 |
ScVal::U32 |
Remaining approval count after the revoke |
struct ProposalRevokedEvent {
id: u64,
approver: Address,
approvals: u32,
}Topics:
| Index | Value | XDR Type |
|---|---|---|
| 0 | Contract address (implicit) | ScVal::Address |
| 1 | "executed" |
ScVal::Symbol |
Data fields:
| Field | Rust Type | XDR SCVal Type | Description |
|---|---|---|---|
id |
u64 |
ScVal::U64 |
Proposal ID that was executed |
executor |
Address |
ScVal::Address |
Owner who triggered the execution |
to |
Address |
ScVal::Address |
Recipient of the transferred tokens |
amount |
i128 |
ScVal::I128 |
Transferred amount (see Token Amounts) |
struct ProposalExecutedEvent {
id: u64,
executor: Address,
to: Address,
amount: i128,
}