Skip to content

Settle at a signed quote ​

Ed25519 · Instructions sysvar · SPL Token

Status: Tested locally in LiteSVM against the Token program and the USDC and wrapped SOL mints copied from mainnet, with Solana's Ed25519 precompile, and in Mollusk, a harness that runs Solana programs without a validator; not yet run on devnet or mainnet.

Cost: Ballista's own work took 8,733 of the tested transaction's 8,892compute units; the two token transfers took the rest. Ballista charges no fee; see what it costs.

What it does ​

Settles a trade at a price a maker signed off chain. The taker pays the signed price, the maker delivers, and no single settlement can go past what the maker signed: the price, the size, the taker, the tokens and the expiry.

The maker, who quotes, signs a quote off chain and sends it to the taker, who accepts it. Settling moves the maker's tokens, so the maker signs the transaction too. Because the template holds the trade to the quote, the service that co-signs for the maker only has to check that the transaction runs this template and nothing else that could spend the maker's accounts.

Three Solana pieces make this work:

  • An Ed25519 signature is 64 bytes that prove the holder of a key, here the maker's wallet key, signed exactly these bytes.
  • A precompile is a program built into the Solana runtime; the Ed25519 precompile checks the signatures in its instruction, and a bad one fails the whole transaction.
  • The Instructions sysvar is a read-only account the runtime fills with the transaction's instructions, so the template can read the Ed25519 instruction before its own.

The quote is 128 bytes: the tag BLSTQT01, then price, maxAmount and expiry as eight-byte little-endian integers, then the 32-byte addresses of taker, baseMint and quoteMint. The maker delivers the base token, and the taker pays in the quote token. Amounts are in base units, a token's smallest unit. price has six decimals, so 1,000,000 means one quote unit per base unit. expiry is the last Unix timestamp at which the quote can settle.

The precompile proves only that some key signed some bytes. The template ties them to the maker and to this trade, checking in order that:

  1. the instruction directly before the run is the Ed25519 precompile;
  2. it holds one self-contained signature over 128 bytes: the signature, the key and the message all sit in its own data, so the bytes the template reads are the bytes the precompile checked;
  3. the signing key is maker's;
  4. the message starts with BLSTQT01, the tag that separates quotes from everything else the maker signs (a signature covers bytes, not what they mean);
  5. the clock hasn't passed expiry;
  6. the quote's taker is the wallet signing as taker;
  7. amount is at most maxAmount;
  8. the taker pays from an account in quoteMint, and the maker delivers from one in baseMint (a token transfer only moves between accounts of one mint, so this pins both sides);
  9. the account the taker pays into belongs to the maker, not one the taker picked.

It then prices the trade at amount × price ÷ 1,000,000, rounded up in the maker's favor, and makes two SPL Token transfers: the taker pays that to the maker, and the maker delivers amount to the taker.

A quote can settle more than once

This template keeps no state, so it can't count settlements. Until a quote expires, it can settle again unless the maker's co-signer refuses a second settlement of the same quote. maxAmount caps each settlement, not the quote as a whole: in a test, settling the same quote twice delivered 3 SOL against a maxAmount of 2 SOL. A template that must refuse replays itself can keep a per-maker nonce in a registry entry.

Template ​

ts
import {
  INSTRUCTIONS_SYSVAR_ADDRESS_BYTES,
  TOKEN_PROGRAM_ADDRESS_BYTES,
  account,
  compileTemplate,
  defineTemplate,
  ed25519Signature,
  expression,
  step,
  tokenTransfer,
} from '@jac0xb/ballista';
import { TOKEN_ACCOUNT_LENGTH, TOKEN_ACCOUNT_MINT_OFFSET, TOKEN_ACCOUNT_OWNER_OFFSET } from './shared.js';

