|
|
|
@ -0,0 +1,312 @@
|
|
|
|
|
//! A relatively advanced example. If new to Anchor, it's recommended to start
|
|
|
|
|
//! with other examples, first.
|
|
|
|
|
//!
|
|
|
|
|
//! dex-crank-relay is a proxy program that relays a `ConsumeEvents` instruction
|
|
|
|
|
//! to the DEX, counts the number of events processed, and pays out a
|
|
|
|
|
//! transaction fee as a function of `fee = fee_rate * num_events_consumed`.
|
|
|
|
|
//!
|
|
|
|
|
//! To be eligible for the reward, one must first own `stake_threshold` staking
|
|
|
|
|
//! pool tokens on the configured staking "registrar".
|
|
|
|
|
|
|
|
|
|
#![feature(proc_macro_hygiene)]
|
|
|
|
|
|
|
|
|
|
use anchor_lang::prelude::*;
|
|
|
|
|
use anchor_lang::solana_program;
|
|
|
|
|
use anchor_lang::solana_program::instruction::Instruction;
|
|
|
|
|
use anchor_spl::token::{self, TokenAccount, Transfer};
|
|
|
|
|
use enumflags2::BitFlags;
|
|
|
|
|
use registry::{Member, Registrar};
|
|
|
|
|
use serum_dex::state::AccountFlag;
|
|
|
|
|
|
|
|
|
|
#[program]
|
|
|
|
|
pub mod dex_crank_relay {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
pub fn create_reward(ctx: Context<CreateReward>, reward_bucket: RewardBucket) -> Result<()> {
|
|
|
|
|
(*ctx.accounts.reward_bucket) = reward_bucket;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_stake_threshold(ctx: Context<Auth>, threshold: u64) -> Result<()> {
|
|
|
|
|
ctx.accounts.reward_bucket.stake_threshold = threshold;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_fee_rate(ctx: Context<Auth>, fee_rate: u64) -> Result<()> {
|
|
|
|
|
ctx.accounts.reward_bucket.fee_rate = fee_rate;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_authority(ctx: Context<Auth>, new_authority: Pubkey) -> Result<()> {
|
|
|
|
|
ctx.accounts.reward_bucket.authority = new_authority;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_dex(ctx: Context<Auth>, new_dex_program: Pubkey) -> Result<()> {
|
|
|
|
|
ctx.accounts.reward_bucket.dex_program = new_dex_program;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_registrar(ctx: Context<Auth>, new_registrar: Pubkey) -> Result<()> {
|
|
|
|
|
ctx.accounts.reward_bucket.registrar = new_registrar;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_registry_program(ctx: Context<Auth>, new_registry_program: Pubkey) -> Result<()> {
|
|
|
|
|
ctx.accounts.reward_bucket.registry_program = new_registry_program;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn migrate(ctx: Context<Migrate>) -> Result<()> {
|
|
|
|
|
let seeds = [
|
|
|
|
|
ctx.accounts.reward_bucket.to_account_info().key.as_ref(),
|
|
|
|
|
&[ctx.accounts.reward_bucket.nonce],
|
|
|
|
|
];
|
|
|
|
|
let signer = &[&seeds[..]];
|
|
|
|
|
let cpi_ctx: CpiContext<Transfer> = (&*ctx.accounts).into();
|
|
|
|
|
token::transfer(cpi_ctx.with_signer(signer), ctx.accounts.vault.amount)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[access_control(CrankRelay::accounts(&ctx))]
|
|
|
|
|
pub fn crank_relay<'a, 'b, 'c, 'info>(
|
|
|
|
|
ctx: Context<'a, 'b, 'c, 'info, CrankRelay<'info>>,
|
|
|
|
|
dex_data: Vec<u8>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
if !is_staked(&ctx) {
|
|
|
|
|
return Err(ErrorCode::InsufficientStake.into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Event queue len before.
|
|
|
|
|
let before_event_count = event_q_len(
|
|
|
|
|
&ctx.accounts
|
|
|
|
|
.dex_event_q
|
|
|
|
|
.to_account_info()
|
|
|
|
|
.try_borrow_data()?,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Invoke crank relay.
|
|
|
|
|
{
|
|
|
|
|
let dex_instruction = {
|
|
|
|
|
let relay_meta_accs = ctx
|
|
|
|
|
.remaining_accounts
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|acc_info| {
|
|
|
|
|
if acc_info.is_writable {
|
|
|
|
|
AccountMeta::new(*acc_info.key, acc_info.is_signer)
|
|
|
|
|
} else {
|
|
|
|
|
AccountMeta::new_readonly(*acc_info.key, acc_info.is_signer)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<AccountMeta>>();
|
|
|
|
|
Instruction {
|
|
|
|
|
program_id: *ctx.accounts.dex_program.key,
|
|
|
|
|
accounts: relay_meta_accs,
|
|
|
|
|
data: dex_data,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let mut relay_accs = vec![ctx.accounts.dex_program.clone()];
|
|
|
|
|
relay_accs.extend_from_slice(ctx.remaining_accounts);
|
|
|
|
|
|
|
|
|
|
solana_program::program::invoke(&dex_instruction, &relay_accs)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Event queue len after.
|
|
|
|
|
let after_event_count = event_q_len(
|
|
|
|
|
&ctx.accounts
|
|
|
|
|
.dex_event_q
|
|
|
|
|
.to_account_info()
|
|
|
|
|
.try_borrow_data()?,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Calculate crank fee.
|
|
|
|
|
let fee = {
|
|
|
|
|
assert!(before_event_count >= after_event_count);
|
|
|
|
|
let num_events = before_event_count - after_event_count;
|
|
|
|
|
let fee = num_events * ctx.accounts.reward_bucket.fee_rate;
|
|
|
|
|
if ctx.accounts.vault.amount < fee {
|
|
|
|
|
msg!("vault depleted");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
fee
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Pay out reward.
|
|
|
|
|
let seeds = [
|
|
|
|
|
ctx.accounts.reward_bucket.to_account_info().key.as_ref(),
|
|
|
|
|
&[ctx.accounts.reward_bucket.nonce],
|
|
|
|
|
];
|
|
|
|
|
let signer = &[&seeds[..]];
|
|
|
|
|
let cpi_ctx: CpiContext<Transfer> = (&*ctx.accounts).into();
|
|
|
|
|
token::transfer(cpi_ctx.with_signer(signer), fee)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Accounts)]
|
|
|
|
|
pub struct CreateReward<'info> {
|
|
|
|
|
#[account(init)]
|
|
|
|
|
reward_bucket: ProgramAccount<'info, RewardBucket>,
|
|
|
|
|
rent: Sysvar<'info, Rent>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Accounts)]
|
|
|
|
|
pub struct Auth<'info> {
|
|
|
|
|
#[account(mut, has_one = authority)]
|
|
|
|
|
reward_bucket: ProgramAccount<'info, RewardBucket>,
|
|
|
|
|
#[account(signer)]
|
|
|
|
|
authority: AccountInfo<'info>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Accounts)]
|
|
|
|
|
pub struct Migrate<'info> {
|
|
|
|
|
#[account(mut, has_one = vault, has_one = authority)]
|
|
|
|
|
reward_bucket: ProgramAccount<'info, RewardBucket>,
|
|
|
|
|
#[account(seeds = [
|
|
|
|
|
reward_bucket.to_account_info().key.as_ref(),
|
|
|
|
|
&[reward_bucket.nonce],
|
|
|
|
|
])]
|
|
|
|
|
reward_bucket_signer: AccountInfo<'info>,
|
|
|
|
|
#[account(signer)]
|
|
|
|
|
authority: AccountInfo<'info>,
|
|
|
|
|
#[account(mut)]
|
|
|
|
|
vault: CpiAccount<'info, TokenAccount>,
|
|
|
|
|
#[account(mut)]
|
|
|
|
|
to: AccountInfo<'info>,
|
|
|
|
|
#[account("token_program.key == &token::ID")]
|
|
|
|
|
token_program: AccountInfo<'info>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Accounts)]
|
|
|
|
|
pub struct CrankRelay<'info> {
|
|
|
|
|
// Reward bucket.
|
|
|
|
|
#[account(
|
|
|
|
|
has_one = vault,
|
|
|
|
|
has_one = registrar,
|
|
|
|
|
has_one = registry_program,
|
|
|
|
|
has_one = dex_program,
|
|
|
|
|
)]
|
|
|
|
|
reward_bucket: ProgramAccount<'info, RewardBucket>,
|
|
|
|
|
#[account(
|
|
|
|
|
seeds = [
|
|
|
|
|
reward_bucket.to_account_info().key.as_ref(),
|
|
|
|
|
&[reward_bucket.nonce],
|
|
|
|
|
]
|
|
|
|
|
)]
|
|
|
|
|
reward_bucket_signer: AccountInfo<'info>,
|
|
|
|
|
vault: CpiAccount<'info, TokenAccount>,
|
|
|
|
|
|
|
|
|
|
// Stake registry. Since they're CPI accounts, make sure to check owners
|
|
|
|
|
// so that we can avoid actually invoking the CPI and instead just read the
|
|
|
|
|
// accounts.
|
|
|
|
|
registry_program: AccountInfo<'info>,
|
|
|
|
|
#[account("registrar.to_account_info().owner == registry_program.key")]
|
|
|
|
|
registrar: CpiAccount<'info, Registrar>,
|
|
|
|
|
#[account(
|
|
|
|
|
belongs_to = registrar,
|
|
|
|
|
"member.to_account_info().owner == registry_program.key"
|
|
|
|
|
)]
|
|
|
|
|
member: CpiAccount<'info, Member>,
|
|
|
|
|
#[account("member_spt.to_account_info().key == &member.balances.spt")]
|
|
|
|
|
member_spt: CpiAccount<'info, TokenAccount>,
|
|
|
|
|
#[account("member_locked_spt.to_account_info().key == &member.balances_locked.spt")]
|
|
|
|
|
member_locked_spt: CpiAccount<'info, TokenAccount>,
|
|
|
|
|
|
|
|
|
|
// DEX.
|
|
|
|
|
#[account("dex_event_q.owner == dex_program.key")]
|
|
|
|
|
dex_event_q: AccountInfo<'info>,
|
|
|
|
|
dex_program: AccountInfo<'info>,
|
|
|
|
|
|
|
|
|
|
// Pay reward to.
|
|
|
|
|
#[account(mut)]
|
|
|
|
|
to: AccountInfo<'info>,
|
|
|
|
|
#[account("token_program.key == &token::ID")]
|
|
|
|
|
token_program: AccountInfo<'info>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'info> CrankRelay<'info> {
|
|
|
|
|
pub fn accounts(ctx: &Context<CrankRelay>) -> Result<()> {
|
|
|
|
|
let data = ctx.accounts.dex_event_q.try_borrow_data()?;
|
|
|
|
|
|
|
|
|
|
// b"serum" || account_flags;
|
|
|
|
|
let mut raw_flags = [0u8; 8];
|
|
|
|
|
raw_flags.copy_from_slice(&data[5..13]);
|
|
|
|
|
let account_flags = BitFlags::from_bits(u64::from_le_bytes(raw_flags))
|
|
|
|
|
.map_err(|_| ErrorCode::UnparseableAccountFlags)?;
|
|
|
|
|
if account_flags != (AccountFlag::Initialized | AccountFlag::EventQueue) {
|
|
|
|
|
return Err(ErrorCode::InvalidEventQueue.into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[account]
|
|
|
|
|
pub struct RewardBucket {
|
|
|
|
|
vault: Pubkey,
|
|
|
|
|
nonce: u8,
|
|
|
|
|
registrar: Pubkey,
|
|
|
|
|
registry_program: Pubkey,
|
|
|
|
|
dex_program: Pubkey,
|
|
|
|
|
authority: Pubkey,
|
|
|
|
|
fee_rate: u64,
|
|
|
|
|
stake_threshold: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_staked(ctx: &Context<CrankRelay>) -> bool {
|
|
|
|
|
let total_staked = ctx.accounts.member_spt.amount + ctx.accounts.member_locked_spt.amount;
|
|
|
|
|
let stake_threshold = ctx.accounts.reward_bucket.stake_threshold;
|
|
|
|
|
if total_staked < stake_threshold {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[error]
|
|
|
|
|
pub enum ErrorCode {
|
|
|
|
|
#[msg("Please stake more to be eligible for crank transaction fees.")]
|
|
|
|
|
InsufficientStake,
|
|
|
|
|
#[msg("Event queue account does not have valid account flags.")]
|
|
|
|
|
InvalidEventQueue,
|
|
|
|
|
#[msg("Unable to parse DEX event queue account flags.")]
|
|
|
|
|
UnparseableAccountFlags,
|
|
|
|
|
Unknown,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a, 'b, 'c, 'info> From<&Migrate<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
|
|
|
|
|
fn from(accounts: &Migrate<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
|
|
|
|
|
let cpi_accounts = Transfer {
|
|
|
|
|
from: accounts.vault.to_account_info().clone(),
|
|
|
|
|
to: accounts.to.to_account_info(),
|
|
|
|
|
authority: accounts.reward_bucket_signer.clone(),
|
|
|
|
|
};
|
|
|
|
|
let cpi_program = accounts.token_program.clone();
|
|
|
|
|
CpiContext::new(cpi_program, cpi_accounts)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a, 'b, 'c, 'info> From<&CrankRelay<'info>>
|
|
|
|
|
for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
|
|
|
|
|
{
|
|
|
|
|
fn from(accounts: &CrankRelay<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
|
|
|
|
|
let cpi_accounts = Transfer {
|
|
|
|
|
from: accounts.vault.to_account_info().clone(),
|
|
|
|
|
to: accounts.to.to_account_info(),
|
|
|
|
|
authority: accounts.reward_bucket_signer.clone(),
|
|
|
|
|
};
|
|
|
|
|
let cpi_program = accounts.token_program.clone();
|
|
|
|
|
CpiContext::new(cpi_program, cpi_accounts)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the length of the Serum DEX event queue account represented by the
|
|
|
|
|
// given `data`.
|
|
|
|
|
fn event_q_len(data: &[u8]) -> u64 {
|
|
|
|
|
// b"serum" || account_flags || head.
|
|
|
|
|
let count_start = 5 + 8 + 8;
|
|
|
|
|
let count_end = count_start + 4;
|
|
|
|
|
let mut b = [0u8; 4];
|
|
|
|
|
b.copy_from_slice(&data[count_start..count_end]);
|
|
|
|
|
u32::from_le_bytes(b) as u64
|
|
|
|
|
}
|