From 313d13e3005960b01f878bff75e9dfe50137e6de Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Mon, 1 Feb 2021 22:46:53 -0800 Subject: [PATCH] examples: Add dex-crank-relay --- examples/dex-crank-relay/Anchor.toml | 2 + examples/dex-crank-relay/Cargo.toml | 4 + examples/dex-crank-relay/migrations/deploy.js | 13 + .../programs/dex-crank-relay/Cargo.toml | 22 ++ .../programs/dex-crank-relay/Xargo.toml | 2 + .../programs/dex-crank-relay/src/lib.rs | 312 ++++++++++++++++++ .../dex-crank-relay/tests/dex-crank-relay.js | 14 + 7 files changed, 369 insertions(+) create mode 100644 examples/dex-crank-relay/Anchor.toml create mode 100644 examples/dex-crank-relay/Cargo.toml create mode 100644 examples/dex-crank-relay/migrations/deploy.js create mode 100644 examples/dex-crank-relay/programs/dex-crank-relay/Cargo.toml create mode 100644 examples/dex-crank-relay/programs/dex-crank-relay/Xargo.toml create mode 100644 examples/dex-crank-relay/programs/dex-crank-relay/src/lib.rs create mode 100644 examples/dex-crank-relay/tests/dex-crank-relay.js diff --git a/examples/dex-crank-relay/Anchor.toml b/examples/dex-crank-relay/Anchor.toml new file mode 100644 index 00000000..0bb269b1 --- /dev/null +++ b/examples/dex-crank-relay/Anchor.toml @@ -0,0 +1,2 @@ +cluster = "devnet" +wallet = "~/.config/solana/id.json" diff --git a/examples/dex-crank-relay/Cargo.toml b/examples/dex-crank-relay/Cargo.toml new file mode 100644 index 00000000..a60de986 --- /dev/null +++ b/examples/dex-crank-relay/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/examples/dex-crank-relay/migrations/deploy.js b/examples/dex-crank-relay/migrations/deploy.js new file mode 100644 index 00000000..7cca2719 --- /dev/null +++ b/examples/dex-crank-relay/migrations/deploy.js @@ -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. +} diff --git a/examples/dex-crank-relay/programs/dex-crank-relay/Cargo.toml b/examples/dex-crank-relay/programs/dex-crank-relay/Cargo.toml new file mode 100644 index 00000000..02d50819 --- /dev/null +++ b/examples/dex-crank-relay/programs/dex-crank-relay/Cargo.toml @@ -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" \ No newline at end of file diff --git a/examples/dex-crank-relay/programs/dex-crank-relay/Xargo.toml b/examples/dex-crank-relay/programs/dex-crank-relay/Xargo.toml new file mode 100644 index 00000000..1744f098 --- /dev/null +++ b/examples/dex-crank-relay/programs/dex-crank-relay/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/examples/dex-crank-relay/programs/dex-crank-relay/src/lib.rs b/examples/dex-crank-relay/programs/dex-crank-relay/src/lib.rs new file mode 100644 index 00000000..2c14f71c --- /dev/null +++ b/examples/dex-crank-relay/programs/dex-crank-relay/src/lib.rs @@ -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, reward_bucket: RewardBucket) -> Result<()> { + (*ctx.accounts.reward_bucket) = reward_bucket; + Ok(()) + } + + pub fn set_stake_threshold(ctx: Context, threshold: u64) -> Result<()> { + ctx.accounts.reward_bucket.stake_threshold = threshold; + Ok(()) + } + + pub fn set_fee_rate(ctx: Context, fee_rate: u64) -> Result<()> { + ctx.accounts.reward_bucket.fee_rate = fee_rate; + Ok(()) + } + + pub fn set_authority(ctx: Context, new_authority: Pubkey) -> Result<()> { + ctx.accounts.reward_bucket.authority = new_authority; + Ok(()) + } + + pub fn set_dex(ctx: Context, new_dex_program: Pubkey) -> Result<()> { + ctx.accounts.reward_bucket.dex_program = new_dex_program; + Ok(()) + } + + pub fn set_registrar(ctx: Context, new_registrar: Pubkey) -> Result<()> { + ctx.accounts.reward_bucket.registrar = new_registrar; + Ok(()) + } + + pub fn set_registry_program(ctx: Context, new_registry_program: Pubkey) -> Result<()> { + ctx.accounts.reward_bucket.registry_program = new_registry_program; + Ok(()) + } + + pub fn migrate(ctx: Context) -> 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 = (&*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, + ) -> 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::>(); + 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 = (&*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) -> 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) -> 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 +} diff --git a/examples/dex-crank-relay/tests/dex-crank-relay.js b/examples/dex-crank-relay/tests/dex-crank-relay.js new file mode 100644 index 00000000..0d6c7d1a --- /dev/null +++ b/examples/dex-crank-relay/tests/dex-crank-relay.js @@ -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); + }); +});