A Soroban smart contract on Stellar implementing a trustless Dollar-Cost Averaging (DCA) vault: users deposit XLM into a personal on-chain vault, configure a recurring swap schedule, and anyone can permissionlessly trigger execution of a due, unpaused schedule's swap — without a custodian.
Dollar-cost averaging means buying a fixed amount of an asset on a fixed
schedule (e.g. weekly) instead of all at once, smoothing out entry price over
time. This contract gives every depositor their own on-chain Vault:
- Balance — XLM the user has deposited but not yet committed to a scheduled swap.
- Schedule — an optional, user-configured recurring purchase: how often
(
Daily/Weekly/Monthly), how much per execution, which asset to buy, which pool contract to swap through (pool_address), and a slippage tolerance (min_amount_out_bps). - Paused flag — lets the owner halt scheduled execution without withdrawing funds or deleting the schedule.
Anyone (not just the owner — e.g. a keeper bot) can permissionlessly trigger
a due, unpaused schedule's swap via execute_swap; the schedule's own
due/paused/balance checks gate execution, not caller identity.
| Function | Description |
|---|---|
initialize(token) |
One-time setup: records the token contract (XLM's Stellar Asset Contract) that deposits/withdrawals move. |
deposit(owner, amount) |
Owner-authorized. Transfers amount XLM from owner into the vault, increasing its balance. |
withdraw(owner, amount) |
Owner-authorized. Decreases the vault balance and transfers amount XLM back to owner. Panics if amount exceeds the balance. |
create_schedule(owner, frequency, amount_per_execution, target_asset, pool_address, min_amount_out_bps) |
Owner-authorized. Creates or replaces the owner's recurring swap schedule and emits a schedule_created event, so a backend indexer can discover the vault immediately rather than waiting for its first swap. |
pause_schedule(owner) |
Owner-authorized. Halts scheduled execution without clearing the schedule. |
resume_schedule(owner) |
Owner-authorized. Re-enables a paused schedule. |
get_vault(owner) |
Read-only. Returns the owner's Vault, or panics if none exists. |
execute_swap(owner) -> i128 |
Permissionless. Executes one due, unpaused schedule's swap through its configured pool, updates the vault's balance/ledgers, emits a swap event, and returns the amount received. Panics if the schedule is missing, paused, not yet due, or the balance is insufficient. |
Soroban contracts cannot interact with the classic Stellar SDEX — there is no
host function for it. Scheduled swaps therefore go through a
contract-to-contract call into an AMM/liquidity-pool-style contract instead.
execute_swap calls a small internal SwapPool trait rather than a fixed
Soroban #[contractclient], so different pool integrations can be swapped in
later; today there's one implementation, GenericPoolAdapter, which targets
pools exposing a generic swap(to, token_in, token_out, amount_in, min_amount_out) -> i128 entrypoint (every transfer in that flow is a direct,
self-authorizing call by whichever contract currently holds the funds being
moved, so no deeper cross-contract authorization plumbing is needed). See the
doc comment on GenericPoolAdapter in contracts/dca-vault/src/lib.rs for
why this doesn't reuse soroban-examples' liquidity_pool interface directly
(its fixed two-asset, exact-output design doesn't fit a generic vault).
cargo test
cargo build --target wasm32v1-none --releaseNote: wasm32-unknown-unknown is not supported by soroban-sdk 26.1.0 on
Rust 1.82+; use wasm32v1-none instead.
The test suite has 14 tests covering deposit/withdraw accounting, schedule
lifecycle (create / pause / resume), the schedule_created event, get_vault
edge cases, all execute_swap guard paths (not-due, paused, insufficient
balance), permissionless invocation, and atomicity (pool failure reverts the
prior token push). See CONTRIBUTING.md for the full list.
Redeployed 2026-08-03 (reserve-based price quote in execute_swap; see
context.md for the full history, including the prior contract ID
CDJF7V5NLGKAV7RHTBCR3LMHC7MUS7IWL6KYSLO6ZWEEJYJGWUVGEDEO, deployed
2026-07-01, now orphaned but still viewable on Stellar Expert).
| Contract ID | CDUJQQ742DH36VLLUQ4PGIQFVIV6OU4EX7HYK5STV5VUUQJIFK7BLQYC |
| Network | Stellar Testnet |
| XLM token (SAC) | CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC |
| Explorer | stellar.expert/explorer/testnet |
initialize was called at deployment with the XLM SAC address above. The
contract is ready to accept deposit and create_schedule calls; swap
execution requires a pool contract also deployed on testnet — see the demo
pool below.
To let execute_swap be verified against a real, callable pool instead of
only the #[cfg(test)] MockPool, a minimal demo AMM pool
(contracts/demo-pool) is deployed on Stellar Testnet. This is a
testnet-only demo counterparty for end-to-end verification — a
deliberately simple, fixed 1:1-rate pool, not a production AMM
integration. See the doc comment at the top of
contracts/demo-pool/src/lib.rs for the full scope note.
| Contract ID | CDZY4VCY2HWX43GT3EAMATC3JZTEASFS7LU73XVMNZFXF7UGU42ZV6AC |
| Network | Stellar Testnet |
| token_a | XLM SAC — CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC |
| token_b | TESTUSD SAC — CCZJ4RREF7QFBYSDQQFFMZ4WAB3QEPULS2TQUA7IRYYX2HFUHZKOR24W |
| TESTUSD issuer | GANTM4FGB37EAQYTJC34DSLVCYQAYDL7XSY2HVX3GKAHESCHZTGNDD2Z |
| Explorer | stellar.expert/explorer/testnet |
Funded with 500 XLM and 500 TESTUSD of liquidity, verified via get_balance.
See CONTRIBUTING.md for prerequisites, branch naming, commit style, the PR checklist, and Drips Wave rules.