examples: Add dex-crank-relay

This commit is contained in:
Armani Ferrante 2021-02-01 22:46:53 -08:00
parent 24e92f29d7
commit 313d13e300
No known key found for this signature in database
GPG Key ID: D597A80BCF8E12B7
7 changed files with 369 additions and 0 deletions

View File

@ -0,0 +1,2 @@
cluster = "devnet"
wallet = "~/.config/solana/id.json"

View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

View File

@ -0,0 +1,13 @@
// Migrations are an early feature. Currently, they're nothing more than this
// single deploy script that's invoked from the CLI, injecting a provider
// configured from the workspace's Anchor.toml.
const anchor = require("@project-serum/anchor");
module.exports = async function (provider) {
// Configure client to use the provider.
anchor.setProvider(provider);
// Add your deploy script here.
}

View File

@ -0,0 +1,22 @@
[package]
name = "dex-crank-relay"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "dex_crank_relay"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
anchor-spl = { git = "https://github.com/project-serum/anchor" }
registry = { path = "../../../lockup/programs/registry", features = ["cpi"] }
serum_dex = { git = "https://github.com/project-serum/serum-dex", features = ["no-entrypoint"] }
enumflags2 = "0.6.4"

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -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
}

View File

@ -0,0 +1,14 @@
const anchor = require('@project-serum/anchor');
describe('dex-crank-relay', () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.Provider.env());
it('Is initialized!', async () => {
// Add your test here.
const program = anchor.workspace.DexCrankRelay;
const tx = await program.rpc.initialize();
console.log("Your transaction signature", tx);
});
});