use {
crate::{
error::TwammError,
math,
oracle::OraclePrice,
state::{
pool::Pool,
token_pair::{MatchingSide, SettlementType, TokenPair},
},
},
anchor_lang::prelude::*,
anchor_spl::token::{Token, TokenAccount},
solana_program::{
instruction::{AccountMeta, Instruction},
program,
},
};
#[derive(Accounts)]
pub struct Crank<'info> {
#[account()]
pub owner: Signer<'info>,
#[account(
mut,
constraint = user_account_token_a.mint == custody_token_a.mint,
has_one = owner
)]
pub user_account_token_a: Box<Account<'info, TokenAccount>>,
#[account(
mut,
constraint = user_account_token_b.mint == custody_token_b.mint,
has_one = owner
)]
pub user_account_token_b: Box<Account<'info, TokenAccount>>,
#[account(
mut,
seeds = [b"token_pair",
token_pair.config_a.mint.as_ref(),
token_pair.config_b.mint.as_ref()],
bump = token_pair.token_pair_bump)]
pub token_pair: Box<Account<'info, TokenPair>>,
#[account(
mut,
seeds = [b"transfer_authority"],
bump = token_pair.transfer_authority_bump
)]
pub transfer_authority: AccountInfo<'info>,
#[account(
mut,
constraint = custody_token_a.key() == token_pair.config_a.custody
)]
pub custody_token_a: Box<Account<'info, TokenAccount>>,
#[account(
mut,
constraint = custody_token_b.key() == token_pair.config_b.custody
)]
pub custody_token_b: Box<Account<'info, TokenAccount>>,
#[account(
constraint = oracle_token_a.key() == token_pair.config_a.oracle_account
)]
pub oracle_token_a: AccountInfo<'info>,
#[account(
constraint = oracle_token_b.key() == token_pair.config_b.oracle_account
)]
pub oracle_token_b: AccountInfo<'info>,
token_program: Program<'info, Token>,
}
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct CrankParams {
router_instruction_data: Vec<u8>,
}
pub fn crank(ctx: Context<Crank>, params: &CrankParams) -> Result<i64> {
let token_pair = ctx.accounts.token_pair.as_mut();
require!(token_pair.allow_cranks, TwammError::CranksNotAllowed);
if token_pair.crank_authority != Pubkey::default()
&& token_pair.crank_authority != ctx.accounts.owner.key()
{
msg!("Error: Permissionless cranks are not enabled for this token pair");
return err!(TwammError::CranksNotAllowed);
}
msg!("Load pools");
let (mut pools, router_program) = token_pair.load_pools(ctx.remaining_accounts)?;
require!(!pools.is_empty(), TwammError::NothingToSettle);
let token_a_change;
let token_b_change;
let swap_price;
let swap_amount;
let receive_amount;
let supply_side;
let oracle_price = token_pair
.get_token_pair_oracle_price(&ctx.accounts.oracle_token_a, &ctx.accounts.oracle_token_b)?;
if router_program != Pubkey::default() {
msg!("Perform swap");
if ctx.remaining_accounts.len() < pools.len() + 5 {
return Err(ProgramError::NotEnoughAccountKeys.into());
}
let initial_token_a_balance = ctx.accounts.custody_token_a.amount;
let initial_token_b_balance = ctx.accounts.custody_token_b.amount;
let authority_seeds: &[&[&[u8]]] =
&[&[b"transfer_authority", &[token_pair.transfer_authority_bump]]];
let mut router_accounts = vec![];
for account in &ctx.remaining_accounts[(pools.len() + 1)..] {
let is_signer = account.key == &ctx.accounts.transfer_authority.key();
router_accounts.push(if account.is_writable {
AccountMeta::new(*account.key, is_signer)
} else {
AccountMeta::new_readonly(*account.key, is_signer)
});
}
let instruction = Instruction {
program_id: router_program,
accounts: router_accounts,
data: params.router_instruction_data.clone(),
};
program::invoke_signed(
&instruction,
&ctx.remaining_accounts[(pools.len() + 1)..],
authority_seeds,
)?;
ctx.accounts.custody_token_a.reload()?;
ctx.accounts.custody_token_b.reload()?;
let token_a_balance = ctx.accounts.custody_token_a.amount;
let token_b_balance = ctx.accounts.custody_token_b.amount;
if token_a_balance > initial_token_a_balance {
token_a_change = math::checked_sub(token_a_balance, initial_token_a_balance)?;
token_b_change = math::checked_sub(initial_token_b_balance, token_b_balance)?;
swap_amount = token_b_change;
receive_amount = token_a_change;
supply_side = MatchingSide::Sell;
} else {
token_a_change = math::checked_sub(initial_token_a_balance, token_a_balance)?;
token_b_change = math::checked_sub(token_b_balance, initial_token_b_balance)?;
swap_amount = token_a_change;
receive_amount = token_b_change;
supply_side = MatchingSide::Buy;
}
require_gte!(
token_a_change,
std::cmp::max(token_pair.config_a.min_swap_amount, 1),
TwammError::SettlementAmountTooSmall
);
require_gte!(
token_b_change,
std::cmp::max(token_pair.config_b.min_swap_amount, 1),
TwammError::SettlementAmountTooSmall
);
msg!("Validate swap price");
swap_price = OraclePrice::new_from_token(math::checked_token_div(
token_b_change,
token_pair.config_b.decimals,
token_a_change,
token_pair.config_a.decimals,
)?);
let swap_price_f64 = swap_price.checked_as_f64()?;
let oracle_price_f64 = oracle_price.checked_as_f64()?;
if (supply_side == MatchingSide::Sell && oracle_price_f64 < swap_price_f64)
|| (supply_side == MatchingSide::Buy && oracle_price_f64 > swap_price_f64)
{
require_gte!(
token_pair.max_swap_price_diff,
math::checked_float_div(
(oracle_price_f64 - swap_price_f64).abs(),
oracle_price_f64
)?,
TwammError::SettlementPriceOutOfBounds
);
}
} else {
token_a_change = 0;
token_b_change = 0;
swap_amount = 0;
receive_amount = 0;
supply_side = MatchingSide::Internal;
swap_price = oracle_price;
}
msg!("Settle pools");
let current_time = token_pair.get_time()?;
let mut pool_refs: Vec<&mut Pool> = Vec::with_capacity(pools.len());
for pool in pools.iter_mut() {
pool_refs.push(pool);
}
let res = token_pair.settle_pools(
&mut pool_refs,
supply_side,
token_a_change,
token_b_change,
swap_price,
oracle_price,
current_time,
)?;
msg!("Validate settled amounts");
if swap_amount > 0 {
require!(
res.settlement_side != supply_side,
TwammError::InvalidSettlementSide
);
require!(
res.net_amount_settled > 0,
TwammError::SettlementAmountTooLarge
);
}
require_eq!(
swap_amount,
res.net_amount_settled,
TwammError::SettlementAmountTooLarge
);
require_eq!(
receive_amount,
res.source_amount_received,
TwammError::SettlementError
);
require_gte!(
res.net_amount_required,
res.net_amount_settled,
TwammError::SettlementError
);
let swap_amount_threshold = if res.settlement_side == MatchingSide::Sell {
std::cmp::max(token_pair.config_a.min_swap_amount, 1)
} else {
std::cmp::max(token_pair.config_b.min_swap_amount, 1)
};
let unsettled_amount = math::checked_sub(res.net_amount_required, res.net_amount_settled)?;
let unsettled_percent = if unsettled_amount <= swap_amount_threshold {
0.0
} else {
math::checked_float_div(unsettled_amount as f64, res.net_amount_required as f64)?
};
if swap_amount > 0 {
require_gte!(
token_pair.max_unsettled_amount,
unsettled_percent,
TwammError::SettlementAmountTooSmall
);
}
msg!("Update pool states");
for pool in pools.iter_mut() {
pool.update_state(token_pair.min_time_till_expiration, current_time)?;
if pool.is_complete(current_time)? {
token_pair.finalize_pool(
pool,
&pool.to_account_info(),
&ctx.accounts.transfer_authority,
)?;
}
}
token_pair.save_pools(&pools)?;
msg!("Update token pair stats");
token_pair.update_trade_stats(
&res,
SettlementType::Crank,
&ctx.accounts.oracle_token_a,
&ctx.accounts.oracle_token_b,
)?;
msg!("Transfer rewards to the transaction payer");
assert!((0.0..=1.0).contains(&unsettled_percent));
let reward_share = 1.0 - unsettled_percent;
let reward_a = std::cmp::min(
token_pair.stats_a.fees_collected,
math::checked_as_u64(math::checked_float_mul(
reward_share,
token_pair.config_a.crank_reward as f64,
)?)?,
);
if reward_a > 0 {
token_pair.stats_a.fees_collected =
math::checked_sub(token_pair.stats_a.fees_collected, reward_a)?;
token_pair.transfer_tokens(
ctx.accounts.custody_token_a.to_account_info(),
ctx.accounts.user_account_token_a.to_account_info(),
ctx.accounts.transfer_authority.clone(),
ctx.accounts.token_program.to_account_info(),
reward_a,
)?;
}
let reward_b = std::cmp::min(
token_pair.stats_b.fees_collected,
math::checked_as_u64(math::checked_float_mul(
reward_share,
token_pair.config_b.crank_reward as f64,
)?)?,
);
if reward_b > 0 {
token_pair.stats_b.fees_collected =
math::checked_sub(token_pair.stats_b.fees_collected, reward_b)?;
token_pair.transfer_tokens(
ctx.accounts.custody_token_b.to_account_info(),
ctx.accounts.user_account_token_b.to_account_info(),
ctx.accounts.transfer_authority.clone(),
ctx.accounts.token_program.to_account_info(),
reward_b,
)?;
}
let net_amount_required = if res.net_amount_required >= i64::MAX as u64 {
i64::MAX
} else {
res.net_amount_required as i64
};
match res.settlement_side {
MatchingSide::Internal => Ok(0),
MatchingSide::Buy => Ok(net_amount_required),
MatchingSide::Sell => Ok(-net_amount_required),
}
}