token-2022: Allow anyone to burn/close an Account owned by the system program or the incinerator (#2890)
* Allow anyone to burn and close token Accounts owned by the system program and the incinerator * Require rent from incinerator/system-owned token accounts be burnt when accounts closed * Add support to OG program
This commit is contained in:
parent
76a92cda2d
commit
810c79ec32
|
@ -3940,6 +3940,7 @@ name = "spl-token-2022-test"
|
|||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"solana-program",
|
||||
"solana-program-test",
|
||||
"solana-sdk",
|
||||
"spl-associated-token-account 1.0.5",
|
||||
|
|
|
@ -15,6 +15,7 @@ walkdir = "2"
|
|||
|
||||
[dev-dependencies]
|
||||
async-trait = "0.1"
|
||||
solana-program = "=1.9.9"
|
||||
solana-program-test = "=1.9.9"
|
||||
solana-sdk = "=1.9.9"
|
||||
spl-associated-token-account = { version = "1.0.5", path = "../../associated-token-account/program" }
|
||||
|
|
|
@ -149,3 +149,114 @@ async fn self_owned_with_extension() {
|
|||
.unwrap();
|
||||
run_self_owned(context).await;
|
||||
}
|
||||
|
||||
async fn run_burn_and_close_system_or_incinerator(context: TestContext, non_owner: &Pubkey) {
|
||||
let TokenContext {
|
||||
decimals,
|
||||
mint_authority,
|
||||
token,
|
||||
alice,
|
||||
..
|
||||
} = context.token_context.unwrap();
|
||||
|
||||
let alice_account = Keypair::new();
|
||||
let alice_account = token
|
||||
.create_auxiliary_token_account(&alice_account, &alice.pubkey())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// mint a token
|
||||
token
|
||||
.mint_to(&alice_account, &mint_authority, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// transfer token to incinerator/system
|
||||
let non_owner_account = Keypair::new();
|
||||
let non_owner_account = token
|
||||
.create_auxiliary_token_account(&non_owner_account, non_owner)
|
||||
.await
|
||||
.unwrap();
|
||||
token
|
||||
.transfer_checked(&alice_account, &non_owner_account, &alice, 1, decimals)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// can't close when holding tokens
|
||||
let carlos = Keypair::new();
|
||||
let error = token
|
||||
.close_account(
|
||||
&non_owner_account,
|
||||
&solana_program::incinerator::id(),
|
||||
&carlos,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error,
|
||||
TokenClientError::Client(Box::new(TransportError::TransactionError(
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::Custom(TokenError::NonNativeHasBalance as u32)
|
||||
)
|
||||
)))
|
||||
);
|
||||
|
||||
// but anyone can burn it
|
||||
token
|
||||
.burn_checked(&non_owner_account, &carlos, 1, decimals)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// closing fails if destination is not the incinerator
|
||||
let error = token
|
||||
.close_account(&non_owner_account, &carlos.pubkey(), &carlos)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error,
|
||||
TokenClientError::Client(Box::new(TransportError::TransactionError(
|
||||
TransactionError::InstructionError(0, InstructionError::InvalidAccountData)
|
||||
)))
|
||||
);
|
||||
|
||||
let error = token
|
||||
.close_account(
|
||||
&non_owner_account,
|
||||
&solana_program::system_program::id(),
|
||||
&carlos,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error,
|
||||
TokenClientError::Client(Box::new(TransportError::TransactionError(
|
||||
TransactionError::InstructionError(0, InstructionError::InvalidAccountData)
|
||||
)))
|
||||
);
|
||||
|
||||
// ... and then close it
|
||||
token.get_new_latest_blockhash().await.unwrap();
|
||||
token
|
||||
.close_account(
|
||||
&non_owner_account,
|
||||
&solana_program::incinerator::id(),
|
||||
&carlos,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn burn_and_close_incinerator_tokens() {
|
||||
let mut context = TestContext::new().await;
|
||||
context.init_token_with_mint(vec![]).await.unwrap();
|
||||
run_burn_and_close_system_or_incinerator(context, &solana_program::incinerator::id()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn burn_and_close_system_tokens() {
|
||||
let mut context = TestContext::new().await;
|
||||
context.init_token_with_mint(vec![]).await.unwrap();
|
||||
run_burn_and_close_system_or_incinerator(context, &solana_program::system_program::id()).await;
|
||||
}
|
||||
|
|
|
@ -784,35 +784,40 @@ impl Processor {
|
|||
}
|
||||
}
|
||||
|
||||
match source_account.base.delegate {
|
||||
COption::Some(ref delegate) if cmp_pubkeys(authority_info.key, delegate) => {
|
||||
Self::validate_owner(
|
||||
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) => {
|
||||
Self::validate_owner(
|
||||
program_id,
|
||||
delegate,
|
||||
authority_info,
|
||||
authority_info_data_len,
|
||||
account_info_iter.as_slice(),
|
||||
)?;
|
||||
|
||||
if source_account.base.delegated_amount < amount {
|
||||
return Err(TokenError::InsufficientFunds.into());
|
||||
}
|
||||
source_account.base.delegated_amount = source_account
|
||||
.base
|
||||
.delegated_amount
|
||||
.checked_sub(amount)
|
||||
.ok_or(TokenError::Overflow)?;
|
||||
if source_account.base.delegated_amount == 0 {
|
||||
source_account.base.delegate = COption::None;
|
||||
}
|
||||
}
|
||||
_ => Self::validate_owner(
|
||||
program_id,
|
||||
delegate,
|
||||
&source_account.base.owner,
|
||||
authority_info,
|
||||
authority_info_data_len,
|
||||
account_info_iter.as_slice(),
|
||||
)?;
|
||||
|
||||
if source_account.base.delegated_amount < amount {
|
||||
return Err(TokenError::InsufficientFunds.into());
|
||||
}
|
||||
source_account.base.delegated_amount = source_account
|
||||
.base
|
||||
.delegated_amount
|
||||
.checked_sub(amount)
|
||||
.ok_or(TokenError::Overflow)?;
|
||||
if source_account.base.delegated_amount == 0 {
|
||||
source_account.base.delegate = COption::None;
|
||||
}
|
||||
)?,
|
||||
}
|
||||
_ => Self::validate_owner(
|
||||
program_id,
|
||||
&source_account.base.owner,
|
||||
authority_info,
|
||||
authority_info_data_len,
|
||||
account_info_iter.as_slice(),
|
||||
)?,
|
||||
}
|
||||
|
||||
// Revisit this later to see if it's worth adding a check to reduce
|
||||
|
@ -861,13 +866,20 @@ impl Processor {
|
|||
.close_authority
|
||||
.unwrap_or(source_account.base.owner);
|
||||
|
||||
Self::validate_owner(
|
||||
program_id,
|
||||
&authority,
|
||||
authority_info,
|
||||
authority_info_data_len,
|
||||
account_info_iter.as_slice(),
|
||||
)?;
|
||||
if !source_account
|
||||
.base
|
||||
.is_owned_by_system_program_or_incinerator()
|
||||
{
|
||||
Self::validate_owner(
|
||||
program_id,
|
||||
&authority,
|
||||
authority_info,
|
||||
authority_info_data_len,
|
||||
account_info_iter.as_slice(),
|
||||
)?;
|
||||
} else if !solana_program::incinerator::check_id(destination_account_info.key) {
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
|
||||
if let Ok(confidential_transfer_state) =
|
||||
source_account.get_extension::<ConfidentialTransferAccount>()
|
||||
|
|
|
@ -116,6 +116,11 @@ impl Account {
|
|||
pub fn is_native(&self) -> bool {
|
||||
self.is_native.is_some()
|
||||
}
|
||||
/// Checks if a token Account's owner is the system_program or the incinerator
|
||||
pub fn is_owned_by_system_program_or_incinerator(&self) -> bool {
|
||||
solana_program::system_program::check_id(&self.owner)
|
||||
|| solana_program::incinerator::check_id(&self.owner)
|
||||
}
|
||||
}
|
||||
impl Sealed for Account {}
|
||||
impl IsInitialized for Account {
|
||||
|
|
|
@ -612,32 +612,34 @@ impl Processor {
|
|||
}
|
||||
}
|
||||
|
||||
match source_account.delegate {
|
||||
COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => {
|
||||
Self::validate_owner(
|
||||
if !source_account.is_owned_by_system_program_or_incinerator() {
|
||||
match source_account.delegate {
|
||||
COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => {
|
||||
Self::validate_owner(
|
||||
program_id,
|
||||
delegate,
|
||||
authority_info,
|
||||
account_info_iter.as_slice(),
|
||||
)?;
|
||||
|
||||
if source_account.delegated_amount < amount {
|
||||
return Err(TokenError::InsufficientFunds.into());
|
||||
}
|
||||
source_account.delegated_amount = source_account
|
||||
.delegated_amount
|
||||
.checked_sub(amount)
|
||||
.ok_or(TokenError::Overflow)?;
|
||||
if source_account.delegated_amount == 0 {
|
||||
source_account.delegate = COption::None;
|
||||
}
|
||||
}
|
||||
_ => Self::validate_owner(
|
||||
program_id,
|
||||
delegate,
|
||||
&source_account.owner,
|
||||
authority_info,
|
||||
account_info_iter.as_slice(),
|
||||
)?;
|
||||
|
||||
if source_account.delegated_amount < amount {
|
||||
return Err(TokenError::InsufficientFunds.into());
|
||||
}
|
||||
source_account.delegated_amount = source_account
|
||||
.delegated_amount
|
||||
.checked_sub(amount)
|
||||
.ok_or(TokenError::Overflow)?;
|
||||
if source_account.delegated_amount == 0 {
|
||||
source_account.delegate = COption::None;
|
||||
}
|
||||
)?,
|
||||
}
|
||||
_ => Self::validate_owner(
|
||||
program_id,
|
||||
&source_account.owner,
|
||||
authority_info,
|
||||
account_info_iter.as_slice(),
|
||||
)?,
|
||||
}
|
||||
|
||||
if amount == 0 {
|
||||
|
@ -679,12 +681,16 @@ impl Processor {
|
|||
let authority = source_account
|
||||
.close_authority
|
||||
.unwrap_or(source_account.owner);
|
||||
Self::validate_owner(
|
||||
program_id,
|
||||
&authority,
|
||||
authority_info,
|
||||
account_info_iter.as_slice(),
|
||||
)?;
|
||||
if !source_account.is_owned_by_system_program_or_incinerator() {
|
||||
Self::validate_owner(
|
||||
program_id,
|
||||
&authority,
|
||||
authority_info,
|
||||
account_info_iter.as_slice(),
|
||||
)?;
|
||||
} else if !solana_program::incinerator::check_id(destination_account_info.key) {
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
|
||||
let destination_starting_lamports = destination_account_info.lamports();
|
||||
**destination_account_info.lamports.borrow_mut() = destination_starting_lamports
|
||||
|
@ -4575,6 +4581,265 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_burn_and_close_system_and_incinerator_tokens() {
|
||||
let program_id = crate::id();
|
||||
let account_key = Pubkey::new_unique();
|
||||
let mut account_account = SolanaAccount::new(
|
||||
account_minimum_balance(),
|
||||
Account::get_packed_len(),
|
||||
&program_id,
|
||||
);
|
||||
let incinerator_account_key = Pubkey::new_unique();
|
||||
let mut incinerator_account = SolanaAccount::new(
|
||||
account_minimum_balance(),
|
||||
Account::get_packed_len(),
|
||||
&program_id,
|
||||
);
|
||||
let system_account_key = Pubkey::new_unique();
|
||||
let mut system_account = SolanaAccount::new(
|
||||
account_minimum_balance(),
|
||||
Account::get_packed_len(),
|
||||
&program_id,
|
||||
);
|
||||
let owner_key = Pubkey::new_unique();
|
||||
let mut owner_account = SolanaAccount::default();
|
||||
let recipient_key = Pubkey::new_unique();
|
||||
let mut recipient_account = SolanaAccount::default();
|
||||
let mut mock_incinerator_account = SolanaAccount::default();
|
||||
let mint_key = Pubkey::new_unique();
|
||||
let mut mint_account =
|
||||
SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id);
|
||||
|
||||
// create new mint
|
||||
do_process_instruction(
|
||||
initialize_mint2(&program_id, &mint_key, &owner_key, None, 2).unwrap(),
|
||||
vec![&mut mint_account],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// create account
|
||||
do_process_instruction(
|
||||
initialize_account3(&program_id, &account_key, &mint_key, &owner_key).unwrap(),
|
||||
vec![&mut account_account, &mut mint_account],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// create incinerator- and system-owned accounts
|
||||
do_process_instruction(
|
||||
initialize_account3(
|
||||
&program_id,
|
||||
&incinerator_account_key,
|
||||
&mint_key,
|
||||
&solana_program::incinerator::id(),
|
||||
)
|
||||
.unwrap(),
|
||||
vec![&mut incinerator_account, &mut mint_account],
|
||||
)
|
||||
.unwrap();
|
||||
do_process_instruction(
|
||||
initialize_account3(
|
||||
&program_id,
|
||||
&system_account_key,
|
||||
&mint_key,
|
||||
&solana_program::system_program::id(),
|
||||
)
|
||||
.unwrap(),
|
||||
vec![&mut system_account, &mut mint_account],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// mint to account
|
||||
do_process_instruction(
|
||||
mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(),
|
||||
vec![&mut mint_account, &mut account_account, &mut owner_account],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// transfer half to incinerator, half to system program
|
||||
do_process_instruction(
|
||||
transfer(
|
||||
&program_id,
|
||||
&account_key,
|
||||
&incinerator_account_key,
|
||||
&owner_key,
|
||||
&[],
|
||||
500,
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut account_account,
|
||||
&mut incinerator_account,
|
||||
&mut owner_account,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
do_process_instruction(
|
||||
transfer(
|
||||
&program_id,
|
||||
&account_key,
|
||||
&system_account_key,
|
||||
&owner_key,
|
||||
&[],
|
||||
500,
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut account_account,
|
||||
&mut system_account,
|
||||
&mut owner_account,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// close with balance fails
|
||||
assert_eq!(
|
||||
Err(TokenError::NonNativeHasBalance.into()),
|
||||
do_process_instruction(
|
||||
close_account(
|
||||
&program_id,
|
||||
&incinerator_account_key,
|
||||
&solana_program::incinerator::id(),
|
||||
&owner_key,
|
||||
&[]
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut incinerator_account,
|
||||
&mut mock_incinerator_account,
|
||||
&mut owner_account,
|
||||
],
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Err(TokenError::NonNativeHasBalance.into()),
|
||||
do_process_instruction(
|
||||
close_account(
|
||||
&program_id,
|
||||
&system_account_key,
|
||||
&solana_program::incinerator::id(),
|
||||
&owner_key,
|
||||
&[]
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut system_account,
|
||||
&mut mock_incinerator_account,
|
||||
&mut owner_account,
|
||||
],
|
||||
)
|
||||
);
|
||||
|
||||
// anyone can burn
|
||||
do_process_instruction(
|
||||
burn(
|
||||
&program_id,
|
||||
&incinerator_account_key,
|
||||
&mint_key,
|
||||
&recipient_key,
|
||||
&[],
|
||||
500,
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut incinerator_account,
|
||||
&mut mint_account,
|
||||
&mut recipient_account,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
do_process_instruction(
|
||||
burn(
|
||||
&program_id,
|
||||
&system_account_key,
|
||||
&mint_key,
|
||||
&recipient_key,
|
||||
&[],
|
||||
500,
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut system_account,
|
||||
&mut mint_account,
|
||||
&mut recipient_account,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// closing fails if destination is not the incinerator
|
||||
assert_eq!(
|
||||
Err(ProgramError::InvalidAccountData),
|
||||
do_process_instruction(
|
||||
close_account(
|
||||
&program_id,
|
||||
&incinerator_account_key,
|
||||
&recipient_key,
|
||||
&owner_key,
|
||||
&[]
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut incinerator_account,
|
||||
&mut recipient_account,
|
||||
&mut owner_account,
|
||||
],
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Err(ProgramError::InvalidAccountData),
|
||||
do_process_instruction(
|
||||
close_account(
|
||||
&program_id,
|
||||
&system_account_key,
|
||||
&recipient_key,
|
||||
&owner_key,
|
||||
&[]
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut system_account,
|
||||
&mut recipient_account,
|
||||
&mut owner_account,
|
||||
],
|
||||
)
|
||||
);
|
||||
|
||||
// closing succeeds with incinerator recipient
|
||||
do_process_instruction(
|
||||
close_account(
|
||||
&program_id,
|
||||
&incinerator_account_key,
|
||||
&solana_program::incinerator::id(),
|
||||
&owner_key,
|
||||
&[],
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut incinerator_account,
|
||||
&mut mock_incinerator_account,
|
||||
&mut owner_account,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
do_process_instruction(
|
||||
close_account(
|
||||
&program_id,
|
||||
&system_account_key,
|
||||
&solana_program::incinerator::id(),
|
||||
&owner_key,
|
||||
&[],
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut system_account,
|
||||
&mut mock_incinerator_account,
|
||||
&mut owner_account,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multisig() {
|
||||
let program_id = crate::id();
|
||||
|
|
|
@ -113,6 +113,11 @@ impl Account {
|
|||
pub fn is_native(&self) -> bool {
|
||||
self.is_native.is_some()
|
||||
}
|
||||
/// Checks if a token Account's owner is the system_program or the incinerator
|
||||
pub fn is_owned_by_system_program_or_incinerator(&self) -> bool {
|
||||
solana_program::system_program::check_id(&self.owner)
|
||||
|| solana_program::incinerator::check_id(&self.owner)
|
||||
}
|
||||
}
|
||||
impl Sealed for Account {}
|
||||
impl IsInitialized for Account {
|
||||
|
|
Loading…
Reference in New Issue