Skip to content

Swap checked against an oracle ​

Jupiter · Pyth

Status: Tested locally in LiteSVM against Jupiter, Meteora and Pyth programs and accounts copied from mainnet; not yet run on devnet or mainnet.

Cost: Ballista's own work took 9,770 of the tested transaction's 82,854compute units; the protocols took the rest. Ballista charges no fee; see what it costs.

What it does ​

Sells SOL for USDC through Jupiter, and reverts unless the swap paid at least what Pyth's SOL/USD price says the SOL was worth, less 1%.

Jupiter's slippageBps limits how far the swap can fall short of the quote Jupiter produced. It doesn't help when the quote itself is the problem, such as a bad quote or a manipulated pool inside the route. An independent price catches those.

The template requires, in order:

  • the Pyth price account's verification level to be Full (priceIsFullyVerified), because the level decides where every other field sits (see reading offsets);
  • the account to hold SOL/USD's feed, FEED_ID (priceIsTheExpectedFeed). Pyth's receiver owns every feed's price account, so otherwise USDC/USD's price would pass for SOL/USD's;
  • the price to be at most 60 seconds old (oracleIsFresh);
  • the source to hold wrapped SOL and the destination USDC, the pair the feed prices, read from mint accounts pinned to those two (sourceHoldsTheSourceMint, destinationHoldsTheDestinationMint);
  • both token accounts to belong to the trader (sellsTheTradersOwnTokens, proceedsGoToTheTrader), since a route's step can pay any account of the output mint;
  • the price to be above zero (oraclePriceIsPositive);
  • the route's platformFeeBps to be at most MAX_PLATFORM_FEE_BPS, a constant that is 0 (platformFeeWithinCap), before Jupiter is called, since whoever builds the run picks the fee account and rate;
  • after the swap, exactly inAmount to have left the source (soldTheRouteInput), since Jupiter doesn't require its steps to move the source account it is given;
  • the destination to have received at least that amount's value at the Pyth price, less TOLERANCE_BPS, 100 basis points or 1% (fillBeatTheOracle).

The feed, the pair and the tolerance are constants, so whoever builds the run can't change them. For another pair or tolerance, change them and upload your own template.

It does not guard against:

  • Venues' own fee accounts. The cap covers the route's platform fee, not fees a venue takes inside the route. The fill check bounds those to TOLERANCE_BPS, so set it to what you would accept losing to the builder, not only to the market.
  • A price the publishers disagree on. The template doesn't read Pyth's confidence interval. Act only on a fresh price shows the check.
  • The choice of price within the last minute. Whoever posts the price update can pick any Pyth price from the last 60 seconds, such as the lowest.
  • Spending the trader's other token accounts. The trader signs route, and Jupiter passes that authority to every step.

Template ​

ts
import {
  TOKEN_PROGRAM_ADDRESS_BYTES,
  account,
  compileTemplate,
  data,
  defineTemplate,
  expression,
  step,
} from '@jac0xb/ballista';
import {
  JUPITER_ROUTE,
  JUPITER_V6,
  PYTH,
  PYTH_RECEIVER,
  SPL_MINT,
  TOKEN_ACCOUNT_AMOUNT_OFFSET,
  TOKEN_ACCOUNT_LENGTH,
  TOKEN_ACCOUNT_MINT_OFFSET,
  TOKEN_ACCOUNT_OWNER_OFFSET,
  USDC_MINT,
  WRAPPED_SOL_MINT,
  addressBytes,
  pythFeedId,
} from './shared.js';

const balanceOf = (name: string) =>
  expression.accountData(account.fixed(name), TOKEN_ACCOUNT_AMOUNT_OFFSET, 'u64');

/** The route's platform fee account and rate are chosen by whoever builds the run: cap the rate. */
export const MAX_PLATFORM_FEE_BPS = 0n;

/** The Pyth feed the price must come from: SOL/USD, which prices the SOL sold in the USDC bought. */
export const FEED_ID = pythFeedId('ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d');

/** 1%, in basis points: how far below the oracle's valuation the fill may land. */
export const TOLERANCE_BPS = 100n;

