Searchers - Limit Orders

Titan’s Limit Orders are designed to thrive in a competitive environment where searchers play a central role. By participating as a searcher, you gain access to a marketplace of on-chain limit orders.

On-chain Structure of Limit Orders

Limit order program address: TitanLozLMhczcwrioEguG2aAmiATAPXdYpBg3DbeKK

Limit orders are resting on-chain and can be partially or completely filled. Fees are charged to takers and are charged as output_mint tokens.

use pinocchio::pubkey::{create_program_address, Pubkey};
use bytemuck::{Pod, Zeroable};

/// Limit order structure
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct LimitOrder {
    // The public key of the order,
    pub maker: Pubkey,
    // Input mint of the limit order
    pub input_mint: Pubkey,
    // Output mint of the limit order
    pub output_mint: Pubkey,
    // Slot which the order was created
    pub creation_slot: u64,
    // The slot at which the order expires
    pub expiration_slot: u64,
    // The amount of input tokens to be exchanged
    pub amount: u64,
    // The amount of input tokens that have been filled
    pub amount_filled: u64,
    // The amount of output tokens that have been exchanged.
    pub out_amount_filled: u64,
    // The amount of output tokens that the maker has withdrawn.
    pub out_amount_withdrawn: u64,
    // The amount of fees paid in the smallest unit of from_token mint.
    pub fees_paid: u64,
    // Price base in the order, in the smallest unit of output token
    pub price_base: u64,
    // Price exponent, price is calculated as price_base * 10^(-price_exponent)
    pub price_exponent: u8,
    // The status of the order
    pub status: u8,
    // Bump seed for the limit order PDA
    pub bump: u8,
    // Unique identifier for the order, used to differentiate orders for same
    // (owner, input_mint, output_mint) tuple
    pub id: u8,
    // Bump seed for the input mint vault PDA
    pub input_mint_vault_bump: u8,
    // Bump seed for the output mint vault PDA
    pub output_mint_vault_bump: u8,
    // Time in order
    pub time_in_force: u8,
    // Fees ticks rate for the order from takers.
    pub fee_ticks: u8,
}

/// Time in Force (TIF) for limit orders. Provides different behaviors for 
/// how long an order remains active and how it can be filled.
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TimeInForce {
    /// Order is good until cancelled. Partial takes are allowed.
    GoodTillCancelled = 0,
    /// After taking any amount, order is closed.
    TakeCancelsOrder = 1,
    /// Takes must completely fill the order.
    AllOrNothing = 2,
    /// Same as TakeCancelsOrder but it must be filled at the same time of creation.
    ImmediateOrCancel = 3,
    /// Same as AllOrNothing but it must be filled at the same time of creation.
    FillOrKill = 4,
}

/// Order status for limit orders. Indicates the current state of the order
/// and how it can be interacted with.
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OrderStatus {
    /// Order is open, can be partially filled, filled, cancelled
    Open = 0,
    /// Order is partially filled, can be filled or cancelled
    PartiallyFilled = 1,
    /// Order is filled, terminates, used for event logging
    Filled = 2,
    /// Order is cancelled, terminates, used for event logging
    Cancelled = 3,
}

/// Fee tick units
pub const FEE_TICK_UNITS: u8 = 25;
/// 1e6 units = 0.0001, used to convert fee tick rate to fee basis points
pub const FEE_TICK_DIVISOR: u128 = 1_000_000;

/// Limit Order methods
impl LimitOrder {
    pub const SEEDS: &'static [u8] = b"order";
    pub const LEN: usize = 168;

    /// Get the pda address for the limit order, given the maker, input mint,
    /// output mint, id and bump.
    pub fn get_pda_address(
        maker: &Pubkey,
        input_mint: &Pubkey,
        output_mint: &Pubkey,
        id: u8,
        bump: u8,
    ) -> Result<Pubkey, ProgramError> {
        let b0 = &[id];
        let b1 = &[bump];
        let seeds_with_bump = [
            LimitOrder::SEEDS,
            maker.as_ref(),
            input_mint.as_ref(),
            output_mint.as_ref(),
            b0,
            b1,
        ]
        .to_vec();

        create_program_address(&seeds_with_bump, &crate::ID)
    }

