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

Commit 7c3c753

Browse files
authored
token-2022: Sync native balance during realloc (#4582)
* token-2022: Sync native balance during realloc * Fix non-empty case and add tests, address other feedback * Don't sync native, add tests for empty extensions * Simplify code, add more test cases * Rename, don't always unpack, use saturating_add * Rename to new_rent_exempt_reserve * Rename variables * Go back to checked math, reorder
1 parent 3bb3b90 commit 7c3c753

File tree

4 files changed

+138
-18
lines changed

4 files changed

+138
-18
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
@@ -29,3 +29,4 @@ spl-instruction-padding = { version = "0.1.0", path="../../instruction-padding/p
2929
spl-token-client = { version = "0.5", path = "../client" }
3030
spl-transfer-hook-example = { version = "0.1", path="../transfer-hook-example", features = ["no-entrypoint"] }
3131
spl-transfer-hook-interface = { version = "0.1", path="../transfer-hook-interface" }
32+
test-case = "3.1"

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

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ use {
55
program_test::{TestContext, TokenContext},
66
solana_program_test::tokio,
77
solana_sdk::{
8-
instruction::InstructionError, program_option::COption, pubkey::Pubkey, signature::Signer,
9-
signer::keypair::Keypair, transaction::TransactionError, transport::TransportError,
8+
instruction::InstructionError,
9+
program_option::COption,
10+
pubkey::Pubkey,
11+
signature::Signer,
12+
signer::keypair::Keypair,
13+
system_instruction,
14+
transaction::{Transaction, TransactionError},
15+
transport::TransportError,
1016
},
1117
spl_token_2022::{error::TokenError, extension::ExtensionType, state::Account},
1218
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
1319
std::convert::TryInto,
20+
test_case::test_case,
1421
};
1522

1623
#[tokio::test]
@@ -188,3 +195,90 @@ async fn reallocate_without_current_extension_knowledge() {
188195
])
189196
);
190197
}
198+
199+
#[test_case(&[ExtensionType::CpiGuard], 1_000_000_000, true ; "transfer more than new rent and sync")]
200+
#[test_case(&[ExtensionType::CpiGuard], 1_000_000_000, false ; "transfer more than new rent")]
201+
#[test_case(&[ExtensionType::CpiGuard], 1, true ; "transfer less than new rent and sync")]
202+
#[test_case(&[ExtensionType::CpiGuard], 1, false ; "transfer less than new rent")]
203+
#[test_case(&[ExtensionType::CpiGuard], 0, false ; "no transfer with extension")]
204+
#[test_case(&[], 1_000_000_000, true ; "transfer lamports and sync without extension")]
205+
#[test_case(&[], 1_000_000_000, false ; "transfer lamports without extension")]
206+
#[test_case(&[], 0, false ; "no transfer without extension")]
207+
#[tokio::test]
208+
async fn reallocate_updates_native_rent_exemption(
209+
extensions: &[ExtensionType],
210+
transfer_lamports: u64,
211+
sync_native: bool,
212+
) {
213+
let mut context = TestContext::new().await;
214+
context.init_token_with_native_mint().await.unwrap();
215+
let TokenContext { token, alice, .. } = context.token_context.unwrap();
216+
let context = context.context.clone();
217+
218+
let alice_account = Keypair::new();
219+
token
220+
.create_auxiliary_token_account(&alice_account, &alice.pubkey())
221+
.await
222+
.unwrap();
223+
let alice_account = alice_account.pubkey();
224+
225+
// transfer more lamports
226+
if transfer_lamports > 0 {
227+
let mut context = context.lock().await;
228+
let instructions = vec![system_instruction::transfer(
229+
&context.payer.pubkey(),
230+
&alice_account,
231+
transfer_lamports,
232+
)];
233+
let tx = Transaction::new_signed_with_payer(
234+
&instructions,
235+
Some(&context.payer.pubkey()),
236+
&[&context.payer],
237+
context.last_blockhash,
238+
);
239+
context.banks_client.process_transaction(tx).await.unwrap();
240+
}
241+
242+
// amount in the account should be 0 no matter what
243+
let account_info = token.get_account_info(&alice_account).await.unwrap();
244+
assert_eq!(account_info.base.amount, 0);
245+
246+
if sync_native {
247+
token.sync_native(&alice_account).await.unwrap();
248+
let account_info = token.get_account_info(&alice_account).await.unwrap();
249+
assert_eq!(account_info.base.amount, transfer_lamports);
250+
}
251+
252+
let token_account = token.get_account_info(&alice_account).await.unwrap();
253+
let pre_amount = token_account.base.amount;
254+
let pre_rent_exempt_reserve = token_account.base.is_native.unwrap();
255+
256+
// reallocate resizes account to accommodate new extension
257+
token
258+
.reallocate(&alice_account, &alice.pubkey(), extensions, &[&alice])
259+
.await
260+
.unwrap();
261+
262+
let account = token.get_account(alice_account).await.unwrap();
263+
assert_eq!(
264+
account.data.len(),
265+
ExtensionType::get_account_len::<Account>(extensions)
266+
);
267+
let expected_rent_exempt_reserve = {
268+
let mut context = context.lock().await;
269+
let rent = context.banks_client.get_rent().await.unwrap();
270+
rent.minimum_balance(account.data.len())
271+
};
272+
let token_account = token.get_account_info(&alice_account).await.unwrap();
273+
let post_amount = token_account.base.amount;
274+
let post_rent_exempt_reserve = token_account.base.is_native.unwrap();
275+
// amount of lamports should be totally unchanged
276+
assert_eq!(pre_amount, post_amount);
277+
// but rent exempt reserve should change
278+
assert_eq!(post_rent_exempt_reserve, expected_rent_exempt_reserve);
279+
if extensions.is_empty() {
280+
assert_eq!(pre_rent_exempt_reserve, post_rent_exempt_reserve);
281+
} else {
282+
assert!(pre_rent_exempt_reserve < post_rent_exempt_reserve);
283+
}
284+
}