export const jupiterOracleCheckedSwap = defineTemplate({
  inputs: {
    /** `route_plan` as the Swap API encoded it: the bytes between the discriminator and `in_amount`. */
    routePlan: { type: 'bytes', maxLength: 512 },
    /** The route's `in_amount`: what the route sells, and exactly what must leave `sourceAta`. */
    inAmount: { type: 'u64' },
    /** The quote's `quoted_out_amount`. */
    quotedOutAmount: { type: 'u64' },
    /** The quote's `slippage_bps`. */
    slippageBps: { type: 'u64' },
    /** The quote's `platform_fee_bps`. */
    platformFeeBps: { type: 'u64' },
  },
  accounts: {
    jupiter: { executable: true, address: addressBytes(JUPITER_V6) },
    tokenProgram: { executable: true, address: TOKEN_PROGRAM_ADDRESS_BYTES },
    priceUpdate: { owner: addressBytes(PYTH_RECEIVER), minDataLength: PYTH.length },
    trader: { signer: true, writable: true },
    sourceAta: {
      writable: true,
      owner: TOKEN_PROGRAM_ADDRESS_BYTES,
      minDataLength: TOKEN_ACCOUNT_LENGTH,
    },
    destinationAta: {
      writable: true,
      owner: TOKEN_PROGRAM_ADDRESS_BYTES,
      minDataLength: TOKEN_ACCOUNT_LENGTH,
    },
    // The pair `FEED_ID` prices: what the route sells, and what it buys.
    sourceMint: {
      address: addressBytes(WRAPPED_SOL_MINT),
      owner: TOKEN_PROGRAM_ADDRESS_BYTES,
      minDataLength: SPL_MINT.length,
    },
    destinationMint: {
      address: addressBytes(USDC_MINT),
      owner: TOKEN_PROGRAM_ADDRESS_BYTES,
      minDataLength: SPL_MINT.length,
    },
  },
  accountGroups: ['routeAccounts'],
  steps: [
    // Pin the verification level first: it decides where every other field sits.
    step.require(
      expression.equal(
        expression.accountData(account.fixed('priceUpdate'), PYTH.verificationLevel, 'u8'),
        expression.u64(PYTH.verificationLevelFull),
      ),
      'priceIsFullyVerified',
    ),

    // The owner pin takes any feed's price; the feed id is what says which one this is.
    step.require(
      expression.equal(
        expression.accountData(account.fixed('priceUpdate'), PYTH.feedId, 'pubkey'),
        expression.pubkey(FEED_ID),
      ),
      'priceIsTheExpectedFeed',
    ),

    step.require(
      expression.lessThanOrEqual(
        expression.subtract(
          expression.clockUnixTimestamp(),
          expression.accountData(account.fixed('priceUpdate'), PYTH.publishTime, 'i64'),
        ),
        expression.i64(60),
      ),
      'oracleIsFresh',
    ),

    // Each token account must hold the mint whose decimals scale it.
    step.require(
      expression.equal(
        expression.accountData(account.fixed('sourceAta'), TOKEN_ACCOUNT_MINT_OFFSET, 'pubkey'),
        expression.accountField(account.fixed('sourceMint'), 'key'),
      ),
      'sourceHoldsTheSourceMint',
    ),
    step.require(
      expression.equal(
        expression.accountData(account.fixed('destinationAta'), TOKEN_ACCOUNT_MINT_OFFSET, 'pubkey'),
        expression.accountField(account.fixed('destinationMint'), 'key'),
      ),
      'destinationHoldsTheDestinationMint',
    ),

    // Both ends of the swap are the trader's: the step that pays the fill can name any account of
    // the destination mint, so the one measured must be the trader's. The key is read once, into a
    // register both checks share: without register reuse, the template uses 62 of the runtime's 64.
    step.let('traderKey', expression.accountKey('trader')),
    step.require(
      expression.equal(
        expression.accountData(account.fixed('sourceAta'), TOKEN_ACCOUNT_OWNER_OFFSET, 'pubkey'),
        expression.variable('traderKey'),
      ),
      'sellsTheTradersOwnTokens',
    ),
    step.require(
      expression.equal(
        expression.accountData(account.fixed('destinationAta'), TOKEN_ACCOUNT_OWNER_OFFSET, 'pubkey'),
        expression.variable('traderKey'),
      ),
      'proceedsGoToTheTrader',
    ),

    // price × 10^exponent is per whole token. In base units the fill is worth
    // sold × price × 10^(destinationDecimals + exponent − sourceDecimals). The exponent is an
    // i32 and usually negative, so split the power into a multiplier and a divisor, each ≥ 0,
    // and let multiplyDivide apply both exactly.
    step.let(
      'scale',
      expression.subtract(
        expression.add(
          expression.cast('i64', expression.accountData(account.fixed('destinationMint'), SPL_MINT.decimals, 'u8')),
          expression.accountData(account.fixed('priceUpdate'), PYTH.exponent, 'i32'),
        ),
        expression.cast('i64', expression.accountData(account.fixed('sourceMint'), SPL_MINT.decimals, 'u8')),
      ),
      'computeDecimalScale',
    ),

    // Pyth prices are signed; a negative or zero price means the feed is unusable here.
    step.let(
      'oraclePrice',
      expression.accountData(account.fixed('priceUpdate'), PYTH.price, 'i64'),
      'readOraclePrice',
    ),
    step.require(
      expression.greaterThan(expression.variable('oraclePrice'), expression.i64(0)),
      'oraclePriceIsPositive',
    ),

    step.snapshot('sourceBefore', balanceOf('sourceAta'), 'readSourceBeforeSwap'),
    step.snapshot('balanceBefore', balanceOf('destinationAta'), 'readBalanceBeforeSwap'),

    // The fee account sits in the route's own accounts: any nonzero rate pays whoever chose it.
    step.require(
      expression.lessThanOrEqual(expression.input('platformFeeBps'), expression.u64(MAX_PLATFORM_FEE_BPS)),
      'platformFeeWithinCap',
    ),

    // `route` takes the token program, the signer, and the user's source and destination token
    // accounts first; the route's own accounts follow as the group. Jupiter moves the accounts its
    // steps name, not necessarily these two, which is why `soldTheRouteInput` below exists.
    step.invoke({
      program: account.fixed('jupiter'),
      accounts: [
        { account: account.fixed('tokenProgram'), signer: false, writable: false },
        { account: account.fixed('trader'), signer: true, writable: false },
        { account: account.fixed('sourceAta'), signer: false, writable: true },
        { account: account.fixed('destinationAta'), signer: false, writable: true },
      ],
      accountGroup: 'routeAccounts',
      data: [
        data.literal(JUPITER_ROUTE),
        data.encode('bytes', expression.input('routePlan')),
        data.encode('u64', expression.input('inAmount')),
        data.encode('u64', expression.input('quotedOutAmount')),
        data.encode('u16', expression.input('slippageBps')),
        data.encode('u8', expression.input('platformFeeBps')),
      ],
      label: 'swap',
    }),

    // What actually left, whatever the route data claimed it would sell.
    step.let(
      'sold',
      expression.subtract(expression.snapshot('sourceBefore'), balanceOf('sourceAta')),
      'measureAmountSold',
    ),

    // The route sold `inAmount`, so that much must have left the account measured. If the steps
    // moved other accounts, `sold` is 0 and so is the floor below, which any fill would clear.
    step.require(
      expression.equal(expression.variable('sold'), expression.input('inAmount')),
      'soldTheRouteInput',
    ),

    // sold × price, scaled by 10^scale, is the fill at the oracle price in destination base
    // units; multiplyDivide computes the exact product and applies it, then the floor keeps all but
    // `TOLERANCE_BPS` of it. sold × price fits u128 because both factors are below 2^64. Using max
    // with zero means at least one of the two powers of ten below is 1, so no select is needed: a
    // select evaluates both branches, and the unused one would fail its cast.
    step.let(
      'fairOut',
      expression.cast(
        'u64',
        expression.multiplyDivide(
          expression.multiplyDivide(
            expression.multiply(
              expression.cast('u128', expression.variable('sold')),
              expression.cast('u128', expression.variable('oraclePrice')),
            ),
            expression.powerOfTen(expression.cast('u64', expression.max(expression.variable('scale'), expression.i64(0)))),
            expression.powerOfTen(
              expression.cast('u64', expression.max(expression.subtract(expression.i64(0), expression.variable('scale')), expression.i64(0))),
            ),
          ),
          expression.u128(10_000n - TOLERANCE_BPS),
          expression.u128(10_000),
        ),
      ),
      'computeOracleFloor',
    ),

    step.require(
      expression.greaterThanOrEqual(
        expression.subtract(balanceOf('destinationAta'), expression.snapshot('balanceBefore')),
        expression.variable('fairOut'),
      ),
      'fillBeatTheOracle',
    ),
  ],
});
rs
/// The Pyth feed id for SOL/USD.
const FEED_ID: [u8; 32] = [
    0xef, 0x0d, 0x8b, 0x6f, 0xda, 0x2c, 0xeb, 0xa4, 0x1d, 0xa1, 0x5d, 0x40, 0x95, 0xd1, 0xda, 0x39,
    0x2a, 0x0d, 0x2f, 0x8e, 0xd0, 0xc6, 0xc7, 0xbc, 0x0f, 0x4c, 0xfa, 0xc8, 0xc2, 0x80, 0xb5, 0x6d,
];
/// How far below the oracle's price the fill may land, in basis points.
const TOLERANCE_BPS: u128 = 100;

