Complete reference for the VaultDAO Soroban smart contract public surface.
Last Updated: March 2026
Status: Aligned with contract implementation in contracts/vault/src/lib.rs
- Initialization
- Proposal Management
- Voting & Execution
- Role & Access Control
- Configuration Management
- Spending Limits & Constraints
- Recurring & Streaming Payments
- Recipient List Management
- Comments & Collaboration
- Audit Trail
- Batch Operations
- Metadata & Tags
- Attachments
- Insurance & Staking
- Dynamic Fees
- View Functions
- Error Codes
Initialize the vault with core configuration. Can only be called once.
Parameters:
admin- Initial administrator address (must authorize)config- Initialization configuration with signers, threshold, limits
Returns: Ok(()) on success
Errors:
AlreadyInitialized- Contract already initializedNoSigners- Signers list is emptyThresholdTooLow- Threshold < 1ThresholdTooHigh- Threshold > signers.len()QuorumTooHigh- Quorum > signers.len()InvalidAmount- Limits are non-positive
Example:
initialize(
admin,
InitConfig {
signers: vec![signer1, signer2, signer3],
threshold: 2,
quorum: 0, // 0 = disabled
spending_limit: 1000e7,
daily_limit: 5000e7,
weekly_limit: 10000e7,
timelock_threshold: 500e7,
timelock_delay: 17280, // ~1 day
velocity_limit: 10, // max proposals per ledger
threshold_strategy: ThresholdStrategy::Absolute,
pre_execution_hooks: vec![],
post_execution_hooks: vec![],
default_voting_deadline: 120960, // ~7 days
veto_addresses: vec![],
retry_config: RetryConfig { enabled: false, .. },
recovery_config: RecoveryConfig { .. },
staking_config: StakingConfig { enabled: false, .. },
}
)Create a single transfer proposal.
Parameters:
proposer: Address- Must have Treasurer or Admin rolerecipient: Address- Destination addresstoken_addr: Address- Token contract IDamount: i128- Transfer amount (stroops)memo: Symbol- Descriptive label (≤32 chars)priority: Priority- Low/Normal/High/Criticalconditions: Vec<Condition>- Optional execution conditionscondition_logic: ConditionLogic- And/Or logicinsurance_amount: i128- Proposer's insurance stake (0 = none)
Returns: Proposal ID (u64)
Errors:
InsufficientRole- Proposer lacks Treasurer/Admin roleInvalidAmount- Amount ≤ 0ExceedsProposalLimit- Amount > spending_limitExceedsDailyLimit- Daily aggregate exceededExceedsWeeklyLimit- Weekly aggregate exceededVelocityLimitExceeded- Too many proposals in current ledgerInsuranceInsufficient- Insurance below required minimumRecipientNotWhitelisted- Recipient not on whitelist (if enabled)RecipientBlacklisted- Recipient on blacklist (if enabled)
Create a transfer proposal with scheduled execution time.
Additional Parameters:
execution_time: u64- Ledger at which execution is allowed
Returns: Proposal ID
Errors: Same as propose_transfer plus validation of execution_time
Create a transfer proposal with prerequisite dependencies.
Additional Parameters:
depends_on: Vec<u64>- Proposal IDs that must execute first
Returns: Proposal ID
Errors: Same as propose_transfer plus:
- Circular dependency detection
- Non-existent dependency validation
Create multiple transfer proposals in one call (multi-token support).
Parameters:
proposer: Address- Must authorizetransfers: Vec<TransferDetails>- Vector of (recipient, token, amount, memo)priority: Priority- Applied to all proposalsconditions: Vec<Condition>- Applied to all proposalscondition_logic: ConditionLogic- Applied to all proposalsinsurance_amount: i128- Total insurance across batch
Returns: Vector of proposal IDs
Errors:
BatchTooLarge- More than 10 proposals- Same as
propose_transferfor aggregate limits
amend_proposal(proposer: Address, proposal_id: u64, new_recipient: Address, new_amount: i128, new_memo: Symbol) -> Result<(), VaultError>
Modify a pending proposal (proposer only).
Behavior:
- Resets all approvals and abstentions
- Records amendment in audit trail
- Recalculates timelock if amount changed
Errors:
Unauthorized- Caller is not proposerProposalNotPending- Proposal not in Pending stateInvalidAmount- New amount ≤ 0- Same limit checks as propose_transfer
Retrieve amendment history for a proposal.
Returns: Vector of amendments with timestamp, old/new values
Cast an approval vote on a pending proposal.
Behavior:
- Requires signer authorization
- Signer must be in config.signers list
- Supports delegation (vote recorded under effective voter)
- Transitions to Approved when threshold + quorum satisfied
Errors:
NotASigner- Caller not in signers listProposalNotPending- Proposal not in Pending stateAlreadyApproved- Signer already votedProposalExpired- Voting deadline passed
Record explicit abstention (counts toward quorum, not threshold).
Behavior:
- Signer must be in signers list
- Counts toward quorum requirement
- Does not count toward approval threshold
Errors: Same as approve_proposal
Execute an approved proposal, transferring funds.
Checks:
- Proposal status is Approved
- Timelock expired (if applicable)
- All dependencies executed
- Conditions satisfied
- Sufficient vault balance
- Gas limit not exceeded
Behavior:
- Transfers amount to recipient
- Returns insurance to proposer (if staked)
- Refunds stake to proposer (if locked)
- Updates reputation scores
- Records in audit trail
Errors:
ProposalNotApproved- Threshold not metProposalAlreadyExecuted- Already executedTimelockNotExpired- Unlock ledger not reachedInsufficientBalance- Vault balance too lowProposalExpired- Proposal lifetime exceededConditionsNotSatisfied- Execution conditions failedDependenciesNotExecuted- Prerequisites not complete
Veto an approved proposal (authorized vetoers only).
Behavior:
- Moves proposal to Vetoed status
- Removes from priority queue
- Blocks execution permanently
Errors:
Unauthorized- Caller not in veto_addressesProposalNotApproved- Proposal not in Approved state
Cancel a pending proposal with refunds.
Permissions:
- Proposer can always cancel
- Admin can cancel any proposal
Behavior:
- Returns insurance to proposer
- Returns stake to proposer
- Reverses spending reservations
- Records cancellation with reason
Errors:
Unauthorized- Caller not proposer/adminProposalNotPending- Proposal not in Pending state
Retrieve cancellation details for a cancelled proposal.
Returns: CancellationRecord with canceller, reason, timestamp, refunds
Get list of all cancelled proposal IDs.
Get retry attempt state for a proposal (if retry enabled).
Assign a role to an address (admin only).
Parameters:
admin- Must have Admin roletarget- Address to assign role torole- Member (0) / Treasurer (1) / Admin (2)
Errors:
Unauthorized- Caller not Admin
Query the role of an address (read-only).
Returns: Role enum value (default: Member)
Get all role assignments for dashboard/admin views.
Returns: Vector of (address, role) pairs
Change the M-of-N approval threshold (admin only).
Constraints:
- 1 ≤ threshold ≤ signers.len()
Errors:
Unauthorized- Caller not AdminThresholdTooLow- threshold < 1ThresholdTooHigh- threshold > signers.len()
update_limits(admin: Address, spending_limit: i128, daily_limit: i128, weekly_limit: i128) -> Result<(), VaultError>
Update spending caps (admin only).
Constraints:
- All values > 0
- spending_limit ≤ daily_limit ≤ weekly_limit
Errors:
Unauthorized- Caller not AdminInvalidAmount- Constraint violated
Set quorum requirement (admin only).
Constraints:
- 0 ≤ quorum ≤ signers.len()
- 0 = disabled
Errors:
Unauthorized- Caller not AdminQuorumTooHigh- quorum > signers.len()
Change voting strategy (admin only).
Strategies:
- Simple - Threshold only
- Weighted - Reputation-weighted votes
- Tiered - Role-based thresholds
extend_voting_deadline(admin: Address, proposal_id: u64, new_deadline: u64) -> Result<(), VaultError>
Extend voting window for a proposal (admin only).
Get today's aggregate spending (read-only).
Get spending for a specific day (read-only).
Get current vault configuration (read-only).
Returns: Full Config struct with all parameters
Get current signer list (read-only).
Check if address is a signer (read-only).
schedule_payment(proposer: Address, recipient: Address, token_addr: Address, amount: i128, memo: Symbol, interval: u64) -> Result<u64, VaultError>
Schedule a recurring automatic payment.
Constraints:
- interval ≥ 720 ledgers (~1 hour)
- Proposer must have Treasurer/Admin role
Returns: Payment ID
Errors:
InsufficientRole- Proposer lacks permissionInvalidAmount- Amount ≤ 0IntervalTooShort- interval < 720
Execute a due recurring payment (anyone can call).
Checks:
- Payment is due (current_ledger ≥ next_execution_ledger)
- Sufficient vault balance
- Daily/weekly limits not exceeded
Errors:
TimelockNotExpired- Not yet dueExceedsDailyLimit- Daily cap exceededInsufficientBalance- Vault balance too low
Fetch a recurring payment by ID (read-only).
Paginated list of recurring payment IDs (capped at 100).
Paginated list of recurring payments (capped at 50).
create_stream(sender: Address, recipient: Address, token_addr: Address, amount: i128, duration: u64) -> Result<u64, VaultError>
Create a token stream (continuous payment over time).
Parameters:
duration- Stream duration in ledgers- Funds transferred to escrow immediately
Returns: Stream ID
Errors:
InvalidAmount- amount ≤ 0 or duration = 0
Set recipient list mode (admin only).
Modes:
- Disabled - No restrictions
- Whitelist - Only whitelisted recipients allowed
- Blacklist - Blacklisted recipients blocked
Get current recipient list mode (read-only).
Add address to whitelist (admin only).
Errors:
Unauthorized- Caller not AdminAddressAlreadyOnList- Address already whitelisted
Remove address from whitelist (admin only).
Check if address is whitelisted (read-only).
Add address to blacklist (admin only).
Remove address from blacklist (admin only).
Check if address is blacklisted (read-only).
add_comment(author: Address, proposal_id: u64, text: Symbol, parent_id: u64) -> Result<u64, VaultError>
Add a comment to a proposal.
Parameters:
parent_id- 0 for top-level, or ID of parent comment for threading
Returns: Comment ID
Errors:
ProposalNotFound- Proposal doesn't exist- Parent comment validation if parent_id > 0
Edit a comment (author only).
Errors:
Unauthorized- Caller not comment author
Get all comments for a proposal (read-only).
Get a single comment by ID (read-only).
Retrieve an audit entry by ID (read-only).
Returns: AuditEntry with action, actor, timestamp, hash chain
Get total number of audit entries (read-only).
Verify audit trail integrity via hash chain (read-only).
Returns: true if chain is valid, false if tampering detected
batch_execute_proposals(executor: Address, proposal_ids: Vec<u64>) -> Result<(Vec<u64>, u32), VaultError>
Execute multiple approved proposals in one transaction.
Behavior:
- Skips proposals that fail validation
- Gas-optimized single TTL extension
- Returns (executed_ids, failed_count)
Errors:
Unauthorized- Caller not authorized
set_proposal_metadata(caller: Address, proposal_id: u64, key: Symbol, value: String) -> Result<(), VaultError>
Set metadata key-value for a proposal (proposer/admin only).
Constraints:
- Max 16 metadata entries per proposal
- Value length: 1-256 chars
Errors:
Unauthorized- Caller not proposer/adminMetadataValueInvalid- Value length invalidExceedsProposalLimit- Too many entries
Remove metadata key from proposal (proposer/admin only).
Get single metadata value (read-only).
Get full metadata map (read-only).
Add tag to proposal (proposer/admin only).
Constraints:
- Max 10 tags per proposal
Errors:
Unauthorized- Caller not proposer/adminTooManyTags- Exceeds limit
Remove tag from proposal (proposer/admin only).
Get all tags for proposal (read-only).
Get proposal IDs with specific tag (read-only).
Add IPFS attachment hash to proposal (proposer/admin only).
Constraints:
- CID length: 46-128 chars (CIDv0/v1 support)
- Max 10 attachments per proposal
Errors:
Unauthorized- Caller not proposer/adminAttachmentHashInvalid- CID length invalidTooManyAttachments- Exceeds limit
Remove attachment by index (proposer/admin only).
Update insurance configuration (admin only).
Parameters:
enabled- Enable/disable insurance requirementmin_amount- Minimum proposal amount triggering insurancemin_insurance_bps- Basis points of proposal amount required
Get current insurance configuration (read-only).
Get slashed insurance balance for token (read-only).
withdraw_insurance_pool(admin: Address, token: Address, recipient: Address, amount: i128) -> Result<(), VaultError>
Withdraw slashed insurance funds (admin only).
Update staking configuration (admin only).
withdraw_stake_pool(admin: Address, token: Address, recipient: Address, amount: i128) -> Result<(), VaultError>
Withdraw slashed stake funds (admin only).
Configure dynamic fee structure (admin only).
Constraints:
- base_fee_bps ≤ 10,000
- Tiers sorted by min_volume
- reputation_discount_percentage ≤ 100
Get current fee structure (read-only).
Calculate fee for transaction without collecting (read-only).
Fetch proposal by ID (read-only).
Returns: Full Proposal struct with all fields
Paginated proposal IDs (capped at 100).
Paginated full proposals (capped at 50).
Get current voting strategy (read-only).
Get quorum status for proposal (read-only).
Returns: (current_votes, required_quorum, quorum_reached)
Get proposal IDs currently executable (read-only).
Checks:
- Status is Approved
- Not expired
- Timelock elapsed
- Dependencies satisfied
change_priority(caller: Address, proposal_id: u64, new_priority: Priority) -> Result<(), VaultError>
Change priority of pending proposal (proposer/admin only).
Get proposal IDs filtered by priority (read-only).
| Code | Name | Description |
|---|---|---|
| 100 | AlreadyInitialized |
Contract already initialized |
| 101 | NotInitialized |
Contract not yet initialized |
| 200 | Unauthorized |
Caller not authorized |
| 201 | NotASigner |
Address not in signers list |
| 202 | InsufficientRole |
Role too low for action |
| 300 | ProposalNotFound |
Proposal ID doesn't exist |
| 301 | ProposalNotPending |
Proposal not in Pending state |
| 302 | AlreadyApproved |
Signer already voted / duplicate item |
| 303 | ProposalExpired |
Proposal lifetime exceeded |
| 304 | ProposalNotApproved |
Threshold not met |
| 305 | ProposalAlreadyExecuted |
Proposal already executed |
| 400 | ExceedsProposalLimit |
Amount > per-proposal limit |
| 401 | ExceedsDailyLimit |
Daily cap exceeded |
| 402 | ExceedsWeeklyLimit |
Weekly cap exceeded |
| 403 | InvalidAmount |
Amount ≤ 0 or invalid value |
| 404 | TimelockNotExpired |
Timelock still active |
| 405 | IntervalTooShort |
Recurring interval < 720 ledgers |
| 500 | ThresholdTooLow |
Threshold < 1 |
| 501 | ThresholdTooHigh |
Threshold > signers.len() |
| 502 | SignerAlreadyExists |
Address already a signer |
| 503 | SignerNotFound |
Address not in signers list |
| 504 | CannotRemoveSigner |
Removal breaks threshold |
| 505 | NoSigners |
Empty signers list |
| 600 | TransferFailed |
Token transfer failed |
| 601 | InsufficientBalance |
Vault balance too low |
| 602 | RecipientNotWhitelisted |
Recipient not on whitelist |
| 603 | RecipientBlacklisted |
Recipient on blacklist |
| 604 | AddressAlreadyOnList |
Address already on list |
| 605 | AddressNotOnList |
Address not on list |
| 606 | VelocityLimitExceeded |
Too many proposals in ledger |
| 607 | InsuranceInsufficient |
Insurance below minimum |
| 608 | BatchTooLarge |
Batch > 10 proposals |
| 609 | AttachmentHashInvalid |
CID length invalid |
| 610 | TooManyAttachments |
> 10 attachments |
| 611 | TooManyTags |
> 10 tags |
| 612 | MetadataValueInvalid |
Metadata value length invalid |
| 613 | QuorumTooHigh |
Quorum > signers.len() |
| 614 | ConditionsNotSatisfied |
Execution conditions failed |
| 615 | DependenciesNotExecuted |
Prerequisites not complete |
- All write functions require caller authorization via
require_auth() - Read-only functions have no authorization requirement
- Pagination limits: 100 for IDs, 50 for full objects
- Timelock is calculated in ledgers (~5 seconds per ledger)
- Reputation scores decay over time and affect spending limits
- Insurance and staking are optional features (configurable)
For a full gap analysis between the contract API and the frontend hook, see INTEGRATION_CHECKLIST.md.
The TypeScript SDK (sdk/src/contract.ts) wraps a subset of these contract functions. Not all contract methods are exposed through the SDK yet. For direct contract calls, use Soroban SDK directly.
Commonly Wrapped Functions:
initialize()propose_transfer()approve_proposal()execute_proposal()cancel_proposal()(mapped asrejectProposalin SDK)set_role()update_limits()schedule_payment()execute_recurring_payment()get_proposal()get_role()get_today_spent()is_signer()
Advanced Functions (Direct Contract Calls Only):
- Batch operations
- Proposal amendments
- Delegation
- Veto
- Comments
- Audit trail verification
- Metadata/tags/attachments
- Insurance/staking configuration
- Dynamic fees
The React frontend uses the SDK for browser-based signing with Freighter. For advanced features not wrapped by the SDK, build transactions directly using soroban-sdk and sign with Freighter.
Node.js keeper bots can call contract functions directly using soroban-sdk with a keypair for signing. No Freighter required.
Proposals and approvals affect reputation scores:
- Successful execution: +10 for proposer, +5 for approvers
- Rejection: -20 for proposer
- Approval: +2 per approval
High reputation (750+) unlocks:
- 1.5x daily/weekly limits
- 50% insurance discount
- Reduced staking requirements
If amount >= timelock_threshold, execution is blocked until current_ledger >= unlock_ledger. The unlock ledger is calculated as:
unlock_ledger = creation_ledger + timelock_delay
Proposals can depend on other proposals. Dependencies are validated at creation time:
- Circular dependencies rejected
- Non-existent dependencies rejected
- Execution blocked until all dependencies executed
Proposals can include execution conditions (e.g., price oracles, time windows). Conditions are evaluated at execution time using the specified logic (And/Or).
If gas configuration is enabled, each proposal has a gas limit. Execution fails if estimated fee exceeds the limit.
Every action is recorded in an immutable audit trail with hash chain verification. Use verify_audit_trail() to detect tampering.
batch_execute_proposals() executes up to 10 proposals in one transaction. Failed proposals are skipped; returns count of successes and failures.
delegate_voting_power() and revoke_delegation() are currently stubbed and return Unauthorized. Full implementation pending.
If enabled, recovery configuration allows designated addresses to recover funds in emergency scenarios.
If retry configuration is enabled, failed executions can be retried. Use get_retry_state() to check retry attempts.
Retrieve vault-wide performance metrics accumulated since initialization.
Returns: VaultMetrics struct with the following fields:
| Field | Type | Description |
|---|---|---|
total_proposals |
u64 | Total proposals ever created |
executed_count |
u64 | Successfully executed proposals |
rejected_count |
u64 | Rejected proposals |
expired_count |
u64 | Proposals that expired without execution |
total_execution_time_ledgers |
u64 | Cumulative ledgers from creation to execution |
total_gas_used |
u64 | Total gas consumed across all executions |
last_updated_ledger |
u64 | Ledger sequence when metrics were last updated |
Derived Metrics (Methods):
-
success_rate_bps() -> u32- Success rate in basis points (0-10000 = 0-100%)- Formula:
(executed_count * 10_000) / (executed_count + rejected_count + expired_count) - Returns 0 if no proposals have been finalized
- Formula:
-
avg_execution_time_ledgers() -> u64- Average ledgers from creation to execution- Formula:
total_execution_time_ledgers / executed_count - Returns 0 if no proposals have been executed
- Formula:
Behavior:
- Metrics are cumulative and never reset
- Updated atomically on proposal creation, execution, rejection, and expiration
- Thread-safe: uses instance storage with atomic updates
- Returns default metrics (all zeros) if no proposals have been created
Units & Scaling:
- Ledger times: Soroban ledger sequence numbers (1 ledger ≈ 5 seconds)
- Gas units: Soroban gas units (varies by operation)
- Basis points: 0-10000 (0-100%), where 100 bps = 1%
Example:
let metrics = vault.get_metrics();
println!("Success rate: {}%", metrics.success_rate_bps() / 100);
println!("Avg execution time: {} ledgers", metrics.avg_execution_time_ledgers());
println!("Total proposals: {}", metrics.total_proposals);Calculate the total USD valuation of the vault's holdings across multiple assets.
Parameters:
assets- Vector of token contract addresses to include in valuation
Returns:
Ok(total_usd)- Total portfolio value in USD (scaled by 10^7 for precision)Err(VaultError)- If valuation cannot be determined
Behavior:
- Empty asset list returns
Ok(0)without error - Skips assets with zero balance (no oracle query needed)
- Uses saturating arithmetic to prevent overflow
- Queries oracle for current price of each asset with non-zero balance
- Returns error if any asset price cannot be determined or is stale
Units & Scaling:
| Component | Unit | Scaling | Example |
|---|---|---|---|
| Asset balance | stroops | 10^-7 (7 decimals) | 1,000,000 stroops = 0.1 token |
| Oracle price | USD per token | 10^7 (scaled) | Price 1500 = $150.00 |
| Output value | USD cents | 10^7 (scaled) | 1,500,000,000 = $150.00 |
Conversion Formula:
usd_value = (balance * price) / 10_000_000
Error Cases:
NotInitialized- Oracle not configuredInvalidAmount- Asset price not found in oracleRetryError- Asset price data is stale (exceedsmax_staleness)
Example:
let assets = vec![usdc_address, xlm_address, btc_address];
match vault.get_portfolio_valuation(&assets) {
Ok(total_usd) => {
let usd_value = total_usd as f64 / 10_000_000.0;
println!("Portfolio value: ${:.2}", usd_value);
}
Err(e) => println!("Valuation error: {:?}", e),
}Oracle Configuration:
Portfolio valuation requires oracle configuration via update_oracle_config():
vault.update_oracle_config(&admin, &VaultOracleConfig {
address: oracle_contract_address,
max_staleness: 1000, // max ledgers before price is considered stale
});The oracle contract must implement the lastprice(asset: Address) -> Option<VaultPriceData> interface.
- Consistency: Metrics are updated atomically with proposal state changes
- Completeness: All proposal lifecycle events are tracked
- Predictability: Metrics only increase (never decrease)
- Precision: Uses u64 for all counters (no overflow risk for reasonable vault lifetimes)
- Consistency: Valuation reflects current vault balances and oracle prices
- Predictability: Empty asset list always returns 0; zero-balance assets are skipped
- Precision: Uses i128 with saturating arithmetic to prevent overflow
- Staleness: Validates oracle price freshness against configured
max_staleness
Metrics:
- No proposals created: All counters are 0, success_rate_bps() returns 0
- Only rejected/expired proposals: success_rate_bps() returns 0
- Single executed proposal: success_rate_bps() returns 10000 (100%)
Portfolio Valuation:
- Empty asset list: Returns Ok(0) immediately
- All assets have zero balance: Returns Ok(0) without oracle queries
- Mixed zero/non-zero balances: Only queries oracle for non-zero assets
- Very large balances: Saturating arithmetic prevents overflow
- Oracle not configured: Returns NotInitialized error
- Stale price data: Returns RetryError; client should retry after staleness window