Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Withdraw Instruction #15

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
program/target
scripts/node_modules
scripts/dist
scripts/dist

.DS_Store
8 changes: 8 additions & 0 deletions program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ pub enum SubscriptionError {
InvalidReceiver,
#[error("Already expired.")]
AlreadyExpired,
#[error("Invalid vault owner.")]
InvalidVaultOwner,
#[error("Invalid mint owner.")]
InvalidMintOwner,
#[error("Invalid subscription owner.")]
InvalidSubscriptionOwner,
#[error("Account balance insufficient for requested withdraw amount.")]
InsufficientWithdrawBalance,
}

impl From<SubscriptionError> for ProgramError {
Expand Down
29 changes: 15 additions & 14 deletions program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ pub enum SubscriptionInstruction {
///
/// 0. `[writable, signer]` user
/// 1. `[writable]` (PDA) metadata counter
/// 1. `[writable]` (PDA) subscription metadata
/// 2. `[writable]` (PDA) deposit vault
/// 3. `[]` (PDA) deposit vault mint
/// 4. `[]` system program
/// 5. `[]` sysvar rent
/// 6. `[]` token program
/// 7. `[]` associated token program
/// 2. `[writable]` (PDA) subscription metadata
/// 3. `[writable]` (PDA) deposit vault
/// 4. `[]` (PDA) deposit vault mint
/// 5. `[]` system program
/// 6. `[]` sysvar rent
/// 7. `[]` token program
/// 8. `[]` associated token program
///
Initialize {
payee: Pubkey,
Expand All @@ -37,18 +37,19 @@ pub enum SubscriptionInstruction {
///
Deposit { amount: u64 },

/// Wrapper on transfer function. Withdraws token from deposit vault
/// as long as caller is rightful owner.
/// Wrapper on transfer function. Withdraws token from deposit vault as long as the
/// the caller is the owner of the subscription associated with that deposit vault.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable, signer]` payer
/// 1. `[writable]` payer token account
/// 2. `[writable]` deposit vault
/// 3. `[]` subscription metadata
/// 4. `[]` token program for token transfers
/// 1. `[writable]` (PDA) payer subscription token account
/// 4. `[writable]` (PDA) payer deposit token account
/// 2. `[writable]` (PDA) deposit vault
/// 5. `[]` (PDA) subscription metadata
/// 6. `[]` token program for token transfers
///
Withdraw { amount: u64 },
Withdraw { withdraw_amount: u64, count: u64 },

/// Renews or deactivates a provided subscription.
///
Expand Down
8 changes: 6 additions & 2 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ use {
const FEE: u64 = 1;
const FEE_DECIMALS: u8 = 2;

pub mod withdraw;

pub struct Processor {}

impl Processor {
Expand Down Expand Up @@ -205,9 +207,11 @@ impl Processor {
msg!("Instruction: Deposit");
msg!("amount: {}", amount);
}
SubscriptionInstruction::Withdraw { amount } => {
SubscriptionInstruction::Withdraw { withdraw_amount, count } => {
msg!("Instruction: Withdraw");
msg!("amount: {}", amount);
msg!("withdraw_amount: {}", withdraw_amount);
msg!("count: {}", count);
withdraw::process_withdraw(program_id, accounts, withdraw_amount, count)?;
}
SubscriptionInstruction::Renew { count } => {
msg!("Instruction: Renew");
Expand Down
110 changes: 110 additions & 0 deletions program/src/processor/withdraw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use {
crate::{
error::SubscriptionError,
instruction::SubscriptionInstruction,
state::{Subscription},
utils::{
assert_msg, check_initialized_ata, check_pda, check_signer, check_writable,
},
},
borsh::{BorshDeserialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::{invoke_signed},
program_pack::Pack,
pubkey::Pubkey,
},
spl_token::{state::Account as TokenAccount},
};

// TODO: store `count` parameter in subscription metadata (struct) and remove the paramter
pub fn process_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], withdraw_amount: u64, count: u64) -> ProgramResult {
let account_info_iter = &mut accounts.iter();

let payer_ai = next_account_info(account_info_iter)?;
let payer_token_account_ai = next_account_info(account_info_iter)?; // SPL token account
let payer_vault_ai = next_account_info(account_info_iter)?;
let deposit_vault_ai = next_account_info(account_info_iter)?;
let subscription_ai = next_account_info(account_info_iter)?;
let token_program_ai = next_account_info(account_info_iter)?;

check_signer(payer_ai);
[payer_ai, payer_vault_ai, deposit_vault_ai].iter().map(|x| check_writable(x));

// Deserialize token account
let deposit_vault = TokenAccount::unpack_from_slice(&deposit_vault_ai.try_borrow_data()?)?;
let payer_token_account = TokenAccount::unpack_from_slice(&payer_token_account_ai.try_borrow_data()?)?;
let payer_vault = TokenAccount::unpack_from_slice(&payer_vault_ai.try_borrow_data()?)?;

// Get subscription data
let subscription = Subscription::try_from_slice(&subscription_ai.try_borrow_data()?)?;
let duration = subscription.duration;
let payee = subscription.payee;

// Validations

// Check payer token account
check_initialized_ata(payer_vault_ai, payer_ai.key, &subscription.deposit_vault)?;

// Check that caller is the rightful owner, ie. owner (payer) of the subscription
if let Some(current_mint) = subscription.mint {
check_initialized_ata(payer_token_account_ai, payer_ai.key, &current_mint)?;
assert_msg(
payer_token_account.amount > 0,
SubscriptionError::InvalidSubscriptionOwner.into(),
"Invalid subscription owner. Only the owner of a subscription associated with the deposit vault can withdraw.",
)?;
}

assert_msg(
deposit_vault.amount > withdraw_amount,
SubscriptionError::InsufficientWithdrawBalance.into(),
"Insufficient funds to withdraw.",
)?;

msg!("Transferring the requested fund to the owner...");

let instruction = &spl_token::instruction::transfer(
&spl_token::id(),
deposit_vault_ai.key,
payer_vault_ai.key,
payer_ai.key,
&[],
withdraw_amount,
)?;

let subscription_seeds = &[
b"subscription_metadata",
payee.as_ref(),
&subscription.amount.to_le_bytes(),
&duration.to_le_bytes(),
&count.to_le_bytes(),
];

check_pda(subscription_ai, subscription_seeds, program_id)?;

let (_, subscription_bump) = Pubkey::find_program_address(subscription_seeds, program_id);

let subscription_seeds = &[
b"subscription_metadata",
payee.as_ref(),
&subscription.amount.to_le_bytes(),
&duration.to_le_bytes(),
&count.to_le_bytes(),
&[subscription_bump],
];

invoke_signed(
instruction,
&[
deposit_vault_ai.clone(),
payer_vault_ai.clone(),
subscription_ai.clone(),
],
&[subscription_seeds],
)?;

Ok(())
}
57 changes: 27 additions & 30 deletions program/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

use num_derive::FromPrimitive;
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
Expand All @@ -17,46 +18,43 @@ pub fn assert_msg(statement: bool, err: ProgramError, msg: &str) -> ProgramResul
}

pub fn check_signer(account: &AccountInfo) -> ProgramResult {
if !account.is_signer {
msg!("Missing required signature on account: {}", account.key);
Err(ProgramError::MissingRequiredSignature)
} else {
Ok(())
}
assert_msg(
account.is_signer,
ProgramError::MissingRequiredSignature,
&format!("Missing required signature on account: {}", account.key),
)
}

pub fn check_writable(account: &AccountInfo) -> ProgramResult {
if !account.is_writable {
msg!("Account should be writable: {}", account.key);
Err(ProgramError::MissingRequiredSignature)
} else {
Ok(())
}
assert_msg(
account.is_writable,
ProgramError::MissingRequiredSignature,
&format!("Account should be writable: {}", account.key),
)
}

pub fn check_pda(account: &AccountInfo, seeds: &[&[u8]], program_id: &Pubkey) -> ProgramResult {
let (pda, _) = Pubkey::find_program_address(seeds, program_id);
if *account.key != pda {
msg!("Invalid PDA:\tExpected: {}\tGot: {}", &pda, account.key);
Err(UtilsError::InvalidProgramAddress.into())
} else {
Ok(())
}
assert_msg(
*account.key == pda,
UtilsError::InvalidProgramAddress.into(),
&format!("Invalid PDA:\tExpected: {}\tGot: {}", &pda, account.key),
)
}

/// Validate that given acount is indeed the associated token address of user (and mint) address
pub fn check_ata(
account: &AccountInfo,
user_address: &Pubkey,
mint_address: &Pubkey,
) -> ProgramResult {
// check pda
let ata = get_associated_token_address(user_address, mint_address);
if *account.key != ata {
msg!("Invalid ATA address:\tExpected: {}\tGot: {}", &ata, account.key);
Err(UtilsError::InvalidProgramAddress.into())
} else {
Ok(())
}
assert_msg(
*account.key == ata,
UtilsError::InvalidProgramAddress.into(),
&format!("Invalid ATA address:\tExpected: {}\tGot: {}", &ata, account.key)
)
}

pub fn check_initialized_ata(
Expand Down Expand Up @@ -84,12 +82,11 @@ pub fn check_initialized_ata(
}

pub fn check_program_id(account: &AccountInfo, program_id: &Pubkey) -> ProgramResult {
if *account.key != *program_id {
msg!("Invalid program id:\tExpected: {}\tGot: {}", program_id, account.key);
Err(ProgramError::IncorrectProgramId)
} else {
Ok(())
}
assert_msg(
*account.key == *program_id,
ProgramError::IncorrectProgramId,
&format!("Invalid program id:\tExpected: {}\tGot: {}", program_id, account.key)
)
}

#[derive(Error, Debug, Copy, Clone, FromPrimitive, PartialEq)]
Expand Down