/// Swap through Jupiter only at a fill the Pyth price backs.
pub fn jupiter_oracle_checked_swap() -> Template {
    let pinned_mint = |address: Pubkey| {
        account::readonly()
            .address(address)
            .owner(TOKEN_PROGRAM_ID)
            .min_data_length(SPL_MINT_LENGTH)
    };
    Template::new()
        .input("routePlan", Type::Bytes(512))
        .input("inAmount", Type::U64)
        .input("quotedOutAmount", Type::U64)
        .input("slippageBps", Type::U64)
        .input("platformFeeBps", Type::U64)
        .account("jupiter", account::program(JUPITER_V6))
        .account("tokenProgram", account::program(TOKEN_PROGRAM_ID))
        .account(
            "priceUpdate",
            account::readonly()
                .owner(PYTH_RECEIVER)
                .min_data_length(PYTH_LENGTH),
        )
        .account("trader", account::signer().writable())
        .account("sourceAta", token_account())
        .account("destinationAta", token_account())
        // The pair `FEED_ID` prices: what the route sells, and what it buys.
        .account("sourceMint", pinned_mint(WRAPPED_SOL_MINT))
        .account("destinationMint", pinned_mint(USDC_MINT))
        .account_group("routeAccounts")
        // Pin the verification level first: it decides where every other field sits.
        .step(
            step::require(
                account_data("priceUpdate", PYTH_VERIFICATION_LEVEL, ReadType::U8)
                    .eq(u64(PYTH_VERIFICATION_LEVEL_FULL)),
            )
            .label("priceIsFullyVerified"),
        )
        // The owner pin takes any feed's price; the feed id is what says which one this is.
        .step(
            step::require(
                account_data("priceUpdate", PYTH_FEED_ID, ReadType::Pubkey).eq(pubkey(FEED_ID)),
            )
            .label("priceIsTheExpectedFeed"),
        )
        .step(
            step::require(
                (clock_unix_timestamp()
                    - account_data("priceUpdate", PYTH_PUBLISH_TIME, ReadType::I64))
                .lte(i64(60)),
            )
            .label("oracleIsFresh"),
        )
        // Each token account must hold the mint whose decimals scale it.
        .step(
            step::require(
                account_data("sourceAta", TOKEN_ACCOUNT_MINT_OFFSET, ReadType::Pubkey)
                    .eq(key("sourceMint")),
            )
            .label("sourceHoldsTheSourceMint"),
        )
        .step(
            step::require(
                account_data(
                    "destinationAta",
                    TOKEN_ACCOUNT_MINT_OFFSET,
                    ReadType::Pubkey,
                )
                .eq(key("destinationMint")),
            )
            .label("destinationHoldsTheDestinationMint"),
        )
        // Both ends of the swap are the trader's. The key is read once, into a register both
        // checks share: without register reuse, the template uses 62 of the runtime's 64.
        .step(step::let_("traderKey", account_key("trader")))
        .step(
            step::require(
                account_data("sourceAta", TOKEN_ACCOUNT_OWNER_OFFSET, ReadType::Pubkey)
                    .eq(var("traderKey")),
            )
            .label("sellsTheTradersOwnTokens"),
        )
        .step(
            step::require(
                account_data(
                    "destinationAta",
                    TOKEN_ACCOUNT_OWNER_OFFSET,
                    ReadType::Pubkey,
                )
                .eq(var("traderKey")),
            )
            .label("proceedsGoToTheTrader"),
        )
        // In base units the fill is worth sold × price × 10^(destinationDecimals + exponent −
        // sourceDecimals).
        .step(
            step::let_(
                "scale",
                account_data("destinationMint", SPL_MINT_DECIMALS, ReadType::U8).cast(Type::I64)
                    + account_data("priceUpdate", PYTH_EXPONENT, ReadType::I32)
                    - account_data("sourceMint", SPL_MINT_DECIMALS, ReadType::U8).cast(Type::I64),
            )
            .label("computeDecimalScale"),
        )
        // Pyth prices are signed; a negative or zero price means the feed is unusable here.
        .step(
            step::let_(
                "oraclePrice",
                account_data("priceUpdate", PYTH_PRICE, ReadType::I64),
            )
            .label("readOraclePrice"),
        )
        .step(step::require(var("oraclePrice").gt(i64(0))).label("oraclePriceIsPositive"))
        .step(step::snapshot("sourceBefore", balance_of("sourceAta")).label("readSourceBeforeSwap"))
        .step(
            step::snapshot("balanceBefore", balance_of("destinationAta"))
                .label("readBalanceBeforeSwap"),
        )
        .step(platform_fee_within_cap())
        .step(
            step::invoke("jupiter")
                .readonly("tokenProgram")
                .signer("trader")
                .writable("sourceAta")
                .writable("destinationAta")
                .account_group("routeAccounts")
                .data_parts(jupiter_route_data(
                    input("inAmount"),
                    input("quotedOutAmount"),
                ))
                .label("swap"),
        )
        // What actually left, whatever the route data claimed it would sell.
        .step(
            step::let_("sold", snapshot("sourceBefore") - balance_of("sourceAta"))
                .label("measureAmountSold"),
        )
        .step(step::require(var("sold").eq(input("inAmount"))).label("soldTheRouteInput"))
        // sold × price, scaled by 10^scale, less `TOLERANCE_BPS`. Taking the max with zero makes
        // one of the two powers of ten 1, so no select is needed.
        .step(
            step::let_(
                "fairOut",
                multiply_divide(
                    multiply_divide(
                        var("sold").cast(Type::U128) * var("oraclePrice").cast(Type::U128),
                        power_of_ten(var("scale").max(i64(0)).cast(Type::U64)),
                        power_of_ten((i64(0) - var("scale")).max(i64(0)).cast(Type::U64)),
                    ),
                    u128(10_000 - TOLERANCE_BPS),
                    u128(10_000),
                )
                .cast(Type::U64),
            )
            .label("computeOracleFloor"),
        )
        .step(
            step::require(
                (balance_of("destinationAta") - snapshot("balanceBefore")).gte(var("fairOut")),
            )
            .label("fillBeatTheOracle"),
        )
}
ts
import type { Address, Instruction } from '@solana/kit';

import { buildKitRunInstruction, type KitAccountBinding } from '@jac0xb/ballista/kit';
import { compiled } from '../jupiter-oracle-checked-swap.js';
import { JUPITER_V6, USDC_MINT, WRAPPED_SOL_MINT, splitJupiterRoute } from '../shared.js';
import { TOKEN_PROGRAM, at, pinned } from './programs.js';

/** The feed, the two mints and the tolerance are the template's own constants, not run inputs. */
export function buildOracleSwapRun(input: {
  templateAddress: Address;
  /** A `PriceUpdateV2` for SOL/USD, the feed the template pins. */
  priceUpdate: Address;
  trader: Address;
  /** The trader's own wrapped SOL and USDC accounts. */
  sourceAta: Address;
  destinationAta: Address;
  /** The Swap API's `route` data. */
  routeData: Uint8Array;
  /** The route's account list from the fifth account on. */
  routeAccounts: readonly KitAccountBinding[];
}): Instruction {
  const route = splitJupiterRoute(input.routeData);
  return buildKitRunInstruction({
    compiled,
    templateAddress: input.templateAddress,
    inputs: {
      routePlan: route.routePlan,
      inAmount: route.inAmount,
      quotedOutAmount: route.quotedOutAmount,
      slippageBps: route.slippageBps,
      platformFeeBps: route.platformFeeBps,
    },
    accounts: {
      jupiter: pinned(JUPITER_V6),
      tokenProgram: pinned(TOKEN_PROGRAM),
      priceUpdate: at(input.priceUpdate),
      trader: at(input.trader),
      sourceAta: at(input.sourceAta),
      destinationAta: at(input.destinationAta),
      sourceMint: pinned(WRAPPED_SOL_MINT),
      destinationMint: pinned(USDC_MINT),
    },
    accountGroups: { routeAccounts: input.routeAccounts },
  });
}
rs
pub struct OracleSwapAccounts {
    /// A `PriceUpdateV2` for SOL/USD, the feed the template pins.
    pub price_update: Pubkey,
    pub trader: Pubkey,
    /// The trader's own wrapped SOL and USDC accounts.
    pub source_ata: Pubkey,
    pub destination_ata: Pubkey,
}