/** The signed quote. Integers are little-endian; keys are their 32 raw bytes. */
export const QUOTE = {
  length: 128,
  /** `QUOTE_TAG`, marking the message as a settlement quote. */
  tag: 0,
  /** Quote-token base units per `PRICE_SCALE` base-token base units. */
  price: 8,
  /** The most base-token base units the maker delivers in one settlement. The quote can settle
   * again until it expires, so this bounds each settlement, not the total. */
  maxAmount: 16,
  /** The last Unix timestamp at which the quote can settle. */
  expiry: 24,
  /** The one wallet that can take the quote. */
  taker: 32,
  /** The mint the maker delivers. */
  baseMint: 64,
  /** The mint the taker pays in. */
  quoteMint: 96,
} as const;

/** The eight bytes every quote starts with. */
export const QUOTE_TAG = new TextEncoder().encode('BLSTQT01');

/** Prices carry six decimals: a price of 1,000,000 is one quote unit per base unit. */
export const PRICE_SCALE = 1_000_000n;

const instructions = account.fixed('instructions');
const taker = account.fixed('taker');
const maker = account.fixed('maker');

/** The maker's signature, in the instruction directly before this template's run. */
export const signedQuote = ed25519Signature({
  sysvar: instructions,
  index: expression.subtract(expression.currentInstructionIndex(instructions), expression.u64(1)),
  signer: expression.accountField(maker, 'key'),
  messageLength: QUOTE.length,
  name: 'quote',
});

const tokenAccount = { writable: true, owner: TOKEN_PROGRAM_ADDRESS_BYTES, minDataLength: TOKEN_ACCOUNT_LENGTH };

export const signedQuoteSettlement = defineTemplate({
  inputs: {
    /** Base-token base units to take, up to the quoted maximum. */
    amount: { type: 'u64' },
  },
  accounts: {
    instructions: { address: INSTRUCTIONS_SYSVAR_ADDRESS_BYTES },
    tokenProgram: { executable: true, address: TOKEN_PROGRAM_ADDRESS_BYTES },
    taker: { signer: true },
    maker: { signer: true },
    /** Pays, in the quote mint. */
    takerQuoteAccount: tokenAccount,
    /** Is paid, in the quote mint. */
    makerQuoteAccount: tokenAccount,
    /** Delivers, in the base mint. */
    makerBaseAccount: tokenAccount,
    /** Receives, in the base mint. */
    takerBaseAccount: tokenAccount,
  },
  steps: [
    ...signedQuote.steps,

    step.require(
      expression.equal(
        signedQuote.field(QUOTE.tag, 'u64'),
        expression.u64(new DataView(QUOTE_TAG.buffer).getBigUint64(0, true)),
      ),
      'quoteIsTagged',
    ),
    step.require(
      expression.lessThanOrEqual(expression.clockUnixTimestamp(), signedQuote.field(QUOTE.expiry, 'i64')),
      'quoteHasNotExpired',
    ),
    step.require(
      expression.equal(signedQuote.field(QUOTE.taker, 'pubkey'), expression.accountField(taker, 'key')),
      'quoteIsForThisTaker',
    ),
    step.require(
      expression.lessThanOrEqual(expression.input('amount'), signedQuote.field(QUOTE.maxAmount, 'u64')),
      'withinTheQuotedSize',
    ),

    // A token `transfer` moves only between two accounts of one mint, so pinning one side of each
    // leg pins both.
    step.require(
      expression.equal(
        expression.accountData(account.fixed('takerQuoteAccount'), TOKEN_ACCOUNT_MINT_OFFSET, 'pubkey'),
        signedQuote.field(QUOTE.quoteMint, 'pubkey'),
      ),
      'paysInTheQuotedMint',
    ),
    step.require(
      expression.equal(
        expression.accountData(account.fixed('makerBaseAccount'), TOKEN_ACCOUNT_MINT_OFFSET, 'pubkey'),
        signedQuote.field(QUOTE.baseMint, 'pubkey'),
      ),
      'deliversTheQuotedMint',
    ),
    // The payment reaches an account the maker owns, not one the taker picked.
    step.require(
      expression.equal(
        expression.accountData(account.fixed('makerQuoteAccount'), TOKEN_ACCOUNT_OWNER_OFFSET, 'pubkey'),
        expression.accountField(maker, 'key'),
      ),
      'paymentReachesTheMaker',
    ),

    step.let(
      'payment',
      expression.multiplyDivide(
        expression.input('amount'),
        signedQuote.field(QUOTE.price, 'u64'),
        expression.u64(PRICE_SCALE),
        'up',
      ),
      'priceTheFill',
    ),
    tokenTransfer({
      tokenProgram: account.fixed('tokenProgram'),
      source: account.fixed('takerQuoteAccount'),
      destination: account.fixed('makerQuoteAccount'),
      authority: taker,
      amount: expression.variable('payment'),
      label: 'takerPays',
    }),
    tokenTransfer({
      tokenProgram: account.fixed('tokenProgram'),
      source: account.fixed('makerBaseAccount'),
      destination: account.fixed('takerBaseAccount'),
      authority: maker,
      amount: expression.input('amount'),
      label: 'makerDelivers',
    }),
  ],
});

