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

