From 8d0a2e10008963c4d1ffe9ff796977c0060219e4 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Tue, 15 Nov 2022 14:10:59 +0100 Subject: [PATCH] token-2022: Add PermanentDelegate extension (#3725) * token-2022: Add PermanentDelegate extension * Address feedback * Refactor getting permanent delegate * Rename function * More cleanup * Fix ATA --- .../program-test/tests/extended_mint.rs | 4 +- token-swap/program/src/processor.rs | 2 +- token/cli/src/main.rs | 8 +- token/client/src/token.rs | 10 +- .../tests/confidential_transfer.rs | 2 +- token/program-2022-test/tests/cpi_guard.rs | 2 +- .../tests/default_account_state.rs | 6 +- .../tests/initialize_account.rs | 2 +- .../tests/initialize_mint.rs | 5 +- .../tests/interest_bearing_mint.rs | 2 +- .../program-2022-test/tests/memo_transfer.rs | 2 +- .../tests/mint_close_authority.rs | 4 +- .../tests/permanent_delegate.rs | 276 ++++++++++++++++++ token/program-2022-test/tests/transfer_fee.rs | 7 +- .../confidential_transfer/processor.rs | 2 +- .../src/extension/cpi_guard/mod.rs | 2 +- .../src/extension/memo_transfer/mod.rs | 2 +- token/program-2022/src/extension/mod.rs | 86 +++--- .../src/extension/permanent_delegate.rs | 30 ++ .../program-2022/src/extension/reallocate.rs | 5 +- .../src/extension/transfer_fee/processor.rs | 2 +- token/program-2022/src/instruction.rs | 59 ++++ token/program-2022/src/processor.rs | 85 +++++- 23 files changed, 529 insertions(+), 76 deletions(-) create mode 100644 token/program-2022-test/tests/permanent_delegate.rs create mode 100644 token/program-2022/src/extension/permanent_delegate.rs diff --git a/associated-token-account/program-test/tests/extended_mint.rs b/associated-token-account/program-test/tests/extended_mint.rs index 5c81de80..e6be2281 100644 --- a/associated-token-account/program-test/tests/extended_mint.rs +++ b/associated-token-account/program-test/tests/extended_mint.rs @@ -17,7 +17,9 @@ use { }, spl_token_2022::{ error::TokenError, - extension::{transfer_fee, ExtensionType, StateWithExtensionsOwned}, + extension::{ + transfer_fee, BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned, + }, state::{Account, Mint}, }, }; diff --git a/token-swap/program/src/processor.rs b/token-swap/program/src/processor.rs index 7abb0c0f..9f2ae5b8 100644 --- a/token-swap/program/src/processor.rs +++ b/token-swap/program/src/processor.rs @@ -33,7 +33,7 @@ use spl_token_2022::{ error::TokenError, extension::{ mint_close_authority::MintCloseAuthority, transfer_fee::TransferFeeConfig, - StateWithExtensions, + BaseStateWithExtensions, StateWithExtensions, }, state::{Account, Mint}, }; diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index 3db4e790..50aead9f 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -43,7 +43,7 @@ use spl_token_2022::{ memo_transfer::MemoTransfer, mint_close_authority::MintCloseAuthority, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, - ExtensionType, StateWithExtensionsOwned, + BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned, }, instruction::*, state::{Account, AccountState, Mint}, @@ -744,6 +744,7 @@ async fn command_authorize( AuthorityType::TransferFeeConfig => "transfer fee authority", AuthorityType::WithheldWithdraw => "withdraw withheld authority", AuthorityType::InterestRate => "interest rate authority", + AuthorityType::PermanentDelegate => "permanent delegate", }; let (mint_pubkey, previous_authority) = if !config.sign_only { @@ -781,6 +782,7 @@ async fn command_authorize( Err(format!("Mint `{}` is not interest-bearing", account)) } } + AuthorityType::PermanentDelegate => unimplemented!(), }?; Ok((account, previous_authority)) @@ -813,7 +815,8 @@ async fn command_authorize( | AuthorityType::CloseMint | AuthorityType::TransferFeeConfig | AuthorityType::WithheldWithdraw - | AuthorityType::InterestRate => Err(format!( + | AuthorityType::InterestRate + | AuthorityType::PermanentDelegate => Err(format!( "Authority type `{}` not supported for SPL Token accounts", auth_str )), @@ -3662,6 +3665,7 @@ async fn process_command<'a>( "transfer-fee-config" => AuthorityType::TransferFeeConfig, "withheld-withdraw" => AuthorityType::WithheldWithdraw, "interest-rate" => AuthorityType::InterestRate, + "permanent-delegate" => AuthorityType::PermanentDelegate, _ => unreachable!(), }; diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 2d874831..ebe92857 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -21,7 +21,8 @@ use { spl_token_2022::{ extension::{ confidential_transfer, cpi_guard, default_account_state, interest_bearing_mint, - memo_transfer, transfer_fee, ExtensionType, StateWithExtensionsOwned, + memo_transfer, transfer_fee, BaseStateWithExtensions, ExtensionType, + StateWithExtensionsOwned, }, instruction, solana_zk_token_sdk::{ @@ -122,6 +123,9 @@ pub enum ExtensionInitializationParams { rate: i16, }, NonTransferable, + PermanentDelegate { + delegate: Pubkey, + }, } impl ExtensionInitializationParams { /// Get the extension type associated with the init params @@ -133,6 +137,7 @@ impl ExtensionInitializationParams { Self::TransferFeeConfig { .. } => ExtensionType::TransferFeeConfig, Self::InterestBearingConfig { .. } => ExtensionType::InterestBearingConfig, Self::NonTransferable => ExtensionType::NonTransferable, + Self::PermanentDelegate { .. } => ExtensionType::PermanentDelegate, } } /// Generate an appropriate initialization instruction for the given mint @@ -188,6 +193,9 @@ impl ExtensionInitializationParams { Self::NonTransferable => { instruction::initialize_non_transferable_mint(token_program_id, mint) } + Self::PermanentDelegate { delegate } => { + instruction::initialize_permanent_delegate(token_program_id, mint, &delegate) + } } } } diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs index c19ff11e..1de985bb 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -15,7 +15,7 @@ use { confidential_transfer::{ ConfidentialTransferAccount, ConfidentialTransferMint, EncryptedWithheldAmount, }, - ExtensionType, + BaseStateWithExtensions, ExtensionType, }, solana_zk_token_sdk::{ encryption::{auth_encryption::*, elgamal::*}, diff --git a/token/program-2022-test/tests/cpi_guard.rs b/token/program-2022-test/tests/cpi_guard.rs index 162734f6..d94ceb3c 100644 --- a/token/program-2022-test/tests/cpi_guard.rs +++ b/token/program-2022-test/tests/cpi_guard.rs @@ -17,7 +17,7 @@ use { error::TokenError, extension::{ cpi_guard::{self, CpiGuard}, - ExtensionType, + BaseStateWithExtensions, ExtensionType, }, instruction::{self, AuthorityType}, processor::Processor as SplToken2022Processor, diff --git a/token/program-2022-test/tests/default_account_state.rs b/token/program-2022-test/tests/default_account_state.rs index 76c445a0..ebee881d 100644 --- a/token/program-2022-test/tests/default_account_state.rs +++ b/token/program-2022-test/tests/default_account_state.rs @@ -9,8 +9,10 @@ use { signer::keypair::Keypair, transaction::TransactionError, transport::TransportError, }, spl_token_2022::{ - error::TokenError, extension::default_account_state::DefaultAccountState, - instruction::AuthorityType, state::AccountState, + error::TokenError, + extension::{default_account_state::DefaultAccountState, BaseStateWithExtensions}, + instruction::AuthorityType, + state::AccountState, }, spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, std::convert::TryFrom, diff --git a/token/program-2022-test/tests/initialize_account.rs b/token/program-2022-test/tests/initialize_account.rs index 10e2f34a..1cf2e82a 100644 --- a/token/program-2022-test/tests/initialize_account.rs +++ b/token/program-2022-test/tests/initialize_account.rs @@ -17,7 +17,7 @@ use { error::TokenError, extension::{ transfer_fee::{self, TransferFeeAmount}, - ExtensionType, StateWithExtensions, + BaseStateWithExtensions, ExtensionType, StateWithExtensions, }, instruction, state::{Account, Mint}, diff --git a/token/program-2022-test/tests/initialize_mint.rs b/token/program-2022-test/tests/initialize_mint.rs index 86af50f4..ac6ec118 100644 --- a/token/program-2022-test/tests/initialize_mint.rs +++ b/token/program-2022-test/tests/initialize_mint.rs @@ -16,7 +16,10 @@ use { }, spl_token_2022::{ error::TokenError, - extension::{mint_close_authority::MintCloseAuthority, transfer_fee, ExtensionType}, + extension::{ + mint_close_authority::MintCloseAuthority, transfer_fee, BaseStateWithExtensions, + ExtensionType, + }, instruction, native_mint, state::Mint, }, diff --git a/token/program-2022-test/tests/interest_bearing_mint.rs b/token/program-2022-test/tests/interest_bearing_mint.rs index 3de7233c..5a2299bb 100644 --- a/token/program-2022-test/tests/interest_bearing_mint.rs +++ b/token/program-2022-test/tests/interest_bearing_mint.rs @@ -23,7 +23,7 @@ use { }, spl_token_2022::{ error::TokenError, - extension::interest_bearing_mint::InterestBearingConfig, + extension::{interest_bearing_mint::InterestBearingConfig, BaseStateWithExtensions}, instruction::{amount_to_ui_amount, ui_amount_to_amount, AuthorityType}, processor::Processor, }, diff --git a/token/program-2022-test/tests/memo_transfer.rs b/token/program-2022-test/tests/memo_transfer.rs index 9bdef284..d76ed691 100644 --- a/token/program-2022-test/tests/memo_transfer.rs +++ b/token/program-2022-test/tests/memo_transfer.rs @@ -17,7 +17,7 @@ use { }, spl_token_2022::{ error::TokenError, - extension::{memo_transfer::MemoTransfer, ExtensionType}, + extension::{memo_transfer::MemoTransfer, BaseStateWithExtensions, ExtensionType}, }, spl_token_client::token::TokenError as TokenClientError, std::sync::Arc, diff --git a/token/program-2022-test/tests/mint_close_authority.rs b/token/program-2022-test/tests/mint_close_authority.rs index 6538635c..01e56533 100644 --- a/token/program-2022-test/tests/mint_close_authority.rs +++ b/token/program-2022-test/tests/mint_close_authority.rs @@ -9,7 +9,9 @@ use { signer::keypair::Keypair, transaction::TransactionError, transport::TransportError, }, spl_token_2022::{ - error::TokenError, extension::mint_close_authority::MintCloseAuthority, instruction, + error::TokenError, + extension::{mint_close_authority::MintCloseAuthority, BaseStateWithExtensions}, + instruction, }, spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, std::convert::TryInto, diff --git a/token/program-2022-test/tests/permanent_delegate.rs b/token/program-2022-test/tests/permanent_delegate.rs new file mode 100644 index 00000000..0543fadd --- /dev/null +++ b/token/program-2022-test/tests/permanent_delegate.rs @@ -0,0 +1,276 @@ +#![cfg(feature = "test-sbf")] + +mod program_test; +use { + program_test::{TestContext, TokenContext}, + solana_program_test::tokio, + solana_sdk::{ + instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, + transaction::TransactionError, transport::TransportError, + }, + spl_token_2022::{ + error::TokenError, + extension::{permanent_delegate::PermanentDelegate, BaseStateWithExtensions}, + instruction, + }, + spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, + std::convert::TryInto, +}; + +async fn setup_accounts(token_context: &TokenContext, amount: u64) -> (Pubkey, Pubkey) { + let alice_account = Keypair::new(); + token_context + .token + .create_auxiliary_token_account(&alice_account, &token_context.alice.pubkey()) + .await + .unwrap(); + let alice_account = alice_account.pubkey(); + let bob_account = Keypair::new(); + token_context + .token + .create_auxiliary_token_account(&bob_account, &token_context.bob.pubkey()) + .await + .unwrap(); + let bob_account = bob_account.pubkey(); + + // mint tokens + token_context + .token + .mint_to( + &alice_account, + &token_context.mint_authority.pubkey(), + amount, + &[&token_context.mint_authority], + ) + .await + .unwrap(); + (alice_account, bob_account) +} + +#[tokio::test] +async fn success_init() { + let delegate = Pubkey::new_unique(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PermanentDelegate { + delegate, + }]) + .await + .unwrap(); + let TokenContext { token, .. } = context.token_context.unwrap(); + + let state = token.get_mint_info().await.unwrap(); + assert!(state.base.is_initialized); + let extension = state.get_extension::().unwrap(); + assert_eq!(extension.delegate, Some(delegate).try_into().unwrap(),); +} + +#[tokio::test] +async fn set_authority() { + let delegate = Keypair::new(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PermanentDelegate { + delegate: delegate.pubkey(), + }]) + .await + .unwrap(); + let token_context = context.token_context.unwrap(); + let new_delegate = Keypair::new(); + + // fail, wrong signature + let wrong = Keypair::new(); + let err = token_context + .token + .set_authority( + token_context.token.get_address(), + &wrong.pubkey(), + Some(&new_delegate.pubkey()), + instruction::AuthorityType::PermanentDelegate, + &[&wrong], + ) + .await + .unwrap_err(); + assert_eq!( + err, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::OwnerMismatch as u32) + ) + ))) + ); + + // success + token_context + .token + .set_authority( + token_context.token.get_address(), + &delegate.pubkey(), + Some(&new_delegate.pubkey()), + instruction::AuthorityType::PermanentDelegate, + &[&delegate], + ) + .await + .unwrap(); + let state = token_context.token.get_mint_info().await.unwrap(); + let extension = state.get_extension::().unwrap(); + assert_eq!( + extension.delegate, + Some(new_delegate.pubkey()).try_into().unwrap(), + ); + + // set to none + token_context + .token + .set_authority( + token_context.token.get_address(), + &new_delegate.pubkey(), + None, + instruction::AuthorityType::PermanentDelegate, + &[&new_delegate], + ) + .await + .unwrap(); + let state = token_context.token.get_mint_info().await.unwrap(); + let extension = state.get_extension::().unwrap(); + assert_eq!(extension.delegate, None.try_into().unwrap(),); + + // fail set again + let err = token_context + .token + .set_authority( + token_context.token.get_address(), + &new_delegate.pubkey(), + Some(&delegate.pubkey()), + instruction::AuthorityType::PermanentDelegate, + &[&new_delegate], + ) + .await + .unwrap_err(); + assert_eq!( + err, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) + ) + ))) + ); + + // setup accounts + let amount = 10; + let (alice_account, bob_account) = setup_accounts(&token_context, amount).await; + + // fail transfer + let error = token_context + .token + .transfer( + &alice_account, + &bob_account, + &new_delegate.pubkey(), + amount, + &[&new_delegate], + ) + .await + .unwrap_err(); + + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::OwnerMismatch as u32) + ) + ))) + ); +} + +#[tokio::test] +async fn success_transfer() { + let delegate = Keypair::new(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PermanentDelegate { + delegate: delegate.pubkey(), + }]) + .await + .unwrap(); + let token_context = context.token_context.unwrap(); + let amount = 10; + let (alice_account, bob_account) = setup_accounts(&token_context, amount).await; + + token_context + .token + .transfer( + &alice_account, + &bob_account, + &delegate.pubkey(), + amount, + &[&delegate], + ) + .await + .unwrap(); + + let destination = token_context + .token + .get_account_info(&bob_account) + .await + .unwrap(); + assert_eq!(destination.base.amount, amount); +} + +#[tokio::test] +async fn success_burn() { + let delegate = Keypair::new(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PermanentDelegate { + delegate: delegate.pubkey(), + }]) + .await + .unwrap(); + let token_context = context.token_context.unwrap(); + let amount = 10; + let (alice_account, _) = setup_accounts(&token_context, amount).await; + + token_context + .token + .burn(&alice_account, &delegate.pubkey(), amount, &[&delegate]) + .await + .unwrap(); + + let destination = token_context + .token + .get_account_info(&alice_account) + .await + .unwrap(); + assert_eq!(destination.base.amount, 0); +} + +#[tokio::test] +async fn fail_without_extension() { + let delegate = Pubkey::new_unique(); + let mut context = TestContext::new().await; + context.init_token_with_mint(vec![]).await.unwrap(); + let token_context = context.token_context.unwrap(); + + // fail set + let err = token_context + .token + .set_authority( + token_context.token.get_address(), + &token_context.mint_authority.pubkey(), + Some(&delegate), + instruction::AuthorityType::PermanentDelegate, + &[&token_context.mint_authority], + ) + .await + .unwrap_err(); + assert_eq!( + err, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidAccountData) + ))) + ); +} diff --git a/token/program-2022-test/tests/transfer_fee.rs b/token/program-2022-test/tests/transfer_fee.rs index 9b552e3c..dff1a4af 100644 --- a/token/program-2022-test/tests/transfer_fee.rs +++ b/token/program-2022-test/tests/transfer_fee.rs @@ -10,8 +10,11 @@ use { }, spl_token_2022::{ error::TokenError, - extension::transfer_fee::{ - TransferFee, TransferFeeAmount, TransferFeeConfig, MAX_FEE_BASIS_POINTS, + extension::{ + transfer_fee::{ + TransferFee, TransferFeeAmount, TransferFeeConfig, MAX_FEE_BASIS_POINTS, + }, + BaseStateWithExtensions, }, instruction, }, diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index b1e1d3d1..ca94eccd 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -4,7 +4,7 @@ use { error::TokenError, extension::{ confidential_transfer::{instruction::*, *}, - StateWithExtensions, StateWithExtensionsMut, + BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut, }, instruction::{decode_instruction_data, decode_instruction_type}, processor::Processor, diff --git a/token/program-2022/src/extension/cpi_guard/mod.rs b/token/program-2022/src/extension/cpi_guard/mod.rs index 46c6981f..f805fbd7 100644 --- a/token/program-2022/src/extension/cpi_guard/mod.rs +++ b/token/program-2022/src/extension/cpi_guard/mod.rs @@ -1,6 +1,6 @@ use { crate::{ - extension::{Extension, ExtensionType, StateWithExtensionsMut}, + extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut}, pod::PodBool, state::Account, }, diff --git a/token/program-2022/src/extension/memo_transfer/mod.rs b/token/program-2022/src/extension/memo_transfer/mod.rs index 4f586132..11be19f6 100644 --- a/token/program-2022/src/extension/memo_transfer/mod.rs +++ b/token/program-2022/src/extension/memo_transfer/mod.rs @@ -1,7 +1,7 @@ use { crate::{ error::TokenError, - extension::{Extension, ExtensionType, StateWithExtensionsMut}, + extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut}, pod::PodBool, state::Account, }, diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index f78c635d..30eba746 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -12,6 +12,7 @@ use { memo_transfer::MemoTransfer, mint_close_authority::MintCloseAuthority, non_transferable::NonTransferable, + permanent_delegate::PermanentDelegate, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, }, pod::*, @@ -48,6 +49,8 @@ pub mod memo_transfer; pub mod mint_close_authority; /// Non Transferable extension pub mod non_transferable; +/// Permanent Delegate extension +pub mod permanent_delegate; /// Utility to reallocate token accounts pub mod reallocate; /// Transfer Fee extension @@ -263,6 +266,27 @@ fn get_extension(tlv_data: &[u8]) -> Result<&V, Prog pod_from_bytes::(&tlv_data[value_start..value_end]) } +/// Trait for base state with extension +pub trait BaseStateWithExtensions { + /// Get the buffer containing all extension data + fn get_tlv_data(&self) -> &[u8]; + + /// Unpack a portion of the TLV data as the desired type + fn get_extension(&self) -> Result<&V, ProgramError> { + get_extension::(self.get_tlv_data()) + } + + /// Iterates through the TLV entries, returning only the types + fn get_extension_types(&self) -> Result, ProgramError> { + get_extension_types(self.get_tlv_data()) + } + + /// Get just the first extension type, useful to track mixed initializations + fn get_first_extension_type(&self) -> Result, ProgramError> { + get_first_extension_type(self.get_tlv_data()) + } +} + /// Encapsulates owned immutable base state data (mint or account) with possible extensions #[derive(Debug, PartialEq)] pub struct StateWithExtensionsOwned { @@ -293,15 +317,11 @@ impl StateWithExtensionsOwned { }) } } +} - /// Unpack a portion of the TLV data as the desired type - pub fn get_extension(&self) -> Result<&V, ProgramError> { - get_extension::(&self.tlv_data) - } - - /// Iterates through the TLV entries, returning only the types - pub fn get_extension_types(&self) -> Result, ProgramError> { - get_extension_types(&self.tlv_data) +impl BaseStateWithExtensions for StateWithExtensionsOwned { + fn get_tlv_data(&self) -> &[u8] { + &self.tlv_data } } @@ -337,15 +357,10 @@ impl<'data, S: BaseState> StateWithExtensions<'data, S> { }) } } - - /// Unpack a portion of the TLV data as the desired type - pub fn get_extension(&self) -> Result<&V, ProgramError> { - get_extension::(self.tlv_data) - } - - /// Iterates through the TLV entries, returning only the types - pub fn get_extension_types(&self) -> Result, ProgramError> { - get_extension_types(self.tlv_data) +} +impl<'a, S: BaseState> BaseStateWithExtensions for StateWithExtensions<'a, S> { + fn get_tlv_data(&self) -> &[u8] { + self.tlv_data } } @@ -451,25 +466,6 @@ impl<'data, S: BaseState> StateWithExtensionsMut<'data, S> { pod_from_bytes_mut::(&mut self.tlv_data[value_start..value_end]) } - /// Unpack a portion of the TLV data as the desired type - pub fn get_extension(&self) -> Result<&V, ProgramError> { - if V::TYPE.get_account_type() != S::ACCOUNT_TYPE { - return Err(ProgramError::InvalidAccountData); - } - let TlvIndices { - type_start, - length_start, - value_start, - } = get_extension_indices::(self.tlv_data, false)?; - - if self.tlv_data[type_start..].len() < V::TYPE.get_tlv_len() { - return Err(ProgramError::InvalidAccountData); - } - let length = pod_from_bytes::(&self.tlv_data[length_start..value_start])?; - let value_end = value_start.saturating_add(usize::from(*length)); - pod_from_bytes::(&self.tlv_data[value_start..value_end]) - } - /// Packs base state data into the base data portion pub fn pack_base(&mut self) { S::pack_into_slice(&self.base, self.base_data); @@ -562,14 +558,10 @@ impl<'data, S: BaseState> StateWithExtensionsMut<'data, S> { } Ok(()) } - - /// Iterates through the TLV entries, returning only the types - pub fn get_extension_types(&self) -> Result, ProgramError> { - get_extension_types(self.tlv_data) - } - - fn get_first_extension_type(&self) -> Result, ProgramError> { - get_first_extension_type(self.tlv_data) +} +impl<'a, S: BaseState> BaseStateWithExtensions for StateWithExtensionsMut<'a, S> { + fn get_tlv_data(&self) -> &[u8] { + self.tlv_data } } @@ -647,6 +639,8 @@ pub enum ExtensionType { InterestBearingConfig, /// Locks privileged token operations from happening via CPI CpiGuard, + /// Includes an optional permanent delegate + PermanentDelegate, /// Padding extension used to make an account exactly Multisig::LEN, used for testing #[cfg(test)] AccountPaddingTest = u16::MAX - 1, @@ -688,6 +682,7 @@ impl ExtensionType { ExtensionType::NonTransferable => pod_get_packed_len::(), ExtensionType::InterestBearingConfig => pod_get_packed_len::(), ExtensionType::CpiGuard => pod_get_packed_len::(), + ExtensionType::PermanentDelegate => pod_get_packed_len::(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -744,7 +739,8 @@ impl ExtensionType { | ExtensionType::ConfidentialTransferMint | ExtensionType::DefaultAccountState | ExtensionType::NonTransferable - | ExtensionType::InterestBearingConfig => AccountType::Mint, + | ExtensionType::InterestBearingConfig + | ExtensionType::PermanentDelegate => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount diff --git a/token/program-2022/src/extension/permanent_delegate.rs b/token/program-2022/src/extension/permanent_delegate.rs new file mode 100644 index 00000000..bfd7bcc1 --- /dev/null +++ b/token/program-2022/src/extension/permanent_delegate.rs @@ -0,0 +1,30 @@ +use { + crate::{ + extension::{BaseState, BaseStateWithExtensions, Extension, ExtensionType}, + pod::*, + }, + bytemuck::{Pod, Zeroable}, + solana_program::pubkey::Pubkey, +}; + +/// Permanent delegate extension data for mints. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub struct PermanentDelegate { + /// Optional permanent delegate for transferring or burning tokens + pub delegate: OptionalNonZeroPubkey, +} +impl Extension for PermanentDelegate { + const TYPE: ExtensionType = ExtensionType::PermanentDelegate; +} + +/// Attempts to get the permanent delegate from the TLV data, returning None +/// if the extension is not found +pub fn get_permanent_delegate>( + state: &BSE, +) -> Option { + state + .get_extension::() + .ok() + .and_then(|e| Option::::from(e.delegate)) +} diff --git a/token/program-2022/src/extension/reallocate.rs b/token/program-2022/src/extension/reallocate.rs index 7c2bf555..587491e3 100644 --- a/token/program-2022/src/extension/reallocate.rs +++ b/token/program-2022/src/extension/reallocate.rs @@ -1,7 +1,10 @@ use { crate::{ error::TokenError, - extension::{set_account_type, AccountType, ExtensionType, StateWithExtensions}, + extension::{ + set_account_type, AccountType, BaseStateWithExtensions, ExtensionType, + StateWithExtensions, + }, processor::Processor, state::Account, }, diff --git a/token/program-2022/src/extension/transfer_fee/processor.rs b/token/program-2022/src/extension/transfer_fee/processor.rs index 917f90c0..d31dc817 100644 --- a/token/program-2022/src/extension/transfer_fee/processor.rs +++ b/token/program-2022/src/extension/transfer_fee/processor.rs @@ -7,7 +7,7 @@ use { instruction::TransferFeeInstruction, TransferFee, TransferFeeAmount, TransferFeeConfig, MAX_FEE_BASIS_POINTS, }, - StateWithExtensions, StateWithExtensionsMut, + BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut, }, processor::Processor, state::{Account, Mint}, diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index 631789e4..df3a114e 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -610,6 +610,26 @@ pub enum TokenInstruction<'a> { /// See `extension::cpi_guard::instruction::CpiGuardInstruction` for /// further details about the extended instructions that share this instruction prefix CpiGuardExtension, + /// Initialize the permanent delegate on a new mint. + /// + /// Fails if the mint has already been initialized, so must be called before + /// `InitializeMint`. + /// + /// The mint must have exactly enough space allocated for the base mint (82 + /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, + /// then space required for this extension, plus any others. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint to initialize. + /// + /// Data expected by this instruction: + /// Pubkey for the permanent delegate + /// + InitializePermanentDelegate { + /// Authority that may sign for `Transfer`s and `Burn`s on any account + delegate: Pubkey, + }, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html). @@ -741,6 +761,10 @@ impl<'a> TokenInstruction<'a> { 32 => Self::InitializeNonTransferableMint, 33 => Self::InterestBearingMintExtension, 34 => Self::CpiGuardExtension, + 35 => { + let (delegate, _rest) = Self::unpack_pubkey(rest)?; + Self::InitializePermanentDelegate { delegate } + } _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -896,6 +920,10 @@ impl<'a> TokenInstruction<'a> { &Self::CpiGuardExtension => { buf.push(34); } + &Self::InitializePermanentDelegate { ref delegate } => { + buf.push(35); + buf.extend_from_slice(delegate.as_ref()); + } }; buf } @@ -977,6 +1005,8 @@ pub enum AuthorityType { CloseMint, /// Authority to set the interest rate InterestRate, + /// Authority to transfer or burn any tokens for a mint + PermanentDelegate, } impl AuthorityType { @@ -990,6 +1020,7 @@ impl AuthorityType { AuthorityType::WithheldWithdraw => 5, AuthorityType::CloseMint => 6, AuthorityType::InterestRate => 7, + AuthorityType::PermanentDelegate => 8, } } @@ -1003,6 +1034,7 @@ impl AuthorityType { 5 => Ok(AuthorityType::WithheldWithdraw), 6 => Ok(AuthorityType::CloseMint), 7 => Ok(AuthorityType::InterestRate), + 8 => Ok(AuthorityType::PermanentDelegate), _ => Err(TokenError::InvalidInstruction.into()), } } @@ -1753,6 +1785,23 @@ pub fn initialize_non_transferable_mint( }) } +/// Creates an `InitializePermanentDelegate` instruction +pub fn initialize_permanent_delegate( + token_program_id: &Pubkey, + mint_pubkey: &Pubkey, + delegate: &Pubkey, +) -> Result { + check_program_account(token_program_id)?; + Ok(Instruction { + program_id: *token_program_id, + accounts: vec![AccountMeta::new(*mint_pubkey, false)], + data: TokenInstruction::InitializePermanentDelegate { + delegate: *delegate, + } + .pack(), + }) +} + /// Utility function that checks index is between MIN_SIGNERS and MAX_SIGNERS pub fn is_valid_signer_index(index: usize) -> bool { (MIN_SIGNERS..=MAX_SIGNERS).contains(&index) @@ -2072,6 +2121,16 @@ mod test { assert_eq!(packed, expect); let unpacked = TokenInstruction::unpack(&expect).unwrap(); assert_eq!(unpacked, check); + + let check = TokenInstruction::InitializePermanentDelegate { + delegate: Pubkey::new(&[11u8; 32]), + }; + let packed = check.pack(); + let mut expect = vec![35u8]; + expect.extend_from_slice(&[11u8; 32]); + assert_eq!(packed, expect); + let unpacked = TokenInstruction::unpack(&expect).unwrap(); + assert_eq!(unpacked, check); } macro_rules! test_instruction { diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index 659b3281..9f0dc011 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -13,9 +13,10 @@ use { memo_transfer::{self, check_previous_sibling_instruction_is_memo, memo_required}, mint_close_authority::MintCloseAuthority, non_transferable::NonTransferable, + permanent_delegate::{get_permanent_delegate, PermanentDelegate}, reallocate, transfer_fee::{self, TransferFeeAmount, TransferFeeConfig}, - ExtensionType, StateWithExtensions, StateWithExtensionsMut, + BaseStateWithExtensions, ExtensionType, StateWithExtensions, StateWithExtensionsMut, }, instruction::{is_valid_signer_index, AuthorityType, TokenInstruction, MAX_SIGNERS}, native_mint, @@ -138,6 +139,13 @@ impl Processor { let mint_data = mint_info.data.borrow(); let mint = StateWithExtensions::::unpack(&mint_data) .map_err(|_| Into::::into(TokenError::InvalidMint))?; + if mint + .get_extension::() + .map(|e| Option::::from(e.delegate).is_some()) + .unwrap_or(false) + { + msg!("Warning: Mint has a permanent delegate, so tokens in this account may be seized at any time"); + } let required_extensions = Self::get_required_account_extensions_from_unpacked_mint(mint_info.owner, &mint)?; if ExtensionType::get_account_len::(&required_extensions) @@ -279,7 +287,9 @@ impl Processor { if source_account.base.amount < amount { return Err(TokenError::InsufficientFunds.into()); } - let fee = if let Some((mint_info, expected_decimals)) = expected_mint_info { + let (fee, maybe_permanent_delegate) = if let Some((mint_info, expected_decimals)) = + expected_mint_info + { if !cmp_pubkeys(&source_account.base.mint, mint_info.key) { return Err(TokenError::MintMismatch.into()); } @@ -295,13 +305,16 @@ impl Processor { return Err(TokenError::MintDecimalsMismatch.into()); } - if let Ok(transfer_fee_config) = mint.get_extension::() { + let fee = if let Ok(transfer_fee_config) = mint.get_extension::() { transfer_fee_config .calculate_epoch_fee(Clock::get()?.epoch, amount) .ok_or(TokenError::Overflow)? } else { 0 - } + }; + + let maybe_permanent_delegate = get_permanent_delegate(&mint); + (fee, maybe_permanent_delegate) } else { // Transfer fee amount extension exists on the account, but no mint // was provided to calculate the fee, abort @@ -311,7 +324,7 @@ impl Processor { { return Err(TokenError::MintRequiredForTransfer.into()); } else { - 0 + (0, None) } }; if let Some(expected_fee) = expected_fee { @@ -322,8 +335,17 @@ impl Processor { } let self_transfer = cmp_pubkeys(source_account_info.key, destination_account_info.key); - match source_account.base.delegate { - COption::Some(ref delegate) if cmp_pubkeys(authority_info.key, delegate) => { + match (source_account.base.delegate, maybe_permanent_delegate) { + (_, Some(ref delegate)) if cmp_pubkeys(authority_info.key, delegate) => { + Self::validate_owner( + program_id, + delegate, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )? + } + (COption::Some(ref delegate), _) if cmp_pubkeys(authority_info.key, delegate) => { Self::validate_owner( program_id, delegate, @@ -360,7 +382,7 @@ impl Processor { } } } - }; + } // Revisit this later to see if it's worth adding a check to reduce // compute costs, ie: @@ -698,6 +720,19 @@ impl Processor { )?; extension.rate_authority = new_authority.try_into()?; } + AuthorityType::PermanentDelegate => { + let extension = mint.get_extension_mut::()?; + let maybe_delegate: Option = extension.delegate.into(); + let delegate = maybe_delegate.ok_or(TokenError::AuthorityTypeNotSupported)?; + Self::validate_owner( + program_id, + &delegate, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )?; + extension.delegate = new_authority.try_into()?; + } _ => { return Err(TokenError::AuthorityTypeNotSupported.into()); } @@ -828,13 +863,23 @@ impl Processor { return Err(TokenError::MintDecimalsMismatch.into()); } } + let maybe_permanent_delegate = get_permanent_delegate(&mint); if !source_account .base .is_owned_by_system_program_or_incinerator() { - match source_account.base.delegate { - COption::Some(ref delegate) if cmp_pubkeys(authority_info.key, delegate) => { + match (source_account.base.delegate, maybe_permanent_delegate) { + (_, Some(ref delegate)) if cmp_pubkeys(authority_info.key, delegate) => { + Self::validate_owner( + program_id, + delegate, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )? + } + (COption::Some(ref delegate), _) if cmp_pubkeys(authority_info.key, delegate) => { Self::validate_owner( program_id, delegate, @@ -1207,6 +1252,22 @@ impl Processor { Ok(()) } + /// Processes an [InitializePermanentDelegate](enum.TokenInstruction.html) instruction + pub fn process_initialize_permanent_delegate( + accounts: &[AccountInfo], + delegate: Pubkey, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_account_info = next_account_info(account_info_iter)?; + + let mut mint_data = mint_account_info.data.borrow_mut(); + let mut mint = StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; + let extension = mint.init_extension::(true)?; + extension.delegate = Some(delegate).try_into()?; + + Ok(()) + } + /// Processes an [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = TokenInstruction::unpack(input)?; @@ -1370,6 +1431,10 @@ impl Processor { TokenInstruction::CpiGuardExtension => { cpi_guard::processor::process_instruction(program_id, accounts, &input[1..]) } + TokenInstruction::InitializePermanentDelegate { delegate } => { + msg!("Instruction: InitializePermanentDelegate"); + Self::process_initialize_permanent_delegate(accounts, delegate) + } } }