export const compiled = compileTemplate(signedQuoteSettlement);
rs
/// The signed quote's layout. Integers are little-endian; keys are their 32 raw bytes.
const QUOTE_LENGTH: u32 = 128;
const QUOTE_TAG_OFFSET: u32 = 0;
const QUOTE_PRICE: u32 = 8;
const QUOTE_MAX_AMOUNT: u32 = 16;
const QUOTE_EXPIRY: u32 = 24;
const QUOTE_TAKER: u32 = 32;
const QUOTE_BASE_MINT: u32 = 64;
const QUOTE_QUOTE_MINT: u32 = 96;

/// The eight bytes every quote starts with.
const QUOTE_TAG: [u8; 8] = *b"BLSTQT01";

/// Prices carry six decimals: a price of 1,000,000 is one quote unit per base unit.
const PRICE_SCALE: u64 = 1_000_000;

/// Settle a trade at a price the maker signed, in the instruction before this run.
pub fn signed_quote_settlement() -> Template {
    // The maker's signature, in the instruction directly before this template's run.
    let quote = ed25519_signature(
        "instructions",
        current_instruction_index("instructions") - u64(1),
        key("maker"),
        QUOTE_LENGTH,
    )
    .name("quote");

    Template::new()
        // Base-token base units to take, up to the quoted maximum.
        .input("amount", Type::U64)
        .account(
            "instructions",
            account::readonly().address(INSTRUCTIONS_SYSVAR_ID),
        )
        .account("tokenProgram", account::program(TOKEN_PROGRAM_ID))
        .account("taker", account::signer())
        .account("maker", account::signer())
        // Pays, in the quote mint.
        .account("takerQuoteAccount", token_account())
        // Is paid, in the quote mint.
        .account("makerQuoteAccount", token_account())
        // Delivers, in the base mint.
        .account("makerBaseAccount", token_account())
        // Receives, in the base mint.
        .account("takerBaseAccount", token_account())
        .steps(quote.steps())
        .step(
            step::require(
                quote
                    .field(QUOTE_TAG_OFFSET, ReadType::U64)
                    .eq(u64(u64::from_le_bytes(QUOTE_TAG))),
            )
            .label("quoteIsTagged"),
        )
        .step(
            step::require(clock_unix_timestamp().lte(quote.field(QUOTE_EXPIRY, ReadType::I64)))
                .label("quoteHasNotExpired"),
        )
        .step(
            step::require(quote.field(QUOTE_TAKER, ReadType::Pubkey).eq(key("taker")))
                .label("quoteIsForThisTaker"),
        )
        .step(
            step::require(input("amount").lte(quote.field(QUOTE_MAX_AMOUNT, ReadType::U64)))
                .label("withinTheQuotedSize"),
        )
        // A token `transfer` moves only between two accounts of one mint, so pinning one side of
        // each leg pins both.
        .step(
            step::require(
                account_data(
                    "takerQuoteAccount",
                    TOKEN_ACCOUNT_MINT_OFFSET,
                    ReadType::Pubkey,
                )
                .eq(quote.field(QUOTE_QUOTE_MINT, ReadType::Pubkey)),
            )
            .label("paysInTheQuotedMint"),
        )
        .step(
            step::require(
                account_data(
                    "makerBaseAccount",
                    TOKEN_ACCOUNT_MINT_OFFSET,
                    ReadType::Pubkey,
                )
                .eq(quote.field(QUOTE_BASE_MINT, ReadType::Pubkey)),
            )
            .label("deliversTheQuotedMint"),
        )
        // The payment reaches an account the maker owns, not one the taker picked.
        .step(
            step::require(
                account_data(
                    "makerQuoteAccount",
                    TOKEN_ACCOUNT_OWNER_OFFSET,
                    ReadType::Pubkey,
                )
                .eq(key("maker")),
            )
            .label("paymentReachesTheMaker"),
        )
        .step(
            step::let_(
                "payment",
                input("amount")
                    .mul_div_up(quote.field(QUOTE_PRICE, ReadType::U64), u64(PRICE_SCALE)),
            )
            .label("priceTheFill"),
        )
        .step(
            token_transfer(
                "tokenProgram",
                "takerQuoteAccount",
                "makerQuoteAccount",
                "taker",
                var("payment"),
            )
            .label("takerPays"),
        )
        .step(
            token_transfer(
                "tokenProgram",
                "makerBaseAccount",
                "takerBaseAccount",
                "maker",
                input("amount"),
            )
            .label("makerDelivers"),
        )
}
ts
import { getAddressDecoder, getAddressEncoder, type Address, type Instruction } from '@solana/kit';

