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:
Tyera Eulberg 2022-03-09 19:31:39 -07:00 committed by GitHub
parent 76a92cda2d
commit 810c79ec32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 459 additions and 59 deletions

1
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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