token/program-2022/src/extension/reallocate.rs

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use {
33
error::TokenError,
44
extension::{
55
set_account_type, AccountType, BaseStateWithExtensions, ExtensionType,
6-
StateWithExtensions,
6+
StateWithExtensions, StateWithExtensionsMut,
77
},
88
processor::Processor,
99
state::Account,
@@ -13,6 +13,7 @@ use {
1313
entrypoint::ProgramResult,
1414
msg,
1515
program::invoke,
16+
program_option::COption,
1617
pubkey::Pubkey,
1718
system_instruction,
1819
sysvar::{rent::Rent, Sysvar},
@@ -33,7 +34,7 @@ pub fn process_reallocate(
3334
let authority_info_data_len = authority_info.data_len();
3435

3536
// check that account is the right type and validate owner
36-
let mut current_extension_types = {
37+
let (mut current_extension_types, native_token_amount) = {
3738
let token_account = token_account_info.data.borrow();
3839
let account = StateWithExtensions::<Account>::unpack(&token_account)?;
3940
Processor::validate_owner(
@@ -43,7 +44,8 @@ pub fn process_reallocate(
4344
authority_info_data_len,
4445
account_info_iter.as_slice(),
4546
)?;
46-
account.get_extension_types()?
47+
let native_token_amount = account.base.is_native().then(|| account.base.amount);
48+
(account.get_extension_types()?, native_token_amount)
4749
};
4850

4951
// check that all desired extensions are for the right account type
@@ -71,20 +73,42 @@ pub fn process_reallocate(
7173

7274
// if additional lamports needed to remain rent-exempt, transfer them
7375
let rent = Rent::get()?;
74-
let new_minimum_balance = rent.minimum_balance(needed_account_len);
75-
let lamports_diff = new_minimum_balance.saturating_sub(token_account_info.lamports());
76-
invoke(
77-
&system_instruction::transfer(payer_info.key, token_account_info.key, lamports_diff),
78-
&[
79-
payer_info.clone(),
80-
token_account_info.clone(),
81-
system_program_info.clone(),
82-
],
83-
)?;
76+
let new_rent_exempt_reserve = rent.minimum_balance(needed_account_len);
8477

85-
// unpack to set account_type, if needed
86-
let mut token_account = token_account_info.data.borrow_mut();
87-
set_account_type::<Account>(&mut token_account)?;
78+
let current_lamport_reserve = token_account_info
79+
.lamports()
80+
.checked_sub(native_token_amount.unwrap_or(0))
81+
.ok_or(TokenError::Overflow)?;
82+
let lamports_diff = new_rent_exempt_reserve.saturating_sub(current_lamport_reserve);
83+
if lamports_diff > 0 {
84+
invoke(
85+
&system_instruction::transfer(payer_info.key, token_account_info.key, lamports_diff),
86+
&[
87+
payer_info.clone(),
88+
token_account_info.clone(),
89+
system_program_info.clone(),
90+
],
91+
)?;
92+
}
93+
94+
// set account_type, if needed
95+
let mut token_account_data = token_account_info.data.borrow_mut();
96+
set_account_type::<Account>(&mut token_account_data)?;
97+
98+
// sync the rent exempt reserve for native accounts
99+
if let Some(native_token_amount) = native_token_amount {
100+
let mut token_account = StateWithExtensionsMut::<Account>::unpack(&mut token_account_data)?;
101+
// sanity check that there are enough lamports to cover the token amount
102+
// and the rent exempt reserve
103+
let minimum_lamports = new_rent_exempt_reserve
104+
.checked_add(native_token_amount)
105+
.ok_or(TokenError::Overflow)?;
106+
if token_account_info.lamports() < minimum_lamports {
107+
return Err(TokenError::InvalidState.into());
108+
}
109+
token_account.base.is_native = COption::Some(new_rent_exempt_reserve);
110+
token_account.pack_base();
111+
}
88112

89113
Ok(())
90114
}

0 commit comments

Comments
 (0)