Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 810c79e

Browse files
author
Tyera Eulberg
authored
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
1 parent 76a92cd commit 810c79e

File tree

7 files changed

+459
-59
lines changed

7 files changed

+459
-59
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

token/program-2022-test/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ walkdir = "2"
1515

1616
[dev-dependencies]
1717
async-trait = "0.1"
18+
solana-program = "=1.9.9"
1819
solana-program-test = "=1.9.9"
1920
solana-sdk = "=1.9.9"
2021
spl-associated-token-account = { version = "1.0.5", path = "../../associated-token-account/program" }

token/program-2022-test/tests/burn.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,114 @@ async fn self_owned_with_extension() {
149149
.unwrap();
150150
run_self_owned(context).await;
151151
}
152+
153+
async fn run_burn_and_close_system_or_incinerator(context: TestContext, non_owner: &Pubkey) {
154+
let TokenContext {
155+
decimals,
156+
mint_authority,
157+
token,
158+
alice,
159+
..
160+
} = context.token_context.unwrap();
161+
162+
let alice_account = Keypair::new();
163+
let alice_account = token
164+
.create_auxiliary_token_account(&alice_account, &alice.pubkey())
165+
.await
166+
.unwrap();
167+
168+
// mint a token
169+
token
170+
.mint_to(&alice_account, &mint_authority, 1)
171+
.await
172+
.unwrap();
173+
174+
// transfer token to incinerator/system
175+
let non_owner_account = Keypair::new();
176+
let non_owner_account = token
177+
.create_auxiliary_token_account(&non_owner_account, non_owner)
178+
.await
179+
.unwrap();
180+
token
181+
.transfer_checked(&alice_account, &non_owner_account, &alice, 1, decimals)
182+
.await
183+
.unwrap();
184+
185+
// can't close when holding tokens
186+
let carlos = Keypair::new();
187+
let error = token
188+
.close_account(
189+
&non_owner_account,
190+
&solana_program::incinerator::id(),
191+
&carlos,
192+
)
193+
.await
194+
.unwrap_err();
195+
assert_eq!(
196+
error,
197+
TokenClientError::Client(Box::new(TransportError::TransactionError(
198+
TransactionError::InstructionError(
199+
0,
200+
InstructionError::Custom(TokenError::NonNativeHasBalance as u32)
201+
)
202+
)))
203+
);
204+
205+
// but anyone can burn it
206+
token
207+
.burn_checked(&non_owner_account, &carlos, 1, decimals)
208+
.await
209+
.unwrap();
210+
211+
// closing fails if destination is not the incinerator
212+
let error = token
213+
.close_account(&non_owner_account, &carlos.pubkey(), &carlos)
214+
.await
215+
.unwrap_err();
216+
assert_eq!(
217+
error,
218+
TokenClientError::Client(Box::new(TransportError::TransactionError(
219+
TransactionError::InstructionError(0, InstructionError::InvalidAccountData)
220+
)))
221+
);
222+
223+
let error = token
224+
.close_account(
225+
&non_owner_account,
226+
&solana_program::system_program::id(),
227+
&carlos,
228+
)
229+
.await
230+
.unwrap_err();
231+
assert_eq!(
232+
error,
233+
TokenClientError::Client(Box::new(TransportError::TransactionError(
234+
TransactionError::InstructionError(0, InstructionError::InvalidAccountData)
235+
)))
236+
);
237+
238+
// ... and then close it
239+
token.get_new_latest_blockhash().await.unwrap();
240+
token
241+
.close_account(
242+
&non_owner_account,
243+
&solana_program::incinerator::id(),
244+
&carlos,
245+
)
246+
.await
247+
.unwrap();
248+
}
249+
250+
#[tokio::test]
251+
async fn burn_and_close_incinerator_tokens() {
252+
let mut context = TestContext::new().await;
253+
context.init_token_with_mint(vec![]).await.unwrap();
254+
run_burn_and_close_system_or_incinerator(context, &solana_program::incinerator::id()).await;
255+
}
256+
257+
#[tokio::test]
258+
async fn burn_and_close_system_tokens() {
259+
let mut context = TestContext::new().await;
260+
context.init_token_with_mint(vec![]).await.unwrap();
261+
run_burn_and_close_system_or_incinerator(context, &solana_program::system_program::id()).await;
262+
}