import { ED25519_PROGRAM_ADDRESS_BYTES, INSTRUCTIONS_SYSVAR_ADDRESS_BYTES } from '@jac0xb/ballista';
import { buildKitRunInstruction } from '@jac0xb/ballista/kit';
import { QUOTE, QUOTE_TAG, compiled } from '../signed-quote-settlement.js';
import { TOKEN_PROGRAM, at, pinned } from './programs.js';

const ED25519_PROGRAM = getAddressDecoder().decode(ED25519_PROGRAM_ADDRESS_BYTES);
const INSTRUCTIONS_SYSVAR = getAddressDecoder().decode(INSTRUCTIONS_SYSVAR_ADDRESS_BYTES);

/** The quote a maker signs off chain. */
export interface Quote {
  /** Quote-token base units per 1,000,000 base-token base units. */
  price: bigint;
  /** The most base-token base units the maker delivers. */
  maxAmount: bigint;
  /** The last Unix timestamp at which the quote can settle. */
  expiry: bigint;
  /** The one wallet that can take the quote. */
  taker: Address;
  /** The mint the maker delivers, and the mint the taker pays in. */
  baseMint: Address;
  quoteMint: Address;
}

/** The 128 bytes the maker signs: the tag `BLSTQT01`, then the fields, integers little-endian. */
export function quoteMessage(quote: Quote): Uint8Array {
  const encoder = getAddressEncoder();
  const message = new Uint8Array(QUOTE.length);
  const view = new DataView(message.buffer);
  message.set(QUOTE_TAG, QUOTE.tag);
  view.setBigUint64(QUOTE.price, quote.price, true);
  view.setBigUint64(QUOTE.maxAmount, quote.maxAmount, true);
  view.setBigInt64(QUOTE.expiry, quote.expiry, true);
  message.set(encoder.encode(quote.taker), QUOTE.taker);
  message.set(encoder.encode(quote.baseMint), QUOTE.baseMint);
  message.set(encoder.encode(quote.quoteMint), QUOTE.quoteMint);
  return message;
}

/**
 * The Ed25519 precompile instruction that verifies `signature`, `signer`'s over `message`. It
 * holds one signature, with the key, the signature and the message in its own data.
 */
export function buildEd25519Instruction(input: {
  signer: Address;
  signature: Uint8Array;
  message: Uint8Array;
}): Instruction {
  if (input.signature.length !== 64) throw new RangeError('An Ed25519 signature is 64 bytes');
  // 0xffff as an instruction index: the bytes are in this instruction's own data.
  const OWN_DATA = 0xffff;
  const keyOffset = 2 + 14;
  const signatureOffset = keyOffset + 32;
  const messageOffset = signatureOffset + 64;
  const data = new Uint8Array(messageOffset + input.message.length);
  const view = new DataView(data.buffer);
  data[0] = 1; // one signature, then a padding byte
  const offsets = [signatureOffset, OWN_DATA, keyOffset, OWN_DATA, messageOffset, input.message.length, OWN_DATA];
  offsets.forEach((field, index) => view.setUint16(2 + 2 * index, field, true));
  data.set(getAddressEncoder().encode(input.signer), keyOffset);
  data.set(input.signature, signatureOffset);
  data.set(input.message, messageOffset);
  return { programAddress: ED25519_PROGRAM, data };
}

/**
 * The transaction's two instructions, in order: the Ed25519 instruction carrying the maker's
 * signature over the quote, and the run, which reads the signature from the instruction directly
 * before it. The taker and the maker both sign the transaction.
 */
export function buildSignedQuoteRun(input: {
  templateAddress: Address;
  taker: Address;
  maker: Address;
  /** Pays, in the quote mint. */
  takerQuoteAccount: Address;
  /** Is paid, in the quote mint: the maker's own account. */
  makerQuoteAccount: Address;
  /** Delivers, in the base mint. */
  makerBaseAccount: Address;
  /** Receives, in the base mint. */
  takerBaseAccount: Address;
  quote: Quote;
  /** The maker's 64-byte Ed25519 signature over `quoteMessage(quote)`. */
  signature: Uint8Array;
  /** Base-token base units to take, up to the quote's `maxAmount`. */
  amount: bigint;
}): [Instruction, Instruction] {
  const verify = buildEd25519Instruction({
    signer: input.maker,
    signature: input.signature,
    message: quoteMessage(input.quote),
  });
  const run = buildKitRunInstruction({
    compiled,
    templateAddress: input.templateAddress,
    inputs: { amount: input.amount },
    accounts: {
      instructions: pinned(INSTRUCTIONS_SYSVAR),
      tokenProgram: pinned(TOKEN_PROGRAM),
      taker: at(input.taker),
      maker: at(input.maker),
      takerQuoteAccount: at(input.takerQuoteAccount),
      makerQuoteAccount: at(input.makerQuoteAccount),
      makerBaseAccount: at(input.makerBaseAccount),
      takerBaseAccount: at(input.takerBaseAccount),
    },
  });
  return [verify, run];
}
rs
/// The quote a maker signs off chain, as `signed-quote-settlement.ts` reads it.
pub struct Quote {
    /// Quote-token base units per 1,000,000 base-token base units.
    pub price: u64,
    /// The most base-token base units the maker delivers in one settlement. The quote can settle
    /// again until it expires, so this bounds each settlement, not the total.
    pub max_amount: u64,
    /// The last Unix timestamp at which the quote can settle.
    pub expiry: i64,
    /// The one wallet that can take the quote.
    pub taker: Pubkey,
    /// The mint the maker delivers, and the mint the taker pays in.
    pub base_mint: Pubkey,
    pub quote_mint: Pubkey,
}