    /// Calculate the costs and fees for a given amount and fee ticks.
    pub fn calculate_costs_and_fee(
        &self,
        amount: u64,
        fee_ticks: u8,
    ) -> Result<(u64, u64), ProgramError> {
        let amount_u128 = amount as u128;

        let price_base = self.price_base as u128;
        let price_exponent = 10u128.pow(self.price_exponent as u32);

        let fee_units_u128 = (fee_ticks as u16).saturating_mul(FEE_TICK_UNITS as u16) as u128;

        // Calculate the transfer amount and fee amount.
        // Should never overflow, since its u64 * u64
        // Use method to perform ceiling math division: (a + b - 1) / b
        let cost_u128 = amount_u128
            .saturating_mul(price_base)
            .checked_add(price_exponent.saturating_sub(1))
            .ok_or(ProgramError::ArithmeticOverflow)?
            .saturating_div(price_exponent);
        let cost: u64 = cost_u128
            .try_into()
            .map_err(|_| ProgramError::ArithmeticOverflow)?;

        let fees: u64 = cost_u128
            .saturating_mul(fee_units_u128)
            .saturating_div(FEE_TICK_DIVISOR)
            .try_into()
            .map_err(|_| ProgramError::ArithmeticOverflow)?;

        Ok((cost, fees.max(1)))
    }
    
    /// Amount left to be filled in the order.
    pub fn get_remaining_amount(&self) -> u64 {
        self.amount.saturating_sub(self.amount_filled)
    }
}

Placing Taker Orders

Takers can fulfill orders by invoking the TakeOrder instruction. The following code snippet outlines how to create a set of instructions to successfully execute.

/// Fees to be paid to this address
pub const FEE_RECEIVER_ADDRESS: Pubkey = pubkey!("Bq5ZzfiU3vTiJPrBJFcr98BnUy9Wc1dg9ASeycB2tX1C");

/// Derives the vault manager address and bump seed.
pub fn get_vault_manager_address_and_bump_seed() -> (Pubkey, u8) {
    Pubkey::find_program_address(&[b"vault_manager"], &TITAN_LIMIT_ORDER_PROGRAM_ID)
}

/// Derives the associated token address and bump seed for a given wallet and token mint.
pub fn get_associated_token_address_and_bump_seed(
    wallet_address: &Pubkey,
    token_mint_address: &Pubkey,
    token_program: &Pubkey,
) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &[
            &wallet_address.to_bytes(),
            &token_program.to_bytes(),
            &token_mint_address.to_bytes(),
        ],
        &ASSOCIATED_TOKEN_PROGRAM_ID,
    )
}

/// Creates an instruction bundle to create a token account with a seed.
pub fn create_token_account_with_seed_instructions(
    payer: &Pubkey,
    authority: &Pubkey,
    mint: &Pubkey,
    seed: &str,
    owner: &Pubkey,
) -> Result<(Pubkey, Vec<Instruction>), TitanSDKError> {
    let token_account = Pubkey::create_with_seed(payer, seed, owner)
        .map_err(|_| TitanSDKError::FailedToCreateInstruction)?;

    // Get minimum balance for rent exemption
    let token_account_space = spl_token::state::Account::LEN;
    let lamports = 2039280u64;

    // Create account with seed instruction
    let create_account_ix = create_account_with_seed(
        payer,
        &token_account,
        payer,
        seed,
        lamports,
        token_account_space as u64,
        owner,
    );

    // Initialize token account instruction
    let init_account_ix = initialize_account(&spl_token::id(), &token_account, mint, authority)
        .map_err(|_| TitanSDKError::FailedToCreateInstruction)?;

    Ok((token_account, vec![create_account_ix, init_account_ix]))
}

