token-2022: Add PermanentDelegate extension (#3725)

* token-2022: Add PermanentDelegate extension

* Address feedback

* Refactor getting permanent delegate

* Rename function

* More cleanup

* Fix ATA
This commit is contained in:
Jon Cinque 2022-11-15 14:10:59 +01:00 committed by GitHub
parent 0ddbd71302
commit 8d0a2e1000
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 529 additions and 76 deletions

View File

@ -17,7 +17,9 @@ use {
},
spl_token_2022::{
error::TokenError,
extension::{transfer_fee, ExtensionType, StateWithExtensionsOwned},
extension::{
transfer_fee, BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned,
},
state::{Account, Mint},
},
};

View File

@ -33,7 +33,7 @@ use spl_token_2022::{
error::TokenError,
extension::{
mint_close_authority::MintCloseAuthority, transfer_fee::TransferFeeConfig,
StateWithExtensions,
BaseStateWithExtensions, StateWithExtensions,
},
state::{Account, Mint},
};

View File

@ -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!(),
};

View File

@ -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)
}
}
}
}

View File

@ -15,7 +15,7 @@ use {
confidential_transfer::{
ConfidentialTransferAccount, ConfidentialTransferMint, EncryptedWithheldAmount,
},
ExtensionType,
BaseStateWithExtensions, ExtensionType,
},
solana_zk_token_sdk::{
encryption::{auth_encryption::*, elgamal::*},

View File

@ -17,7 +17,7 @@ use {
error::TokenError,
extension::{
cpi_guard::{self, CpiGuard},
ExtensionType,
BaseStateWithExtensions, ExtensionType,
},
instruction::{self, AuthorityType},
processor::Processor as SplToken2022Processor,

View File

@ -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,

View File

@ -17,7 +17,7 @@ use {
error::TokenError,
extension::{
transfer_fee::{self, TransferFeeAmount},
ExtensionType, StateWithExtensions,
BaseStateWithExtensions, ExtensionType, StateWithExtensions,
},
instruction,
state::{Account, Mint},

View File

@ -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,
},

View File

@ -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,
},

View File

@ -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,

View File

@ -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,

View File

@ -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::<PermanentDelegate>().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::<PermanentDelegate>().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::<PermanentDelegate>().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)
)))
);
}

View File

@ -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,
},

View File

@ -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,

View File

@ -1,6 +1,6 @@
use {
crate::{
extension::{Extension, ExtensionType, StateWithExtensionsMut},
extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut},
pod::PodBool,
state::Account,
},

View File

@ -1,7 +1,7 @@
use {
crate::{
error::TokenError,
extension::{Extension, ExtensionType, StateWithExtensionsMut},
extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut},
pod::PodBool,
state::Account,
},

View File

@ -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<S: BaseState, V: Extension>(tlv_data: &[u8]) -> Result<&V, Prog
pod_from_bytes::<V>(&tlv_data[value_start..value_end])
}
/// Trait for base state with extension
pub trait BaseStateWithExtensions<S: BaseState> {
/// 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<V: Extension>(&self) -> Result<&V, ProgramError> {
get_extension::<S, V>(self.get_tlv_data())
}
/// Iterates through the TLV entries, returning only the types
fn get_extension_types(&self) -> Result<Vec<ExtensionType>, 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<Option<ExtensionType>, 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<S: BaseState> {
@ -293,15 +317,11 @@ impl<S: BaseState> StateWithExtensionsOwned<S> {
})
}
}
}
/// Unpack a portion of the TLV data as the desired type
pub fn get_extension<V: Extension>(&self) -> Result<&V, ProgramError> {
get_extension::<S, V>(&self.tlv_data)
}
/// Iterates through the TLV entries, returning only the types
pub fn get_extension_types(&self) -> Result<Vec<ExtensionType>, ProgramError> {
get_extension_types(&self.tlv_data)
impl<S: BaseState> BaseStateWithExtensions<S> for StateWithExtensionsOwned<S> {
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<V: Extension>(&self) -> Result<&V, ProgramError> {
get_extension::<S, V>(self.tlv_data)
}
/// Iterates through the TLV entries, returning only the types
pub fn get_extension_types(&self) -> Result<Vec<ExtensionType>, ProgramError> {
get_extension_types(self.tlv_data)
}
impl<'a, S: BaseState> BaseStateWithExtensions<S> 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::<V>(&mut self.tlv_data[value_start..value_end])
}
/// Unpack a portion of the TLV data as the desired type
pub fn get_extension<V: 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::<V>(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::<Length>(&self.tlv_data[length_start..value_start])?;
let value_end = value_start.saturating_add(usize::from(*length));
pod_from_bytes::<V>(&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<Vec<ExtensionType>, ProgramError> {
get_extension_types(self.tlv_data)
}
fn get_first_extension_type(&self) -> Result<Option<ExtensionType>, ProgramError> {
get_first_extension_type(self.tlv_data)
}
impl<'a, S: BaseState> BaseStateWithExtensions<S> 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::<NonTransferable>(),
ExtensionType::InterestBearingConfig => pod_get_packed_len::<InterestBearingConfig>(),
ExtensionType::CpiGuard => pod_get_packed_len::<CpiGuard>(),
ExtensionType::PermanentDelegate => pod_get_packed_len::<PermanentDelegate>(),
#[cfg(test)]
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
#[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

View File

@ -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<S: BaseState, BSE: BaseStateWithExtensions<S>>(
state: &BSE,
) -> Option<Pubkey> {
state
.get_extension::<PermanentDelegate>()
.ok()
.and_then(|e| Option::<Pubkey>::from(e.delegate))
}

View File

@ -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,
},

View File

@ -7,7 +7,7 @@ use {
instruction::TransferFeeInstruction, TransferFee, TransferFeeAmount,
TransferFeeConfig, MAX_FEE_BASIS_POINTS,
},
StateWithExtensions, StateWithExtensionsMut,
BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut,
},
processor::Processor,
state::{Account, Mint},

View File

@ -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<Instruction, ProgramError> {
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 {

View File

@ -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::<Mint>::unpack(&mint_data)
.map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?;
if mint
.get_extension::<PermanentDelegate>()
.map(|e| Option::<Pubkey>::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::<Account>(&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::<TransferFeeConfig>() {
let fee = if let Ok(transfer_fee_config) = mint.get_extension::<TransferFeeConfig>() {
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::<PermanentDelegate>()?;
let maybe_delegate: Option<Pubkey> = 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::<Mint>::unpack_uninitialized(&mut mint_data)?;
let extension = mint.init_extension::<PermanentDelegate>(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)
}
}
}