impl Quote {
    /// The 128 bytes the maker signs: the tag `BLSTQT01`, then the fields, integers
    /// little-endian.
    pub fn message(&self) -> Vec<u8> {
        let mut message = Vec::with_capacity(128);
        message.extend_from_slice(b"BLSTQT01");
        message.extend_from_slice(&self.price.to_le_bytes());
        message.extend_from_slice(&self.max_amount.to_le_bytes());
        message.extend_from_slice(&self.expiry.to_le_bytes());
        for key in [self.taker, self.base_mint, self.quote_mint] {
            message.extend_from_slice(key.as_ref());
        }
        message
    }
}

/// The Ed25519 precompile instruction that verifies `signature`, `signer`'s over `message`. It
/// holds one signature, with the key, the signature and the message in its own data, laid out as
/// `new_ed25519_instruction_with_signature` lays them out.
pub fn ed25519_instruction(signer: &Pubkey, signature: &[u8; 64], message: &[u8]) -> Instruction {
    // `u16::MAX` as an instruction index: the bytes are in this instruction's own data.
    const OWN_DATA: u16 = u16::MAX;
    let key_offset: u16 = 2 + 14;
    let signature_offset = key_offset + 32;
    let message_offset = signature_offset + 64;
    let mut data = vec![1, 0]; // one signature, then a padding byte
    for field in [
        signature_offset,
        OWN_DATA,
        key_offset,
        OWN_DATA,
        message_offset,
        message.len() as u16,
        OWN_DATA,
    ] {
        data.extend_from_slice(&field.to_le_bytes());
    }
    data.extend_from_slice(signer.as_ref());
    data.extend_from_slice(signature);
    data.extend_from_slice(message);
    Instruction {
        program_id: ED25519_PROGRAM_ID,
        accounts: vec![],
        data,
    }
}

pub struct SignedQuoteAccounts {
    pub taker: Pubkey,
    pub maker: Pubkey,
    /// Pays, in the quote mint.
    pub taker_quote_account: Pubkey,
    /// Is paid, in the quote mint: the maker's own account.
    pub maker_quote_account: Pubkey,
    /// Delivers, in the base mint.
    pub maker_base_account: Pubkey,
    /// Receives, in the base mint.
    pub taker_base_account: Pubkey,
}

/// The transaction's two instructions, in order: the Ed25519 instruction carrying `signature`,
/// the maker's over `quote`, and the run, which reads the signature from the instruction directly
/// before it. The taker and the maker both sign the transaction. `amount` is in base-token base
/// units, up to the quote's `max_amount`.
pub fn run_signed_quote(
    template: Pubkey,
    a: &SignedQuoteAccounts,
    quote: &Quote,
    signature: &[u8; 64],
    amount: u64,
) -> Result<[Instruction; 2], Box<dyn Error>> {
    let verify = ed25519_instruction(&a.maker, signature, &quote.message());
    let run = templates::signed_quote_settlement()
        .compile()?
        .run(template)
        .input("amount", amount)
        .account("instructions", INSTRUCTIONS_SYSVAR_ID)
        .account("tokenProgram", TOKEN_PROGRAM_ID)
        .account("taker", a.taker)
        .account("maker", a.maker)
        .account("takerQuoteAccount", a.taker_quote_account)
        .account("makerQuoteAccount", a.maker_quote_account)
        .account("makerBaseAccount", a.maker_base_account)
        .account("takerBaseAccount", a.taker_base_account)
        .instruction()?;
    Ok([verify, run])
}

The first steps come from the SDK's ed25519Signature helper (ed25519_signature in Rust), which returns them with a field reader for the signed message. field refuses a read past the message, and a template that uses field without the steps doesn't compile. The Rust template takes its token_account() from the shared helpers.

The helper's signer must be a key the transaction's builder can't choose. With an input, or the key of an account nothing constrains, the builder could sign a quote with a key of their own. Here it is the key of maker, which must also sign the transaction, so a quote settles only if its signer signs the settlement too. The helper refuses an input, but it can't see an account's constraints: those are the template's to get right.

