diff --git a/src/bank.rs b/src/bank.rs index 197676114d..d8f1187a5b 100644 --- a/src/bank.rs +++ b/src/bank.rs @@ -32,6 +32,7 @@ use system_transaction::SystemTransaction; use tictactoe_dashboard_program::TicTacToeDashboardProgram; use tictactoe_program::TicTacToeProgram; use timing::{duration_as_us, timestamp}; +use token_program::TokenProgram; use transaction::Transaction; use window::WINDOW_SIZE; @@ -532,6 +533,11 @@ impl Bank { { return Err(BankError::ProgramRuntimeError(instruction_index as u8)); } + } else if TokenProgram::check_id(&tx_program_id) { + if TokenProgram::process_transaction(&tx, instruction_index, program_accounts).is_err() + { + return Err(BankError::ProgramRuntimeError(instruction_index as u8)); + } } else if self.loaded_contract(tx_program_id, tx, instruction_index, program_accounts) { } else { return Err(BankError::UnknownContractId(instruction_index as u8)); diff --git a/src/lib.rs b/src/lib.rs index 7cc4086a27..62ebb34348 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,7 @@ pub mod thin_client; pub mod tictactoe_dashboard_program; pub mod tictactoe_program; pub mod timing; +pub mod token_program; pub mod tpu; pub mod transaction; pub mod tvu; diff --git a/src/token_program.rs b/src/token_program.rs new file mode 100644 index 0000000000..b5c1e8ec76 --- /dev/null +++ b/src/token_program.rs @@ -0,0 +1,462 @@ +//! ERC20-like Token program + +use bincode; + +use solana_program_interface::account::Account; +use solana_program_interface::pubkey::Pubkey; +use std; +use transaction::Transaction; + +#[derive(Debug, PartialEq)] +pub enum Error { + InvalidArgument, + InsufficentFunds, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "error") + } +} +impl std::error::Error for Error {} + +pub type Result = std::result::Result; + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct TokenInfo { + /** + * Total supply of tokens + */ + supply: u64, + + /** + * Number of base 10 digits to the right of the decimal place in the total supply + */ + decimals: u8, + + /** + * Descriptive name of this token + */ + name: String, + + /** + * Symbol for this token + */ + symbol: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] +pub struct TokenAccountInfo { + /** + * The kind of token this account holds + */ + token: Pubkey, + + /** + * Owner of this account + */ + owner: Pubkey, + + /** + * Amount of tokens this account holds + */ + amount: u64, + + /** + * The source account for the tokens. + * + * If `source` is None, `amount` belongs to this account. + * If `source` is Option<>, `amount` represents an allowance of tokens that + * may be transferred from the `source` account. + */ + source: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +enum Command { + NewToken(TokenInfo), + NewTokenAccount(), + Transfer(u64), + Approve(u64), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub enum TokenProgram { + Unallocated, + Token(TokenInfo), + Account(TokenAccountInfo), + Invalid, +} +impl Default for TokenProgram { + fn default() -> TokenProgram { + TokenProgram::Unallocated + } +} + +pub const TOKEN_PROGRAM_ID: [u8; 32] = [ + 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +impl TokenProgram { + #[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] + fn map_to_invalid_args(err: std::boxed::Box) -> Error { + warn!("invalid argument: {:?}", err); + Error::InvalidArgument + } + + fn deserialize(input: &[u8]) -> Result { + if input.is_empty() { + Err(Error::InvalidArgument)?; + } + match input[0] { + 0 => Ok(TokenProgram::Unallocated), + 1 => Ok(TokenProgram::Token( + bincode::deserialize(&input[1..]).map_err(Self::map_to_invalid_args)?, + )), + 2 => Ok(TokenProgram::Account( + bincode::deserialize(&input[1..]).map_err(Self::map_to_invalid_args)?, + )), + _ => Err(Error::InvalidArgument), + } + } + + fn serialize(self: &TokenProgram, output: &mut [u8]) -> Result<()> { + if output.is_empty() { + warn!("serialize fail: ouput.len is 0"); + Err(Error::InvalidArgument)?; + } + match self { + TokenProgram::Unallocated | TokenProgram::Invalid => Err(Error::InvalidArgument), + TokenProgram::Token(token_info) => { + output[0] = 1; + let writer = std::io::BufWriter::new(&mut output[1..]); + bincode::serialize_into(writer, &token_info).map_err(Self::map_to_invalid_args) + } + TokenProgram::Account(account_info) => { + output[0] = 2; + let writer = std::io::BufWriter::new(&mut output[1..]); + bincode::serialize_into(writer, &account_info).map_err(Self::map_to_invalid_args) + } + } + } + + pub fn check_id(program_id: &Pubkey) -> bool { + program_id.as_ref() == TOKEN_PROGRAM_ID + } + + pub fn id() -> Pubkey { + Pubkey::new(&TOKEN_PROGRAM_ID) + } + + pub fn process_command_newtoken( + tx: &Transaction, + pix: usize, + token_info: TokenInfo, + input_program_accounts: &[TokenProgram], + output_program_accounts: &mut Vec<(usize, TokenProgram)>, + ) -> Result<()> { + if input_program_accounts.len() != 2 { + error!("Expected 2 accounts"); + Err(Error::InvalidArgument)?; + } + + if let TokenProgram::Account(dest_account) = &input_program_accounts[1] { + if tx.key(pix, 0) != Some(&dest_account.token) { + info!("account 1 token mismatch"); + Err(Error::InvalidArgument)?; + } + + if dest_account.source.is_some() { + info!("account 1 is a delegate and cannot accept tokens"); + Err(Error::InvalidArgument)?; + } + + let mut output_dest_account = dest_account.clone(); + output_dest_account.amount = token_info.supply; + output_program_accounts.push((1, TokenProgram::Account(output_dest_account))); + } else { + info!("account 1 invalid"); + Err(Error::InvalidArgument)?; + } + + if input_program_accounts[0] != TokenProgram::Unallocated { + info!("account 0 not available"); + Err(Error::InvalidArgument)?; + } + output_program_accounts.push((0, TokenProgram::Token(token_info))); + Ok(()) + } + + pub fn process_command_newaccount( + tx: &Transaction, + pix: usize, + input_program_accounts: &[TokenProgram], + output_program_accounts: &mut Vec<(usize, TokenProgram)>, + ) -> Result<()> { + // key 0 - Destination new token account + // key 1 - Owner of the account + // key 2 - Token this account is associated with + // key 3 - Source account that this account is a delegate for (optional) + if input_program_accounts.len() < 3 { + error!("Expected 3 accounts"); + Err(Error::InvalidArgument)?; + } + if input_program_accounts[0] != TokenProgram::Unallocated { + info!("account 0 is already allocated"); + Err(Error::InvalidArgument)?; + } + + let mut token_account_info = TokenAccountInfo { + token: *tx.key(pix, 2).unwrap(), + owner: *tx.key(pix, 1).unwrap(), + amount: 0, + source: None, + }; + if input_program_accounts.len() >= 4 { + token_account_info.source = Some(*tx.key(pix, 3).unwrap()); + } + output_program_accounts.push((0, TokenProgram::Account(token_account_info))); + Ok(()) + } + + pub fn process_command_transfer( + tx: &Transaction, + pix: usize, + amount: u64, + input_program_accounts: &[TokenProgram], + output_program_accounts: &mut Vec<(usize, TokenProgram)>, + ) -> Result<()> { + if input_program_accounts.len() < 3 { + error!("Expected 3 accounts"); + Err(Error::InvalidArgument)?; + } + + if let (TokenProgram::Account(source_account), TokenProgram::Account(dest_account)) = + (&input_program_accounts[1], &input_program_accounts[2]) + { + if source_account.token != dest_account.token { + info!("account 1/2 token mismatch"); + Err(Error::InvalidArgument)?; + } + + if dest_account.source.is_some() { + info!("account 2 is a delegate and cannot accept tokens"); + Err(Error::InvalidArgument)?; + } + + if Some(&source_account.owner) != tx.key(pix, 0) { + info!("owner of account 1 not present"); + Err(Error::InvalidArgument)?; + } + + if source_account.amount < amount { + Err(Error::InsufficentFunds)?; + } + + let mut output_source_account = source_account.clone(); + output_source_account.amount -= amount; + output_program_accounts.push((1, TokenProgram::Account(output_source_account))); + + if source_account.source.is_some() { + if input_program_accounts.len() != 4 { + error!("Expected 4 accounts"); + Err(Error::InvalidArgument)?; + } + + let delegate_account = source_account; + if let TokenProgram::Account(source_account) = &input_program_accounts[3] { + if source_account.token != delegate_account.token { + info!("account 1/3 token mismatch"); + Err(Error::InvalidArgument)?; + } + if Some(&delegate_account.source.unwrap()) != tx.key(pix, 3) { + info!("Account 1 is not a delegate of account 3"); + Err(Error::InvalidArgument)?; + } + + if source_account.amount < amount { + Err(Error::InsufficentFunds)?; + } + + let mut output_source_account = source_account.clone(); + output_source_account.amount -= amount; + output_program_accounts.push((3, TokenProgram::Account(output_source_account))); + } else { + info!("account 3 is an invalid account"); + Err(Error::InvalidArgument)?; + } + } + + let mut output_dest_account = dest_account.clone(); + output_dest_account.amount += amount; + output_program_accounts.push((2, TokenProgram::Account(output_dest_account))); + } else { + info!("account 1 and/or 2 are invalid accounts"); + Err(Error::InvalidArgument)?; + } + Ok(()) + } + + pub fn process_command_approve( + tx: &Transaction, + pix: usize, + amount: u64, + input_program_accounts: &[TokenProgram], + output_program_accounts: &mut Vec<(usize, TokenProgram)>, + ) -> Result<()> { + if input_program_accounts.len() != 3 { + error!("Expected 3 accounts"); + Err(Error::InvalidArgument)?; + } + + if let (TokenProgram::Account(source_account), TokenProgram::Account(delegate_account)) = + (&input_program_accounts[1], &input_program_accounts[2]) + { + if source_account.token != delegate_account.token { + info!("account 1/2 token mismatch"); + Err(Error::InvalidArgument)?; + } + + if Some(&source_account.owner) != tx.key(pix, 0) { + info!("owner of account 1 not present"); + Err(Error::InvalidArgument)?; + } + + if source_account.source.is_some() { + info!("account 1 is a delegate"); + Err(Error::InvalidArgument)?; + } + + if Some(&delegate_account.source.unwrap()) != tx.key(pix, 1) { + info!("account 2 is not a delegate of account 1"); + Err(Error::InvalidArgument)?; + } + + let mut output_delegate_account = delegate_account.clone(); + output_delegate_account.amount = amount; + output_program_accounts.push((2, TokenProgram::Account(output_delegate_account))); + } else { + info!("account 1 and/or 2 are invalid accounts"); + Err(Error::InvalidArgument)?; + } + Ok(()) + } + + pub fn process_transaction( + tx: &Transaction, + pix: usize, + accounts: &mut [&mut Account], + ) -> Result<()> { + let command = bincode::deserialize::(&tx.userdata(pix)) + .map_err(Self::map_to_invalid_args)?; + info!("process_transaction: command={:?}", command); + + let input_program_accounts: Vec = accounts + .iter() + .map(|account| { + if Self::check_id(&account.program_id) { + Self::deserialize(&account.userdata) + .map_err(|err| { + info!("deserialize failed: {:?}", err); + TokenProgram::Invalid + }).unwrap() + } else { + TokenProgram::Invalid + } + }).collect(); + + let mut output_program_accounts: Vec<(_, _)> = vec![]; + + match command { + Command::NewToken(token_info) => Self::process_command_newtoken( + tx, + pix, + token_info, + &input_program_accounts, + &mut output_program_accounts, + )?, + Command::NewTokenAccount() => Self::process_command_newaccount( + tx, + pix, + &input_program_accounts, + &mut output_program_accounts, + )?, + + Command::Transfer(amount) => Self::process_command_transfer( + tx, + pix, + amount, + &input_program_accounts, + &mut output_program_accounts, + )?, + + Command::Approve(amount) => Self::process_command_approve( + tx, + pix, + amount, + &input_program_accounts, + &mut output_program_accounts, + )?, + } + + for (index, program_account) in &output_program_accounts { + info!( + "output_program_account: index={} userdata={:?}", + index, program_account + ); + Self::serialize(program_account, &mut accounts[*index].userdata)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + pub fn serde() { + assert_eq!(TokenProgram::deserialize(&[0]), Ok(TokenProgram::default())); + + let mut userdata = vec![0; 256]; + + let account = TokenProgram::Account(TokenAccountInfo { + token: Pubkey::new(&[1; 32]), + owner: Pubkey::new(&[2; 32]), + amount: 123, + source: None, + }); + assert!(account.serialize(&mut userdata).is_ok()); + assert_eq!(TokenProgram::deserialize(&userdata), Ok(account)); + + let account = TokenProgram::Token(TokenInfo { + supply: 12345, + decimals: 2, + name: "A test token".to_string(), + symbol: "TEST".to_string(), + }); + assert!(account.serialize(&mut userdata).is_ok()); + assert_eq!(TokenProgram::deserialize(&userdata), Ok(account)); + } + + #[test] + pub fn serde_expect_fail() { + let mut userdata = vec![0; 256]; + + // Certain TokenProgram's may not be serialized + let account = TokenProgram::default(); + assert_eq!(account, TokenProgram::Unallocated); + assert!(account.serialize(&mut userdata).is_err()); + assert!(account.serialize(&mut userdata).is_err()); + let account = TokenProgram::Invalid; + assert!(account.serialize(&mut userdata).is_err()); + + // Bad deserialize userdata + assert!(TokenProgram::deserialize(&[]).is_err()); + assert!(TokenProgram::deserialize(&[1]).is_err()); + assert!(TokenProgram::deserialize(&[1, 2]).is_err()); + assert!(TokenProgram::deserialize(&[2, 2]).is_err()); + assert!(TokenProgram::deserialize(&[3]).is_err()); + } + + // Note: business logic tests are located in the @solana/web3.js test suite +}