/// `route` is the Swap API's `route` data split by [`RouteQuote::split`], and `route_accounts` its
/// account list from the fifth account on. The feed, the two mints and the tolerance are the
/// template's own constants, not run inputs.
pub fn run_jupiter_oracle_swap(
    template: Pubkey,
    a: &OracleSwapAccounts,
    route: &RouteQuote,
    route_accounts: Vec<AccountMeta>,
) -> Result<Instruction, Box<dyn Error>> {
    let instruction = templates::jupiter_oracle_checked_swap()
        .compile()?
        .run(template)
        .input("routePlan", route.route_plan)
        .input("inAmount", route.in_amount)
        .input("quotedOutAmount", route.quoted_out_amount)
        .input("slippageBps", route.slippage_bps)
        .input("platformFeeBps", route.platform_fee_bps)
        .account("jupiter", JUPITER_V6)
        .account("tokenProgram", TOKEN_PROGRAM_ID)
        .account("priceUpdate", a.price_update)
        .account("trader", a.trader)
        .account("sourceAta", a.source_ata)
        .account("destinationAta", a.destination_ata)
        .account("sourceMint", WRAPPED_SOL_MINT)
        .account("destinationMint", USDC_MINT)
        .group("routeAccounts", route_accounts)
        .instruction()?;
    Ok(instruction)
}

