From bfa2535ea1e78e8f6c8d12a8d0f2e3ddeef15b79 Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Mon, 18 Nov 2019 18:09:42 -0700 Subject: [PATCH] Add non-fungible token program (#7007) * Add non-fungible token program * Remove issuer and id from state * Boot NftInstruction and NftState * Rename NFT to Ownable Maybe this should be "Owned" to avoid confusion with an Ownable trait? * Rename directory * Delete unreachable branch * Don't use copy_from_slice - need an error, not a panic. * Rename contract_pubkey to account_pubkey --- Cargo.lock | 13 ++ Cargo.toml | 1 + programs/ownable_api/Cargo.toml | 24 +++ programs/ownable_api/src/lib.rs | 12 ++ .../ownable_api/src/ownable_instruction.rs | 64 ++++++ programs/ownable_api/src/ownable_processor.rs | 185 ++++++++++++++++++ 6 files changed, 299 insertions(+) create mode 100644 programs/ownable_api/Cargo.toml create mode 100644 programs/ownable_api/src/lib.rs create mode 100644 programs/ownable_api/src/ownable_instruction.rs create mode 100644 programs/ownable_api/src/ownable_processor.rs diff --git a/Cargo.lock b/Cargo.lock index 84acb2ca99..c76f2cd236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3789,6 +3789,19 @@ dependencies = [ "solana-sdk 0.21.0", ] +[[package]] +name = "solana-ownable-api" +version = "0.21.0" +dependencies = [ + "bincode 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-runtime 0.21.0", + "solana-sdk 0.21.0", +] + [[package]] name = "solana-perf" version = "0.21.0" diff --git a/Cargo.toml b/Cargo.toml index e5388f7aed..320b89d959 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ members = [ "programs/exchange_program", "programs/failure_program", "programs/noop_program", + "programs/ownable_api", "programs/stake_api", "programs/stake_program", "programs/stake_tests", diff --git a/programs/ownable_api/Cargo.toml b/programs/ownable_api/Cargo.toml new file mode 100644 index 0000000000..e08f8030d0 --- /dev/null +++ b/programs/ownable_api/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "solana-ownable-api" +version = "0.21.0" +description = "ownable program API" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +bincode = "1.2.0" +serde = "1.0.102" +serde_derive = "1.0.102" +solana-sdk = { path = "../../sdk", version = "0.21.0" } +num-derive = "0.3" +num-traits = "0.2" + +[dev-dependencies] +solana-runtime = { path = "../../runtime", version = "0.21.0" } + +[lib] +crate-type = ["lib"] +name = "solana_ownable_api" diff --git a/programs/ownable_api/src/lib.rs b/programs/ownable_api/src/lib.rs new file mode 100644 index 0000000000..667e26b941 --- /dev/null +++ b/programs/ownable_api/src/lib.rs @@ -0,0 +1,12 @@ +pub mod ownable_instruction; +pub mod ownable_processor; + +const OWNABLE_PROGRAM_ID: [u8; 32] = [ + 12, 6, 169, 236, 232, 53, 216, 159, 221, 186, 8, 8, 33, 45, 166, 249, 243, 55, 177, 184, 195, + 132, 141, 34, 63, 108, 219, 80, 0, 0, 0, 0, +]; + +solana_sdk::solana_name_id!( + OWNABLE_PROGRAM_ID, + "ownab1e111111111111111111111111111111111111" +); diff --git a/programs/ownable_api/src/ownable_instruction.rs b/programs/ownable_api/src/ownable_instruction.rs new file mode 100644 index 0000000000..3d49302c1b --- /dev/null +++ b/programs/ownable_api/src/ownable_instruction.rs @@ -0,0 +1,64 @@ +use num_derive::{FromPrimitive, ToPrimitive}; +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + instruction_processor_utils::DecodeError, + pubkey::Pubkey, + system_instruction, +}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, FromPrimitive, ToPrimitive)] +pub enum OwnableError { + IncorrectOwner, +} + +impl DecodeError for OwnableError { + fn type_of() -> &'static str { + "OwnableError" + } +} + +impl std::fmt::Display for OwnableError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + OwnableError::IncorrectOwner => "incorrect owner", + } + ) + } +} +impl std::error::Error for OwnableError {} + +fn initialize_account(account_pubkey: &Pubkey, owner_pubkey: &Pubkey) -> Instruction { + let keys = vec![AccountMeta::new(*account_pubkey, false)]; + Instruction::new(crate::id(), &owner_pubkey, keys) +} + +pub fn create_account( + payer_pubkey: &Pubkey, + account_pubkey: &Pubkey, + owner_pubkey: &Pubkey, + lamports: u64, +) -> Vec { + let space = std::mem::size_of::() as u64; + vec![ + system_instruction::create_account( + &payer_pubkey, + account_pubkey, + lamports, + space, + &crate::id(), + ), + initialize_account(account_pubkey, owner_pubkey), + ] +} + +pub fn set_owner(account_pubkey: &Pubkey, old_pubkey: &Pubkey, new_pubkey: &Pubkey) -> Instruction { + let keys = vec![ + AccountMeta::new(*account_pubkey, false), + AccountMeta::new(*old_pubkey, true), + ]; + Instruction::new(crate::id(), &new_pubkey, keys) +} diff --git a/programs/ownable_api/src/ownable_processor.rs b/programs/ownable_api/src/ownable_processor.rs new file mode 100644 index 0000000000..9cf9bafdef --- /dev/null +++ b/programs/ownable_api/src/ownable_processor.rs @@ -0,0 +1,185 @@ +//! Ownable program + +use crate::ownable_instruction::OwnableError; +use bincode::serialize_into; +use solana_sdk::{ + account::KeyedAccount, + instruction::InstructionError, + instruction_processor_utils::{limited_deserialize, next_keyed_account}, + pubkey::Pubkey, +}; + +fn set_owner( + account_owner_pubkey: &mut Pubkey, + new_owner_pubkey: Pubkey, + owner_keyed_account: &KeyedAccount, +) -> Result<(), InstructionError> { + match owner_keyed_account.signer_key() { + None => return Err(InstructionError::MissingRequiredSignature), + Some(signer_key) => { + if account_owner_pubkey != signer_key { + return Err(OwnableError::IncorrectOwner.into()); + } + *account_owner_pubkey = new_owner_pubkey; + } + } + Ok(()) +} + +pub fn process_instruction( + _program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], +) -> Result<(), InstructionError> { + let new_owner_pubkey: Pubkey = limited_deserialize(data)?; + let keyed_accounts_iter = &mut keyed_accounts.iter_mut(); + let account_keyed_account = &mut next_keyed_account(keyed_accounts_iter)?; + let mut account_owner_pubkey: Pubkey = + limited_deserialize(&account_keyed_account.account.data)?; + + if account_owner_pubkey == Pubkey::default() { + account_owner_pubkey = new_owner_pubkey; + } else { + let owner_keyed_account = &mut next_keyed_account(keyed_accounts_iter)?; + set_owner( + &mut account_owner_pubkey, + new_owner_pubkey, + &owner_keyed_account, + )?; + } + + serialize_into( + &mut account_keyed_account.account.data[..], + &account_owner_pubkey, + ) + .map_err(|_| InstructionError::AccountDataTooSmall) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ownable_instruction; + use solana_runtime::{bank::Bank, bank_client::BankClient}; + use solana_sdk::{ + account::Account, + client::SyncClient, + genesis_config::create_genesis_config, + message::Message, + signature::{Keypair, KeypairUtil, Signature}, + system_program, + transport::Result, + }; + + fn create_bank(lamports: u64) -> (Bank, Keypair) { + let (genesis_config, mint_keypair) = create_genesis_config(lamports); + let mut bank = Bank::new(&genesis_config); + bank.add_instruction_processor(crate::id(), process_instruction); + (bank, mint_keypair) + } + + fn create_bank_client(lamports: u64) -> (BankClient, Keypair) { + let (bank, mint_keypair) = create_bank(lamports); + (BankClient::new(bank), mint_keypair) + } + + fn create_ownable_account( + bank_client: &BankClient, + payer_keypair: &Keypair, + account_keypair: &Keypair, + owner_pubkey: &Pubkey, + lamports: u64, + ) -> Result { + let instructions = ownable_instruction::create_account( + &payer_keypair.pubkey(), + &account_keypair.pubkey(), + owner_pubkey, + lamports, + ); + let message = Message::new(instructions); + bank_client.send_message(&[&payer_keypair, &account_keypair], message) + } + + fn send_set_owner( + bank_client: &BankClient, + payer_keypair: &Keypair, + account_pubkey: &Pubkey, + old_owner_keypair: &Keypair, + new_owner_pubkey: &Pubkey, + ) -> Result { + let instruction = ownable_instruction::set_owner( + account_pubkey, + &old_owner_keypair.pubkey(), + new_owner_pubkey, + ); + let message = Message::new_with_payer(vec![instruction], Some(&payer_keypair.pubkey())); + bank_client.send_message(&[&payer_keypair, &old_owner_keypair], message) + } + + #[test] + fn test_ownable_set_owner() { + let (bank_client, payer_keypair) = create_bank_client(2); + let account_keypair = Keypair::new(); + let account_pubkey = account_keypair.pubkey(); + let owner_keypair = Keypair::new(); + let owner_pubkey = owner_keypair.pubkey(); + + create_ownable_account( + &bank_client, + &payer_keypair, + &account_keypair, + &owner_pubkey, + 1, + ) + .unwrap(); + + let new_owner_keypair = Keypair::new(); + let new_owner_pubkey = new_owner_keypair.pubkey(); + send_set_owner( + &bank_client, + &payer_keypair, + &account_pubkey, + &owner_keypair, + &new_owner_pubkey, + ) + .unwrap(); + + let account_data = bank_client + .get_account_data(&account_pubkey) + .unwrap() + .unwrap(); + let account_owner_pubkey: Pubkey = limited_deserialize(&account_data).unwrap(); + assert_eq!(account_owner_pubkey, new_owner_pubkey); + } + + #[test] + fn test_ownable_missing_owner_signature() { + let mut account_owner_pubkey = Pubkey::new_rand(); + let owner_pubkey = account_owner_pubkey; + let new_owner_pubkey = Pubkey::new_rand(); + let mut account = Account::new(1, 0, &system_program::id()); + let owner_keyed_account = KeyedAccount::new(&owner_pubkey, false, &mut account); // <-- Attack! Setting owner without the original owner's signature. + let err = set_owner( + &mut account_owner_pubkey, + new_owner_pubkey, + &owner_keyed_account, + ) + .unwrap_err(); + assert_eq!(err, InstructionError::MissingRequiredSignature); + } + + #[test] + fn test_ownable_incorrect_owner() { + let mut account_owner_pubkey = Pubkey::new_rand(); + let new_owner_pubkey = Pubkey::new_rand(); + let mut account = Account::new(1, 0, &system_program::id()); + let mallory_pubkey = Pubkey::new_rand(); // <-- Attack! Signing with wrong pubkey + let owner_keyed_account = KeyedAccount::new(&mallory_pubkey, true, &mut account); + let err = set_owner( + &mut account_owner_pubkey, + new_owner_pubkey, + &owner_keyed_account, + ) + .unwrap_err(); + assert_eq!(err, OwnableError::IncorrectOwner.into()); + } +}