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:
parent
0ddbd71302
commit
8d0a2e1000
|
@ -17,7 +17,9 @@ use {
|
|||
},
|
||||
spl_token_2022::{
|
||||
error::TokenError,
|
||||
extension::{transfer_fee, ExtensionType, StateWithExtensionsOwned},
|
||||
extension::{
|
||||
transfer_fee, BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned,
|
||||
},
|
||||
state::{Account, Mint},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -33,7 +33,7 @@ use spl_token_2022::{
|
|||
error::TokenError,
|
||||
extension::{
|
||||
mint_close_authority::MintCloseAuthority, transfer_fee::TransferFeeConfig,
|
||||
StateWithExtensions,
|
||||
BaseStateWithExtensions, StateWithExtensions,
|
||||
},
|
||||
state::{Account, Mint},
|
||||
};
|
||||
|
|
|
@ -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!(),
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ use {
|
|||
confidential_transfer::{
|
||||
ConfidentialTransferAccount, ConfidentialTransferMint, EncryptedWithheldAmount,
|
||||
},
|
||||
ExtensionType,
|
||||
BaseStateWithExtensions, ExtensionType,
|
||||
},
|
||||
solana_zk_token_sdk::{
|
||||
encryption::{auth_encryption::*, elgamal::*},
|
||||
|
|
|
@ -17,7 +17,7 @@ use {
|
|||
error::TokenError,
|
||||
extension::{
|
||||
cpi_guard::{self, CpiGuard},
|
||||
ExtensionType,
|
||||
BaseStateWithExtensions, ExtensionType,
|
||||
},
|
||||
instruction::{self, AuthorityType},
|
||||
processor::Processor as SplToken2022Processor,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -17,7 +17,7 @@ use {
|
|||
error::TokenError,
|
||||
extension::{
|
||||
transfer_fee::{self, TransferFeeAmount},
|
||||
ExtensionType, StateWithExtensions,
|
||||
BaseStateWithExtensions, ExtensionType, StateWithExtensions,
|
||||
},
|
||||
instruction,
|
||||
state::{Account, Mint},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
)))
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use {
|
||||
crate::{
|
||||
extension::{Extension, ExtensionType, StateWithExtensionsMut},
|
||||
extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut},
|
||||
pod::PodBool,
|
||||
state::Account,
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use {
|
||||
crate::{
|
||||
error::TokenError,
|
||||
extension::{Extension, ExtensionType, StateWithExtensionsMut},
|
||||
extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut},
|
||||
pod::PodBool,
|
||||
state::Account,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@ use {
|
|||
instruction::TransferFeeInstruction, TransferFee, TransferFeeAmount,
|
||||
TransferFeeConfig, MAX_FEE_BASIS_POINTS,
|
||||
},
|
||||
StateWithExtensions, StateWithExtensionsMut,
|
||||
BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut,
|
||||
},
|
||||
processor::Processor,
|
||||
state::{Account, Mint},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue