Skip to content

Alwin24/jupiter-dca-program

Repository files navigation

Jupiter-Integrated DCA Program

Lets a user automate recurring buys (dollar-cost averaging) on Solana without handing custody to a centralized service. Funds stay in a program-controlled vault the user can exit at any time. A permissionless off-chain keeper triggers each scheduled swap by CPIing into Jupiter v6.

Architecture

  • State: one Position PDA per active DCA strategy. Seeds: [b"position", owner, user_nonce]. The user_nonce lets a single owner run multiple concurrent DCA positions. The Position is the authority over its input vault (an ATA holding the deposited input token) and output vault (an ATA accumulating the output token).
  • Instructions:
    • create_dca opens a position, deposits the full input budget, and sets the schedule and the catastrophic-loss floor.
    • execute_swap is permissionless. It validates now >= last + interval, snapshots input/output vault balances, CPIs Jupiter's route instruction, then asserts input_delta == amount_per_cycle and output_delta >= min_out_per_cycle.
    • claim_output is owner-only. It drains the output vault to the owner's ATA.
    • close_dca is owner-only. It returns remaining input and output, closes the vaults, closes the position, and returns rent.

Slippage protection (no-oracle design)

v1 has no on-chain notion of fair market price. Protection works in two independent layers, and it is important not to confuse them:

  • Per-cycle fill quality is handled by the keeper, which sets Jupiter's own slippage tolerance tightly when it builds each route. Jupiter reverts any fill that misses that tolerance. This absorbs ordinary quote-vs-execution drift.
  • Catastrophic-loss protection is handled by min_out_per_cycle, an immutable floor stored on the Position and set once at creation. The program enforces it against the actual vault balance delta, computed from before/after snapshots, never from a value Jupiter or the keeper reports.

min_out_per_cycle is a disaster backstop, not a price tracker. It should be set loosely, well below expected market output, so that normal price movement across the DCA schedule never trips it. Setting it tightly would cause cycles to fail on routine volatility, which defeats the purpose of DCA (running through price movement). The cost of a loose floor is covered under "Security considerations" below.

The key property: Jupiter's slippage check is parameterised by the untrusted keeper, so it cannot be the security boundary. min_out_per_cycle is the boundary the keeper cannot influence.

Security considerations

What the program defends against:

  • PDA-owned vaults. Input and output vaults are authority-controlled by the Position PDA. Only the program can move funds out of them.
  • Owner-only instructions. claim_output and close_dca require the owner's signature and verify it against the Position.
  • Input bound. execute_swap asserts input_delta == amount_per_cycle, so a keeper cannot push more than one cycle's budget through a route, regardless of how the Jupiter instruction is constructed.
  • Measured, not trusted. The output floor is checked against the real balance delta on the vault, not a return value from the swap.
  • Atomic failure. A failed Jupiter CPI or a sub-floor fill reverts the entire transaction. The cycle counter is not decremented and no funds move, so the keeper can safely retry next block.

Out of scope in v1:

  • No on-chain price reference. Because there is no oracle, a keeper that is both malicious and the route builder can still produce a fill anywhere above min_out_per_cycle. The catastrophic floor caps the worst case; it does not guarantee a fair per-cycle price. An oracle-based guard (see roadmap) narrows this window by giving the program its own price reference, bounded by oracle confidence and staleness. It reduces the gap; it does not eliminate it.

Building

anchor build

Testing

Tests run against surfpool, a drop-in replacement for solana-test-validator that JIT-clones any account or program from mainnet on first reference. This lets the test suite CPI into the real Jupiter v6 program with real DEX state, rather than mocking it.

In one terminal:

surfpool start --rpc-url <mainnet-rpc-url>

In another:

anchor run test

Coverage includes the happy path plus the adversarial cases: early-crank rejection, last-cycle behavior, sub-floor fill rejection, double-claim, and unauthorized close.

Devnet deployment

solana config set --url devnet
anchor deploy

Devnet program ID: BJtVmmKQq1fcgbyWtU1PBbyftNdKXpz4gnCaMFpVyuBz (view on Solana Explorer).

Keeper

A keeper is any off-chain process that periodically:

  1. Lists active positions (e.g. via an indexer or getProgramAccounts).
  2. For each position whose last_execution_ts + interval_seconds <= now and cycles_remaining > 0:
    1. Calls Jupiter's /build endpoint for the position's (input_mint, output_mint, amount_per_cycle) with userPublicKey = position, setting a tight slippage tolerance.
    2. Sends execute_swap with the returned instruction data as jupiter_data.

v1 ships the program and the test suite; a production keeper is left as an integration exercise. The flow above is implemented end to end in tests/fixtures/jupiter.ts and is a fine reference.

Roadmap

  • Pyth/Switchboard price guard for a tighter, market-aware per-execution floor.
  • Keeper fee (bps cut of output) to incentivise cranking.
  • Pause / resume.
  • Top-up (deposit_more).

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors