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

Orders can be partially or fully fulfilled and in both cases the taker receives their tokens immediately. In the case of partial fill, tokens are left in the contract owned vaults, the limit order maker can choose to withdraw these funds early. In the latter case, tokens are sent from the vault to the maker. For optimal UX during a full fulfilment of the limit order, the contract will take the following actions if necessary:

  • Create the makers ATA for the output tokens.

  • Refund the original rent used in the Limit Order back to the maker.

  • Reimburse any lamports back to the taker if they had to pay for any ATA creation.

  • In the case of WSOL, return funds back to the Maker as SOL instead of WSOL.

Example: 100 USDC -> 1 SOL w/ 5 BPS fee

  1. User creates a limit order paying the rent and depositing 100 USDC into the vault. Price is set to 0.01.

  2. Adjusting for fees the when it is favourable to trade 1.0005 SOL -> 100 USDC, the taker comes in and takes the full order.

  3. Under the hood, taker deposits 1.0005 SOL into the vault. 100 USDC is moved from the vault to the taker's ATA, 0.0005 WSOL fee is moved to the fee takers wallet.

  4. Contract has determined that the limit order is fulfilled and since it is SOL, the special edge case must be handled:

    1. The contract expects a taker owned WSOL (non-ATA) token account is passed into the call.

    2. The WSOL vault sends 1 SOL to this token account and closes it out to the maker, crediting their wallet balance with the 1 SOL.

    3. The limit order is closed send rent is sent to the taker.

    4. The taker will then send the rent funds to the maker subtracting any rent they paid for the WSOL token account.

Other Examples

For partial fills, the above example holds just skipping step 4.

For non-WSOL trades, only step 4 differs as it will initialize the makers ATA with the taker being the rent payer. When the limit order is closed the taker is rebated accordingly.

TakeOrder Call

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

Error Codes

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

Limit Order Events

You can listen into the events of the limit order by parsing through the program logs for emitted data.

Last updated