token/program-2022/src/processor.rs

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -784,35 +784,40 @@ impl Processor {
784784
}
785785
}
786786

787-
match source_account.base.delegate {
788-
COption::Some(ref delegate) if cmp_pubkeys(authority_info.key, delegate) => {
789-
Self::validate_owner(
787+
if !source_account
788+
.base
789+
.is_owned_by_system_program_or_incinerator()
790+
{
791+
match source_account.base.delegate {
792+
COption::Some(ref delegate) if cmp_pubkeys(authority_info.key, delegate) => {
793+
Self::validate_owner(
794+
program_id,
795+
delegate,
796+
authority_info,
797+
authority_info_data_len,
798+
account_info_iter.as_slice(),
799+
)?;
800+
801+
if source_account.base.delegated_amount < amount {
802+
return Err(TokenError::InsufficientFunds.into());
803+
}
804+
source_account.base.delegated_amount = source_account
805+
.base
806+
.delegated_amount
807+
.checked_sub(amount)
808+
.ok_or(TokenError::Overflow)?;
809+
if source_account.base.delegated_amount == 0 {
810+
source_account.base.delegate = COption::None;
811+
}
812+
}
813+
_ => Self::validate_owner(
790814
program_id,
791-
delegate,
815+
&source_account.base.owner,
792816
authority_info,
793817
authority_info_data_len,
794818
account_info_iter.as_slice(),
795-
)?;
796-
797-
if source_account.base.delegated_amount < amount {
798-
return Err(TokenError::InsufficientFunds.into());
799-
}
800-
source_account.base.delegated_amount = source_account
801-
.base
802-
.delegated_amount
803-
.checked_sub(amount)
804-
.ok_or(TokenError::Overflow)?;
805-
if source_account.base.delegated_amount == 0 {
806-
source_account.base.delegate = COption::None;
807-
}
819+
)?,
808820
}
809-
_ => Self::validate_owner(
810-
program_id,
811-
&source_account.base.owner,
812-
authority_info,
813-
authority_info_data_len,
814-
account_info_iter.as_slice(),
815-
)?,
816821
}
817822

818823
// Revisit this later to see if it's worth adding a check to reduce
@@ -861,13 +866,20 @@ impl Processor {
861866
.close_authority
862867
.unwrap_or(source_account.base.owner);
863868

864-
Self::validate_owner(
865-
program_id,
866-
&authority,
867-
authority_info,
868-
authority_info_data_len,
869-
account_info_iter.as_slice(),
870-
)?;
869+
if !source_account
870+
.base
871+
.is_owned_by_system_program_or_incinerator()
872+
{
873+
Self::validate_owner(
874+
program_id,
875+
&authority,
876+
authority_info,
877+
authority_info_data_len,
878+
account_info_iter.as_slice(),
879+
)?;
880+
} else if !solana_program::incinerator::check_id(destination_account_info.key) {
881+
return Err(ProgramError::InvalidAccountData);
882+
}
871883

872884
if let Ok(confidential_transfer_state) =
873885
source_account.get_extension::<ConfidentialTransferAccount>()

token/program-2022/src/state.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ impl Account {
116116
pub fn is_native(&self) -> bool {
117117
self.is_native.is_some()
118118
}
119+
/// Checks if a token Account's owner is the system_program or the incinerator
120+
pub fn is_owned_by_system_program_or_incinerator(&self) -> bool {
121+
solana_program::system_program::check_id(&self.owner)
122+
|| solana_program::incinerator::check_id(&self.owner)
123+
}
119124
}
120125
impl Sealed for Account {}
121126
impl IsInitialized for Account {

0 commit comments

Comments
 (0)