The Rust template takes its program addresses, token_account(), balance_of() and jupiter_route_data() from the shared helpers.

SOL/USD prices the SOL sold in the USDC bought, counting a USDC as a dollar. Pyth publishes a price as price × 10^exponent per whole token. The template reads the exponent and both mints' decimals on chain, and values what was sold, in the destination token's smallest units, as sold × price × 10^(destinationDecimals + exponent − sourceDecimals).

Run it ​

Jupiter's route starts its account list with the token program, the signer, and the signer's source and destination token accounts. The template passes those four itself; the rest of the route's accounts arrive as the routeAccounts account group.

The Run tabs pass the eight declared accounts, jupiter, tokenProgram, priceUpdate, trader, sourceAta, destinationAta, sourceMint (wrapped SOL's mint) and destinationMint (USDC's), then the inputs routePlan, inAmount, quotedOutAmount, slippageBps and platformFeeBps, then the group. splitJupiterRoute (TypeScript) and RouteQuote::split (Rust) split the Swap API's route data into routePlan and the four numbers after it.

priceUpdate is a fully verified PriceUpdateV2 carrying FEED_ID, SOL/USD's feed id ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d: the account Pyth's push oracle keeps for it, 7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE, or an update you post with Pyth's receiver earlier in the transaction.

Getting a Jupiter route says how to request the route from Jupiter's Swap API and what to keep from its response.

What has been tested ​

  • In LiteSVM. tests/protocols/tests/oracle_checked_swap.rs sells 1 SOL for USDC through Jupiter and a Meteora pool, valued at Pyth's SOL/USD price, in place of route in the transaction Jupiter's API built. It fills exactly as that transaction does alone, for 110 more bytes. Its floor matches the formula above to the last unit. Jupiter's route on its own takes exactly in_amount through a split route and with a platform fee, as soldTheRouteInput needs.
  • Failures. Each of these fails, and the whole transaction reverts: an oracle 5% above the market (fillBeatTheOracle, after the swap); spare token accounts of the trader's at sourceAta and destinationAta while the route moves others (soldTheRouteInput); another wallet's source or an attacker's destination (sellsTheTradersOwnTokens, proceedsGoToTheTrader, before Jupiter is called); USDC/USD's price account passed as SOL/USD's (priceIsTheExpectedFeed); USDC's mint as sourceMint (its pinned address); a route that charges a platform fee (platformFeeWithinCap, before Jupiter is called).
  • An opt-in test checks the Pyth offsets against devnet accounts.

All protocol templates · What has been tested

BALLISTA / A SMALL MACHINE FOR COMPLEX TRANSACTIONS