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

Commit d3539c1

Browse files
authored
token-2022: Fail to close account with withheld fees (#2871)
* token-2022: Fail to close account with withheld fees * Add comment to use `get_extension`
1 parent 93c7ad7 commit d3539c1

File tree

4 files changed

+87
-1
lines changed

4 files changed

+87
-1
lines changed

token/program-2022/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ pub enum TokenError {
124124
/// The owner authority cannot be changed
125125
#[error("The owner authority cannot be changed")]
126126
ImmutableOwner,
127+
/// An account can only be closed if its withheld fee balance is zero, harvest fees to the
128+
/// mint and try again
129+
#[error("An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again")]
130+
AccountHasWithheldTransferFees,
127131
}
128132
impl From<TokenError> for ProgramError {
129133
fn from(e: TokenError) -> Self {

token/program-2022/src/extension/transfer_fee/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use {
22
crate::{
3+
error::TokenError,
34
extension::{Extension, ExtensionType},
45
pod::*,
56
},
67
bytemuck::{Pod, Zeroable},
7-
solana_program::clock::Epoch,
8+
solana_program::{clock::Epoch, entrypoint::ProgramResult},
89
std::{cmp, convert::TryFrom},
910
};
1011

@@ -90,6 +91,16 @@ pub struct TransferFeeAmount {
9091
/// Amount withheld during transfers, to be harvested to the mint
9192
pub withheld_amount: PodU64,
9293
}
94+
impl TransferFeeAmount {
95+
/// Check if the extension is in a closable state
96+
pub fn closable(&self) -> ProgramResult {
97+
if self.withheld_amount == 0.into() {
98+
Ok(())
99+
} else {
100+
Err(TokenError::AccountHasWithheldTransferFees.into())
101+
}
102+
}
103+
}
93104
impl Extension for TransferFeeAmount {
94105
const TYPE: ExtensionType = ExtensionType::TransferFeeAmount;
95106
}

token/program-2022/src/processor.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,11 +847,20 @@ impl Processor {
847847
account_info_iter.as_slice(),
848848
)?;
849849

850+
// TODO use get_extension when
851+
// https://github.com/solana-labs/solana-program-library/pull/2822 lands
850852
if let Ok(confidential_transfer_state) =
851853
source_account.get_extension_mut::<ConfidentialTransferAccount>()
852854
{
853855
confidential_transfer_state.closable()?
854856
}
857+
858+
// TODO use get_extension when
859+
// https://github.com/solana-labs/solana-program-library/pull/2822 lands
860+
if let Ok(transfer_fee_state) = source_account.get_extension_mut::<TransferFeeAmount>()
861+
{
862+
transfer_fee_state.closable()?
863+
}
855864
} else if let Ok(mut mint) =
856865
StateWithExtensionsMut::<Mint>::unpack(&mut source_account_data)
857866
{
@@ -1284,6 +1293,9 @@ impl PrintProgramError for TokenError {
12841293
TokenError::ImmutableOwner => {
12851294
msg!("The owner authority cannot be changed");
12861295
}
1296+
TokenError::AccountHasWithheldTransferFees => {
1297+
msg!("Error: An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again");
1298+
}
12871299
}
12881300
}
12891301
}

token/program-2022/tests/transfer_fee.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,3 +1539,62 @@ async fn withdraw_withheld_tokens_from_accounts() {
15391539
)))
15401540
);
15411541
}
1542+
1543+
#[tokio::test]
1544+
async fn fail_close_with_withheld() {
1545+
let amount = TEST_MAXIMUM_FEE;
1546+
let alice_amount = amount * 100;
1547+
let TokenWithAccounts {
1548+
token,
1549+
transfer_fee_config,
1550+
alice,
1551+
alice_account,
1552+
decimals,
1553+
..
1554+
} = create_mint_with_accounts(alice_amount).await;
1555+
1556+
// accrue withheld fees on new account
1557+
let account = create_and_transfer_to_account(
1558+
&token,
1559+
&alice_account,
1560+
&alice,
1561+
&alice.pubkey(),
1562+
amount,
1563+
decimals,
1564+
)
1565+
.await;
1566+
1567+
// empty the account
1568+
let fee = transfer_fee_config.calculate_epoch_fee(0, amount).unwrap();
1569+
token
1570+
.transfer_checked(&account, &alice_account, &alice, amount - fee, decimals)
1571+
.await
1572+
.unwrap();
1573+
1574+
// fail to close
1575+
let error = token
1576+
.close_account(&account, &Pubkey::new_unique(), &alice)
1577+
.await
1578+
.unwrap_err();
1579+
assert_eq!(
1580+
error,
1581+
TokenClientError::Client(Box::new(TransportError::TransactionError(
1582+
TransactionError::InstructionError(
1583+
0,
1584+
InstructionError::Custom(TokenError::AccountHasWithheldTransferFees as u32)
1585+
)
1586+
)))
1587+
);
1588+
1589+
// harvest the fees to the mint
1590+
token
1591+
.harvest_withheld_tokens_to_mint(&[&account])
1592+
.await
1593+
.unwrap();
1594+
1595+
// successfully close
1596+
token
1597+
.close_account(&account, &Pubkey::new_unique(), &alice)
1598+
.await
1599+
.unwrap();
1600+
}

0 commit comments

Comments
 (0)