/// Discriminator for TakeOrder instruction.
mod TakeOrder {
    pub const DISCRIMINATOR: u8 = 2;
}

/// Represents a bundle of instructions, including setup instructions and the main instruction.
pub struct InstructionBundle {
    /// A vector of setup instructions that need to be executed before the main instruction.
    pub setup: Vec<Instruction>,
    /// The main instruction to be executed.
    pub instruction: Instruction,
    /// Cleanup instructions to be executed after the main instruction.
    pub cleanup: Vec<Instruction>,
}


pub fn create_take_order_instruction(
    // Taker of the order, signer.
    taker: Pubkey,
    // Input mint token account
    taker_input_mint_token_account: Pubkey,
    // Output mint token account w/ taker authority
    taker_output_mint_token_account: Pubkey,
    // Limit order state.
    limit_order: &LimitOrder,
    // Input mint amount to recieve
    amount: u64,
    // Max cost taken from output mint token account
    // If this is breached the ixn will fail. 
    max_cost_limit: Option<u64>,
    // Input token program [spl / spl-2022]
    input_mint_program: Pubkey,
    // Output token program [spl / spl-2022]
    output_mint_program: Pubkey,
) -> Result<InstructionBundle> {
    let time_in_force = TimeInForce::try_from(limit_order.time_in_force)?;
    let max_cost_limit = max_cost_limit.unwrap_or(u64::MAX);

    let order_will_fulfill = amount == limit_order.get_remaining_amount()
        || time_in_force == TimeInForce::ImmediateOrCancel
        || time_in_force == TimeInForce::TakeCancelsOrder;

    let fee_ticks = limit_order.fee_ticks;
    let (cost, fee) = limit_order
        .calculate_costs_and_fee(amount, fee_ticks)?;

    let output_is_wsol = limit_order.output_mint.eq(&WRAPPED_SOL);
    let input_is_wsol = limit_order.input_mint.eq(&WRAPPED_SOL);

    let limit_order_address = derive_limit_order_address(limit_order);

    let (vault_manager_address, _) = get_vault_manager_address_and_bump_seed();

    let maker = Pubkey::new_from_array(limit_order.maker);
    let input_mint = Pubkey::new_from_array(limit_order.input_mint);
    let output_mint = Pubkey::new_from_array(limit_order.output_mint);

    let mut setup = vec![create_associated_token_account_idempotent(
        &taker,
        &Pubkey::new_from_array(FEE_RECEIVER_ADDRESS),
        &output_mint,
        &output_mint_program,
    )];
    let mut cleanup = vec![];

    if output_is_wsol {
        let wsol_ata = get_associated_token_address_with_program_id(
            &taker,
            &output_mint,
            &output_mint_program,
        );
        setup.extend_from_slice(&[
            create_associated_token_account_idempotent(
                &taker,
                &taker,
                &output_mint,
                &output_mint_program,
            ),
            transfer(&taker, &wsol_ata, cost.saturating_add(fee)),
            sync_native(&output_mint_program, &wsol_ata)?,
        ]);
        cleanup.push(
            close_account(&output_mint_program, &wsol_ata, &taker, &taker, &[])?,
        )
    }
    if input_is_wsol {
        let wsol_ata =
            get_associated_token_address_with_program_id(&taker, &input_mint, &input_mint_program);
        setup.extend_from_slice(&[
            create_associated_token_account_idempotent(
                &taker,
                &taker,
                &input_mint,
                &input_mint_program,
            ),
            transfer(&taker, &wsol_ata, cost.saturating_add(fee)),
            sync_native(&input_mint_program, &wsol_ata)?,
        ]);
    }

    let (output_mint_token_account_address, output_mint_token_account_bump) =
        if order_will_fulfill && output_is_wsol {
            // Handle the special case here
            let (pk, instructions) = create_token_account_with_seed_instructions(
                &taker,
                &taker,
                &output_mint,
                "token_seed",
                &output_mint_program,
            )?;
            setup.extend(instructions);
            (pk, 0) // Bump is not used in this case
        } else {
            // otherwise always assume its the makers output ata.
            get_associated_token_address_and_bump_seed(&maker, &output_mint, &output_mint_program)
        };

    let (input_mint_token_account_address, input_mint_token_account_bump) =
        if order_will_fulfill && input_is_wsol {
            // Handle the special case here
            let (pk, instructions) = create_token_account_with_seed_instructions(
                &taker,
                &taker,
                &input_mint,
                "token_seed",
                &input_mint_program,
            )?;
            setup.extend(instructions);
            (pk, 0) // Bump is not used in this case
        } else {
            // otherwise always assume its the makers output ata.
            get_associated_token_address_and_bump_seed(&maker, &input_mint, &input_mint_program)
        };

    let (input_mint_vault_address, _) = get_associated_token_address_and_bump_seed(
        &vault_manager_address,
        &input_mint,
        &input_mint_program,
    );
    let (output_mint_vault_address, _) = get_associated_token_address_and_bump_seed(
        &vault_manager_address,
        &output_mint,
        &output_mint_program,
    );

    // Create the vault manager output token account if it doesn't exist
    setup.push(create_associated_token_account_idempotent(
        &taker,
        &vault_manager_address,
        &output_mint,
        &output_mint_program,
    ));

    let fee_receiver = Pubkey::new_from_array(FEE_RECEIVER_ADDRESS);
    let (fee_receiver_output_mint_token_account, fee_reciever_output_mint_bump) =
        get_associated_token_address_and_bump_seed(
            &fee_receiver,
            &output_mint,
            &output_mint_program,
        );

    let mut data = vec![*instructions::TakeOrder::DISCRIMINATOR];
    data.extend_from_slice(&amount.to_le_bytes());
    data.extend_from_slice(&max_cost_limit.to_le_bytes());
    data.extend_from_slice(&[
        output_mint_token_account_bump,
        input_mint_token_account_bump,
        fee_reciever_output_mint_bump,
    ]);

    let accounts = vec![
        AccountMeta::new(taker, true),
        AccountMeta::new(maker, false),
        AccountMeta::new_readonly(input_mint, false),
        AccountMeta::new_readonly(output_mint, false),
        AccountMeta::new(limit_order_address, false),
        AccountMeta::new(taker_input_mint_token_account, false),
        AccountMeta::new(taker_output_mint_token_account, false),
        AccountMeta::new(output_mint_token_account_address, false),
        AccountMeta::new(input_mint_token_account_address, false),
        AccountMeta::new(fee_receiver_output_mint_token_account, false),
        AccountMeta::new_readonly(vault_manager_address, false),
        AccountMeta::new(input_mint_vault_address, false),
        AccountMeta::new(output_mint_vault_address, false),
        AccountMeta::new_readonly(solana_program::system_program::ID, false),
        AccountMeta::new_readonly(input_mint_program, false),
        AccountMeta::new_readonly(output_mint_program, false),
        AccountMeta::new_readonly(ASSOCIATED_TOKEN_PROGRAM_ID, false),
        AccountMeta::new_readonly(solana_program::sysvar::instructions::ID, false),
    ];

    Ok(InstructionBundle {
        setup,
        instruction: Instruction {
            program_id: TITAN_LIMIT_ORDER_PROGRAM_ID,
            accounts,
            data,
        },
        cleanup,
    })
}

Error Codes

These are the custom error codes that are thrown in the contract.

#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LimitOrderError {
    InvalidOrderStatus,                   // 0
    InvalidAmount,                        // 1
    InvalidMaker,                         // 2
    InvalidMint,                          // 3
    InvalidPrice,                         // 4
    InvalidTokenAccountAuthority,         // 5
    InvalidTokenProgramId,                // 6
    InvalidAssociatedTokenAccountAddress, // 7
    InvalidExtension,                     // 8
    EventDeserializationError,            // 9
    ExpirationSlotExceeded,               // 10
    ExecutionTimeInForceViolation,        // 11
    MaxCostLimitExceeded,                 // 12
}

Last updated