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::{
|
spl_token_2022::{
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{transfer_fee, ExtensionType, StateWithExtensionsOwned},
|
extension::{
|
||||||
|
transfer_fee, BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned,
|
||||||
|
},
|
||||||
state::{Account, Mint},
|
state::{Account, Mint},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,7 +33,7 @@ use spl_token_2022::{
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{
|
extension::{
|
||||||
mint_close_authority::MintCloseAuthority, transfer_fee::TransferFeeConfig,
|
mint_close_authority::MintCloseAuthority, transfer_fee::TransferFeeConfig,
|
||||||
StateWithExtensions,
|
BaseStateWithExtensions, StateWithExtensions,
|
||||||
},
|
},
|
||||||
state::{Account, Mint},
|
state::{Account, Mint},
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,7 +43,7 @@ use spl_token_2022::{
|
||||||
memo_transfer::MemoTransfer,
|
memo_transfer::MemoTransfer,
|
||||||
mint_close_authority::MintCloseAuthority,
|
mint_close_authority::MintCloseAuthority,
|
||||||
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
|
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
|
||||||
ExtensionType, StateWithExtensionsOwned,
|
BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned,
|
||||||
},
|
},
|
||||||
instruction::*,
|
instruction::*,
|
||||||
state::{Account, AccountState, Mint},
|
state::{Account, AccountState, Mint},
|
||||||
|
@ -744,6 +744,7 @@ async fn command_authorize(
|
||||||
AuthorityType::TransferFeeConfig => "transfer fee authority",
|
AuthorityType::TransferFeeConfig => "transfer fee authority",
|
||||||
AuthorityType::WithheldWithdraw => "withdraw withheld authority",
|
AuthorityType::WithheldWithdraw => "withdraw withheld authority",
|
||||||
AuthorityType::InterestRate => "interest rate authority",
|
AuthorityType::InterestRate => "interest rate authority",
|
||||||
|
AuthorityType::PermanentDelegate => "permanent delegate",
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mint_pubkey, previous_authority) = if !config.sign_only {
|
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))
|
Err(format!("Mint `{}` is not interest-bearing", account))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AuthorityType::PermanentDelegate => unimplemented!(),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
Ok((account, previous_authority))
|
Ok((account, previous_authority))
|
||||||
|
@ -813,7 +815,8 @@ async fn command_authorize(
|
||||||
| AuthorityType::CloseMint
|
| AuthorityType::CloseMint
|
||||||
| AuthorityType::TransferFeeConfig
|
| AuthorityType::TransferFeeConfig
|
||||||
| AuthorityType::WithheldWithdraw
|
| AuthorityType::WithheldWithdraw
|
||||||
| AuthorityType::InterestRate => Err(format!(
|
| AuthorityType::InterestRate
|
||||||
|
| AuthorityType::PermanentDelegate => Err(format!(
|
||||||
"Authority type `{}` not supported for SPL Token accounts",
|
"Authority type `{}` not supported for SPL Token accounts",
|
||||||
auth_str
|
auth_str
|
||||||
)),
|
)),
|
||||||
|
@ -3662,6 +3665,7 @@ async fn process_command<'a>(
|
||||||
"transfer-fee-config" => AuthorityType::TransferFeeConfig,
|
"transfer-fee-config" => AuthorityType::TransferFeeConfig,
|
||||||
"withheld-withdraw" => AuthorityType::WithheldWithdraw,
|
"withheld-withdraw" => AuthorityType::WithheldWithdraw,
|
||||||
"interest-rate" => AuthorityType::InterestRate,
|
"interest-rate" => AuthorityType::InterestRate,
|
||||||
|
"permanent-delegate" => AuthorityType::PermanentDelegate,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,8 @@ use {
|
||||||
spl_token_2022::{
|
spl_token_2022::{
|
||||||
extension::{
|
extension::{
|
||||||
confidential_transfer, cpi_guard, default_account_state, interest_bearing_mint,
|
confidential_transfer, cpi_guard, default_account_state, interest_bearing_mint,
|
||||||
memo_transfer, transfer_fee, ExtensionType, StateWithExtensionsOwned,
|
memo_transfer, transfer_fee, BaseStateWithExtensions, ExtensionType,
|
||||||
|
StateWithExtensionsOwned,
|
||||||
},
|
},
|
||||||
instruction,
|
instruction,
|
||||||
solana_zk_token_sdk::{
|
solana_zk_token_sdk::{
|
||||||
|
@ -122,6 +123,9 @@ pub enum ExtensionInitializationParams {
|
||||||
rate: i16,
|
rate: i16,
|
||||||
},
|
},
|
||||||
NonTransferable,
|
NonTransferable,
|
||||||
|
PermanentDelegate {
|
||||||
|
delegate: Pubkey,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
impl ExtensionInitializationParams {
|
impl ExtensionInitializationParams {
|
||||||
/// Get the extension type associated with the init params
|
/// Get the extension type associated with the init params
|
||||||
|
@ -133,6 +137,7 @@ impl ExtensionInitializationParams {
|
||||||
Self::TransferFeeConfig { .. } => ExtensionType::TransferFeeConfig,
|
Self::TransferFeeConfig { .. } => ExtensionType::TransferFeeConfig,
|
||||||
Self::InterestBearingConfig { .. } => ExtensionType::InterestBearingConfig,
|
Self::InterestBearingConfig { .. } => ExtensionType::InterestBearingConfig,
|
||||||
Self::NonTransferable => ExtensionType::NonTransferable,
|
Self::NonTransferable => ExtensionType::NonTransferable,
|
||||||
|
Self::PermanentDelegate { .. } => ExtensionType::PermanentDelegate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Generate an appropriate initialization instruction for the given mint
|
/// Generate an appropriate initialization instruction for the given mint
|
||||||
|
@ -188,6 +193,9 @@ impl ExtensionInitializationParams {
|
||||||
Self::NonTransferable => {
|
Self::NonTransferable => {
|
||||||
instruction::initialize_non_transferable_mint(token_program_id, mint)
|
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::{
|
confidential_transfer::{
|
||||||
ConfidentialTransferAccount, ConfidentialTransferMint, EncryptedWithheldAmount,
|
ConfidentialTransferAccount, ConfidentialTransferMint, EncryptedWithheldAmount,
|
||||||
},
|
},
|
||||||
ExtensionType,
|
BaseStateWithExtensions, ExtensionType,
|
||||||
},
|
},
|
||||||
solana_zk_token_sdk::{
|
solana_zk_token_sdk::{
|
||||||
encryption::{auth_encryption::*, elgamal::*},
|
encryption::{auth_encryption::*, elgamal::*},
|
||||||
|
|
|
@ -17,7 +17,7 @@ use {
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{
|
extension::{
|
||||||
cpi_guard::{self, CpiGuard},
|
cpi_guard::{self, CpiGuard},
|
||||||
ExtensionType,
|
BaseStateWithExtensions, ExtensionType,
|
||||||
},
|
},
|
||||||
instruction::{self, AuthorityType},
|
instruction::{self, AuthorityType},
|
||||||
processor::Processor as SplToken2022Processor,
|
processor::Processor as SplToken2022Processor,
|
||||||
|
|
|
@ -9,8 +9,10 @@ use {
|
||||||
signer::keypair::Keypair, transaction::TransactionError, transport::TransportError,
|
signer::keypair::Keypair, transaction::TransactionError, transport::TransportError,
|
||||||
},
|
},
|
||||||
spl_token_2022::{
|
spl_token_2022::{
|
||||||
error::TokenError, extension::default_account_state::DefaultAccountState,
|
error::TokenError,
|
||||||
instruction::AuthorityType, state::AccountState,
|
extension::{default_account_state::DefaultAccountState, BaseStateWithExtensions},
|
||||||
|
instruction::AuthorityType,
|
||||||
|
state::AccountState,
|
||||||
},
|
},
|
||||||
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
|
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
|
||||||
std::convert::TryFrom,
|
std::convert::TryFrom,
|
||||||
|
|
|
@ -17,7 +17,7 @@ use {
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{
|
extension::{
|
||||||
transfer_fee::{self, TransferFeeAmount},
|
transfer_fee::{self, TransferFeeAmount},
|
||||||
ExtensionType, StateWithExtensions,
|
BaseStateWithExtensions, ExtensionType, StateWithExtensions,
|
||||||
},
|
},
|
||||||
instruction,
|
instruction,
|
||||||
state::{Account, Mint},
|
state::{Account, Mint},
|
||||||
|
|
|
@ -16,7 +16,10 @@ use {
|
||||||
},
|
},
|
||||||
spl_token_2022::{
|
spl_token_2022::{
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{mint_close_authority::MintCloseAuthority, transfer_fee, ExtensionType},
|
extension::{
|
||||||
|
mint_close_authority::MintCloseAuthority, transfer_fee, BaseStateWithExtensions,
|
||||||
|
ExtensionType,
|
||||||
|
},
|
||||||
instruction, native_mint,
|
instruction, native_mint,
|
||||||
state::Mint,
|
state::Mint,
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,7 +23,7 @@ use {
|
||||||
},
|
},
|
||||||
spl_token_2022::{
|
spl_token_2022::{
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::interest_bearing_mint::InterestBearingConfig,
|
extension::{interest_bearing_mint::InterestBearingConfig, BaseStateWithExtensions},
|
||||||
instruction::{amount_to_ui_amount, ui_amount_to_amount, AuthorityType},
|
instruction::{amount_to_ui_amount, ui_amount_to_amount, AuthorityType},
|
||||||
processor::Processor,
|
processor::Processor,
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,7 +17,7 @@ use {
|
||||||
},
|
},
|
||||||
spl_token_2022::{
|
spl_token_2022::{
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{memo_transfer::MemoTransfer, ExtensionType},
|
extension::{memo_transfer::MemoTransfer, BaseStateWithExtensions, ExtensionType},
|
||||||
},
|
},
|
||||||
spl_token_client::token::TokenError as TokenClientError,
|
spl_token_client::token::TokenError as TokenClientError,
|
||||||
std::sync::Arc,
|
std::sync::Arc,
|
||||||
|
|
|
@ -9,7 +9,9 @@ use {
|
||||||
signer::keypair::Keypair, transaction::TransactionError, transport::TransportError,
|
signer::keypair::Keypair, transaction::TransactionError, transport::TransportError,
|
||||||
},
|
},
|
||||||
spl_token_2022::{
|
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},
|
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
|
||||||
std::convert::TryInto,
|
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::{
|
spl_token_2022::{
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::transfer_fee::{
|
extension::{
|
||||||
TransferFee, TransferFeeAmount, TransferFeeConfig, MAX_FEE_BASIS_POINTS,
|
transfer_fee::{
|
||||||
|
TransferFee, TransferFeeAmount, TransferFeeConfig, MAX_FEE_BASIS_POINTS,
|
||||||
|
},
|
||||||
|
BaseStateWithExtensions,
|
||||||
},
|
},
|
||||||
instruction,
|
instruction,
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,7 @@ use {
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{
|
extension::{
|
||||||
confidential_transfer::{instruction::*, *},
|
confidential_transfer::{instruction::*, *},
|
||||||
StateWithExtensions, StateWithExtensionsMut,
|
BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut,
|
||||||
},
|
},
|
||||||
instruction::{decode_instruction_data, decode_instruction_type},
|
instruction::{decode_instruction_data, decode_instruction_type},
|
||||||
processor::Processor,
|
processor::Processor,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
extension::{Extension, ExtensionType, StateWithExtensionsMut},
|
extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut},
|
||||||
pod::PodBool,
|
pod::PodBool,
|
||||||
state::Account,
|
state::Account,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{Extension, ExtensionType, StateWithExtensionsMut},
|
extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut},
|
||||||
pod::PodBool,
|
pod::PodBool,
|
||||||
state::Account,
|
state::Account,
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,6 +12,7 @@ use {
|
||||||
memo_transfer::MemoTransfer,
|
memo_transfer::MemoTransfer,
|
||||||
mint_close_authority::MintCloseAuthority,
|
mint_close_authority::MintCloseAuthority,
|
||||||
non_transferable::NonTransferable,
|
non_transferable::NonTransferable,
|
||||||
|
permanent_delegate::PermanentDelegate,
|
||||||
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
|
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
|
||||||
},
|
},
|
||||||
pod::*,
|
pod::*,
|
||||||
|
@ -48,6 +49,8 @@ pub mod memo_transfer;
|
||||||
pub mod mint_close_authority;
|
pub mod mint_close_authority;
|
||||||
/// Non Transferable extension
|
/// Non Transferable extension
|
||||||
pub mod non_transferable;
|
pub mod non_transferable;
|
||||||
|
/// Permanent Delegate extension
|
||||||
|
pub mod permanent_delegate;
|
||||||
/// Utility to reallocate token accounts
|
/// Utility to reallocate token accounts
|
||||||
pub mod reallocate;
|
pub mod reallocate;
|
||||||
/// Transfer Fee extension
|
/// 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])
|
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
|
/// Encapsulates owned immutable base state data (mint or account) with possible extensions
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct StateWithExtensionsOwned<S: BaseState> {
|
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
|
impl<S: BaseState> BaseStateWithExtensions<S> for StateWithExtensionsOwned<S> {
|
||||||
pub fn get_extension<V: Extension>(&self) -> Result<&V, ProgramError> {
|
fn get_tlv_data(&self) -> &[u8] {
|
||||||
get_extension::<S, V>(&self.tlv_data)
|
&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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,15 +357,10 @@ impl<'data, S: BaseState> StateWithExtensions<'data, S> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// Unpack a portion of the TLV data as the desired type
|
impl<'a, S: BaseState> BaseStateWithExtensions<S> for StateWithExtensions<'a, S> {
|
||||||
pub fn get_extension<V: Extension>(&self) -> Result<&V, ProgramError> {
|
fn get_tlv_data(&self) -> &[u8] {
|
||||||
get_extension::<S, V>(self.tlv_data)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,25 +466,6 @@ impl<'data, S: BaseState> StateWithExtensionsMut<'data, S> {
|
||||||
pod_from_bytes_mut::<V>(&mut self.tlv_data[value_start..value_end])
|
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
|
/// Packs base state data into the base data portion
|
||||||
pub fn pack_base(&mut self) {
|
pub fn pack_base(&mut self) {
|
||||||
S::pack_into_slice(&self.base, self.base_data);
|
S::pack_into_slice(&self.base, self.base_data);
|
||||||
|
@ -562,14 +558,10 @@ impl<'data, S: BaseState> StateWithExtensionsMut<'data, S> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// Iterates through the TLV entries, returning only the types
|
impl<'a, S: BaseState> BaseStateWithExtensions<S> for StateWithExtensionsMut<'a, S> {
|
||||||
pub fn get_extension_types(&self) -> Result<Vec<ExtensionType>, ProgramError> {
|
fn get_tlv_data(&self) -> &[u8] {
|
||||||
get_extension_types(self.tlv_data)
|
self.tlv_data
|
||||||
}
|
|
||||||
|
|
||||||
fn get_first_extension_type(&self) -> Result<Option<ExtensionType>, ProgramError> {
|
|
||||||
get_first_extension_type(self.tlv_data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -647,6 +639,8 @@ pub enum ExtensionType {
|
||||||
InterestBearingConfig,
|
InterestBearingConfig,
|
||||||
/// Locks privileged token operations from happening via CPI
|
/// Locks privileged token operations from happening via CPI
|
||||||
CpiGuard,
|
CpiGuard,
|
||||||
|
/// Includes an optional permanent delegate
|
||||||
|
PermanentDelegate,
|
||||||
/// Padding extension used to make an account exactly Multisig::LEN, used for testing
|
/// Padding extension used to make an account exactly Multisig::LEN, used for testing
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
AccountPaddingTest = u16::MAX - 1,
|
AccountPaddingTest = u16::MAX - 1,
|
||||||
|
@ -688,6 +682,7 @@ impl ExtensionType {
|
||||||
ExtensionType::NonTransferable => pod_get_packed_len::<NonTransferable>(),
|
ExtensionType::NonTransferable => pod_get_packed_len::<NonTransferable>(),
|
||||||
ExtensionType::InterestBearingConfig => pod_get_packed_len::<InterestBearingConfig>(),
|
ExtensionType::InterestBearingConfig => pod_get_packed_len::<InterestBearingConfig>(),
|
||||||
ExtensionType::CpiGuard => pod_get_packed_len::<CpiGuard>(),
|
ExtensionType::CpiGuard => pod_get_packed_len::<CpiGuard>(),
|
||||||
|
ExtensionType::PermanentDelegate => pod_get_packed_len::<PermanentDelegate>(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
|
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -744,7 +739,8 @@ impl ExtensionType {
|
||||||
| ExtensionType::ConfidentialTransferMint
|
| ExtensionType::ConfidentialTransferMint
|
||||||
| ExtensionType::DefaultAccountState
|
| ExtensionType::DefaultAccountState
|
||||||
| ExtensionType::NonTransferable
|
| ExtensionType::NonTransferable
|
||||||
| ExtensionType::InterestBearingConfig => AccountType::Mint,
|
| ExtensionType::InterestBearingConfig
|
||||||
|
| ExtensionType::PermanentDelegate => AccountType::Mint,
|
||||||
ExtensionType::ImmutableOwner
|
ExtensionType::ImmutableOwner
|
||||||
| ExtensionType::TransferFeeAmount
|
| ExtensionType::TransferFeeAmount
|
||||||
| ExtensionType::ConfidentialTransferAccount
|
| 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 {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
error::TokenError,
|
error::TokenError,
|
||||||
extension::{set_account_type, AccountType, ExtensionType, StateWithExtensions},
|
extension::{
|
||||||
|
set_account_type, AccountType, BaseStateWithExtensions, ExtensionType,
|
||||||
|
StateWithExtensions,
|
||||||
|
},
|
||||||
processor::Processor,
|
processor::Processor,
|
||||||
state::Account,
|
state::Account,
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@ use {
|
||||||
instruction::TransferFeeInstruction, TransferFee, TransferFeeAmount,
|
instruction::TransferFeeInstruction, TransferFee, TransferFeeAmount,
|
||||||
TransferFeeConfig, MAX_FEE_BASIS_POINTS,
|
TransferFeeConfig, MAX_FEE_BASIS_POINTS,
|
||||||
},
|
},
|
||||||
StateWithExtensions, StateWithExtensionsMut,
|
BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut,
|
||||||
},
|
},
|
||||||
processor::Processor,
|
processor::Processor,
|
||||||
state::{Account, Mint},
|
state::{Account, Mint},
|
||||||
|
|
|
@ -610,6 +610,26 @@ pub enum TokenInstruction<'a> {
|
||||||
/// See `extension::cpi_guard::instruction::CpiGuardInstruction` for
|
/// See `extension::cpi_guard::instruction::CpiGuardInstruction` for
|
||||||
/// further details about the extended instructions that share this instruction prefix
|
/// further details about the extended instructions that share this instruction prefix
|
||||||
CpiGuardExtension,
|
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> {
|
impl<'a> TokenInstruction<'a> {
|
||||||
/// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html).
|
/// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html).
|
||||||
|
@ -741,6 +761,10 @@ impl<'a> TokenInstruction<'a> {
|
||||||
32 => Self::InitializeNonTransferableMint,
|
32 => Self::InitializeNonTransferableMint,
|
||||||
33 => Self::InterestBearingMintExtension,
|
33 => Self::InterestBearingMintExtension,
|
||||||
34 => Self::CpiGuardExtension,
|
34 => Self::CpiGuardExtension,
|
||||||
|
35 => {
|
||||||
|
let (delegate, _rest) = Self::unpack_pubkey(rest)?;
|
||||||
|
Self::InitializePermanentDelegate { delegate }
|
||||||
|
}
|
||||||
_ => return Err(TokenError::InvalidInstruction.into()),
|
_ => return Err(TokenError::InvalidInstruction.into()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -896,6 +920,10 @@ impl<'a> TokenInstruction<'a> {
|
||||||
&Self::CpiGuardExtension => {
|
&Self::CpiGuardExtension => {
|
||||||
buf.push(34);
|
buf.push(34);
|
||||||
}
|
}
|
||||||
|
&Self::InitializePermanentDelegate { ref delegate } => {
|
||||||
|
buf.push(35);
|
||||||
|
buf.extend_from_slice(delegate.as_ref());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
@ -977,6 +1005,8 @@ pub enum AuthorityType {
|
||||||
CloseMint,
|
CloseMint,
|
||||||
/// Authority to set the interest rate
|
/// Authority to set the interest rate
|
||||||
InterestRate,
|
InterestRate,
|
||||||
|
/// Authority to transfer or burn any tokens for a mint
|
||||||
|
PermanentDelegate,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthorityType {
|
impl AuthorityType {
|
||||||
|
@ -990,6 +1020,7 @@ impl AuthorityType {
|
||||||
AuthorityType::WithheldWithdraw => 5,
|
AuthorityType::WithheldWithdraw => 5,
|
||||||
AuthorityType::CloseMint => 6,
|
AuthorityType::CloseMint => 6,
|
||||||
AuthorityType::InterestRate => 7,
|
AuthorityType::InterestRate => 7,
|
||||||
|
AuthorityType::PermanentDelegate => 8,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1003,6 +1034,7 @@ impl AuthorityType {
|
||||||
5 => Ok(AuthorityType::WithheldWithdraw),
|
5 => Ok(AuthorityType::WithheldWithdraw),
|
||||||
6 => Ok(AuthorityType::CloseMint),
|
6 => Ok(AuthorityType::CloseMint),
|
||||||
7 => Ok(AuthorityType::InterestRate),
|
7 => Ok(AuthorityType::InterestRate),
|
||||||
|
8 => Ok(AuthorityType::PermanentDelegate),
|
||||||
_ => Err(TokenError::InvalidInstruction.into()),
|
_ => 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
|
/// Utility function that checks index is between MIN_SIGNERS and MAX_SIGNERS
|
||||||
pub fn is_valid_signer_index(index: usize) -> bool {
|
pub fn is_valid_signer_index(index: usize) -> bool {
|
||||||
(MIN_SIGNERS..=MAX_SIGNERS).contains(&index)
|
(MIN_SIGNERS..=MAX_SIGNERS).contains(&index)
|
||||||
|
@ -2072,6 +2121,16 @@ mod test {
|
||||||
assert_eq!(packed, expect);
|
assert_eq!(packed, expect);
|
||||||
let unpacked = TokenInstruction::unpack(&expect).unwrap();
|
let unpacked = TokenInstruction::unpack(&expect).unwrap();
|
||||||
assert_eq!(unpacked, check);
|
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 {
|
macro_rules! test_instruction {
|
||||||
|
|
|
@ -13,9 +13,10 @@ use {
|
||||||
memo_transfer::{self, check_previous_sibling_instruction_is_memo, memo_required},
|
memo_transfer::{self, check_previous_sibling_instruction_is_memo, memo_required},
|
||||||
mint_close_authority::MintCloseAuthority,
|
mint_close_authority::MintCloseAuthority,
|
||||||
non_transferable::NonTransferable,
|
non_transferable::NonTransferable,
|
||||||
|
permanent_delegate::{get_permanent_delegate, PermanentDelegate},
|
||||||
reallocate,
|
reallocate,
|
||||||
transfer_fee::{self, TransferFeeAmount, TransferFeeConfig},
|
transfer_fee::{self, TransferFeeAmount, TransferFeeConfig},
|
||||||
ExtensionType, StateWithExtensions, StateWithExtensionsMut,
|
BaseStateWithExtensions, ExtensionType, StateWithExtensions, StateWithExtensionsMut,
|
||||||
},
|
},
|
||||||
instruction::{is_valid_signer_index, AuthorityType, TokenInstruction, MAX_SIGNERS},
|
instruction::{is_valid_signer_index, AuthorityType, TokenInstruction, MAX_SIGNERS},
|
||||||
native_mint,
|
native_mint,
|
||||||
|
@ -138,6 +139,13 @@ impl Processor {
|
||||||
let mint_data = mint_info.data.borrow();
|
let mint_data = mint_info.data.borrow();
|
||||||
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)
|
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)
|
||||||
.map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?;
|
.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 =
|
let required_extensions =
|
||||||
Self::get_required_account_extensions_from_unpacked_mint(mint_info.owner, &mint)?;
|
Self::get_required_account_extensions_from_unpacked_mint(mint_info.owner, &mint)?;
|
||||||
if ExtensionType::get_account_len::<Account>(&required_extensions)
|
if ExtensionType::get_account_len::<Account>(&required_extensions)
|
||||||
|
@ -279,7 +287,9 @@ impl Processor {
|
||||||
if source_account.base.amount < amount {
|
if source_account.base.amount < amount {
|
||||||
return Err(TokenError::InsufficientFunds.into());
|
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) {
|
if !cmp_pubkeys(&source_account.base.mint, mint_info.key) {
|
||||||
return Err(TokenError::MintMismatch.into());
|
return Err(TokenError::MintMismatch.into());
|
||||||
}
|
}
|
||||||
|
@ -295,13 +305,16 @@ impl Processor {
|
||||||
return Err(TokenError::MintDecimalsMismatch.into());
|
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
|
transfer_fee_config
|
||||||
.calculate_epoch_fee(Clock::get()?.epoch, amount)
|
.calculate_epoch_fee(Clock::get()?.epoch, amount)
|
||||||
.ok_or(TokenError::Overflow)?
|
.ok_or(TokenError::Overflow)?
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
};
|
||||||
|
|
||||||
|
let maybe_permanent_delegate = get_permanent_delegate(&mint);
|
||||||
|
(fee, maybe_permanent_delegate)
|
||||||
} else {
|
} else {
|
||||||
// Transfer fee amount extension exists on the account, but no mint
|
// Transfer fee amount extension exists on the account, but no mint
|
||||||
// was provided to calculate the fee, abort
|
// was provided to calculate the fee, abort
|
||||||
|
@ -311,7 +324,7 @@ impl Processor {
|
||||||
{
|
{
|
||||||
return Err(TokenError::MintRequiredForTransfer.into());
|
return Err(TokenError::MintRequiredForTransfer.into());
|
||||||
} else {
|
} else {
|
||||||
0
|
(0, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(expected_fee) = expected_fee {
|
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);
|
let self_transfer = cmp_pubkeys(source_account_info.key, destination_account_info.key);
|
||||||
match source_account.base.delegate {
|
match (source_account.base.delegate, maybe_permanent_delegate) {
|
||||||
COption::Some(ref delegate) if cmp_pubkeys(authority_info.key, 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(
|
Self::validate_owner(
|
||||||
program_id,
|
program_id,
|
||||||
delegate,
|
delegate,
|
||||||
|
@ -360,7 +382,7 @@ impl Processor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Revisit this later to see if it's worth adding a check to reduce
|
// Revisit this later to see if it's worth adding a check to reduce
|
||||||
// compute costs, ie:
|
// compute costs, ie:
|
||||||
|
@ -698,6 +720,19 @@ impl Processor {
|
||||||
)?;
|
)?;
|
||||||
extension.rate_authority = new_authority.try_into()?;
|
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());
|
return Err(TokenError::AuthorityTypeNotSupported.into());
|
||||||
}
|
}
|
||||||
|
@ -828,13 +863,23 @@ impl Processor {
|
||||||
return Err(TokenError::MintDecimalsMismatch.into());
|
return Err(TokenError::MintDecimalsMismatch.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let maybe_permanent_delegate = get_permanent_delegate(&mint);
|
||||||
|
|
||||||
if !source_account
|
if !source_account
|
||||||
.base
|
.base
|
||||||
.is_owned_by_system_program_or_incinerator()
|
.is_owned_by_system_program_or_incinerator()
|
||||||
{
|
{
|
||||||
match source_account.base.delegate {
|
match (source_account.base.delegate, maybe_permanent_delegate) {
|
||||||
COption::Some(ref delegate) if cmp_pubkeys(authority_info.key, 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(
|
Self::validate_owner(
|
||||||
program_id,
|
program_id,
|
||||||
delegate,
|
delegate,
|
||||||
|
@ -1207,6 +1252,22 @@ impl Processor {
|
||||||
Ok(())
|
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).
|
/// Processes an [Instruction](enum.Instruction.html).
|
||||||
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
|
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
|
||||||
let instruction = TokenInstruction::unpack(input)?;
|
let instruction = TokenInstruction::unpack(input)?;
|
||||||
|
@ -1370,6 +1431,10 @@ impl Processor {
|
||||||
TokenInstruction::CpiGuardExtension => {
|
TokenInstruction::CpiGuardExtension => {
|
||||||
cpi_guard::processor::process_instruction(program_id, accounts, &input[1..])
|
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