Run it ​

The maker signs the quote's 128 bytes with its wallet key and sends the quote and the 64-byte signature to the taker. The taker builds one transaction with two instructions, in this order: the Ed25519 instruction, carrying the maker's key, the signature and the quote, then the run. The taker and the maker both sign it.

The Run tabs build both. quoteMessage (TypeScript) and Quote::message (Rust) write the 128 bytes the maker signs. buildEd25519Instruction and ed25519_instruction build the Ed25519 instruction with one signature and each of its three instruction indexes set to u16::MAX (0xffff), which means "this instruction's own data". buildSignedQuoteRun and run_signed_quote return the two instructions in order.

The Run tabs pass the eight declared accounts, instructions, tokenProgram, taker, maker, takerQuoteAccount, makerQuoteAccount, makerBaseAccount and takerBaseAccount, then the input amount. instructions is the Instructions sysvar. The four token accounts are writable, and the SPL Token program must own them, so Token-2022 accounts are rejected.

The Ed25519 instruction goes directly before the run

The template reads the signature from the instruction just before its own. With nothing before the run, it fails at quoteInstructionIndex. With any other instruction in between, even a memo, it fails at quoteIsEd25519. currentInstructionIndex gives the top-level instruction's index, so if another program calls the run through a CPI, the signature must sit directly before that program's instruction.

What has been tested ​

  • In LiteSVM. tests/protocols/tests/signed_quote.rs runs it against copies of mainnet's Token program and the USDC and wrapped SOL mints, with the Ed25519 precompile. Selling 1.5 SOL and a lamport pays 225,375,001 USDC units, rounded up. Settling at exactly expiry lands and one second later fails; exactly maxAmount lands and one lamport more fails; USDC named as the base mint fails at deliversTheQuotedMint. The same quote settled twice in its window lands both times, 3 SOL against a maxAmount of 2 SOL. No failed run moves any balance. A settlement costs 8,892 compute units, and the transaction is 783 bytes with a 15,000 lamport fee. The precompile uses no compute units: it adds one signature to the fee and 276 of the 783 bytes.
  • End to end in Mollusk. tests/ballista/src/lib.rs (signed_quote_settles_only_as_the_maker_signed) uploads the template as the TypeScript SDK compiles it and runs it in Mollusk after a real Ed25519 instruction. At a price of 2,500,000 (2.5 quote units per base unit), taking 3,000,001 base units pays the maker 7,500,003, rounded up from 7,500,002.5, and delivers 3,000,001 to the taker.
  • Failures in the precompile. A signature or signed price with one bit flipped fails the Ed25519 instruction with InvalidSignature, so Ballista never runs.
  • Failures in the template, each at its own step: nothing before the run (quoteInstructionIndex); a memo in between (quoteIsEd25519); another key's signature (quoteIsBySigner); two signatures, a 127-byte message, or any one of the three instruction indexes set to 0 instead of u16::MAX (quoteIsOneSelfContainedSignature); a message the maker signed under the tag BLSTQT02 (quoteIsTagged); an expiry one second before the clock (quoteHasNotExpired); another taker (quoteIsForThisTaker); one base unit over maxAmount (withinTheQuotedSize); a quote for another quote mint (paysInTheQuotedMint); a payment account the taker owns (paymentReachesTheMaker).
  • Not tested. Devnet and mainnet. Both suites build their own Ed25519 instruction and run, in the same layout as the Run tabs (the LiteSVM test checks its copy against Solana's own builder), so no test runs the Run tabs' code against the program. The TypeScript run is only type-checked.
  • clients/js/src/compiler.test.ts checks the ed25519Signature helper: its steps and header check, that it refuses an input as signer, and that field stays inside the message.

All protocol templates · What has been tested

BALLISTA / A SMALL MACHINE FOR COMPLEX TRANSACTIONS