Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions basics/realloc/native/program/src/instructions/reallocate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ pub fn reallocate_without_zero_init(accounts: &[AccountInfo], args: EnhancedAddr
let account_span = borsh::to_vec(&enhanced_address_info_data)?.len();
let lamports_required = (Rent::get()?).minimum_balance(account_span);

let diff = lamports_required - target_account.lamports();
invoke(
&solana_system_interface::instruction::transfer(payer.key, target_account.key, diff),
&[payer.clone(), target_account.clone(), system_program.clone()],
)?;
let diff = lamports_required.saturating_sub(target_account.lamports());
if diff > 0 {
invoke(
&solana_system_interface::instruction::transfer(payer.key, target_account.key, diff),
&[payer.clone(), target_account.clone(), system_program.clone()],
)?;
}

target_account.resize(account_span)?;

Expand Down
52 changes: 52 additions & 0 deletions basics/realloc/native/tests/realloc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
setTransactionMessageFeePayerSigner,
signTransactionMessageWithSigners,
} from '@solana/kit';
import { getTransferSolInstruction } from '@solana-program/system';
import { assert } from 'chai';
import { FailedTransactionMetadata, LiteSVM } from 'litesvm';
import {
Expand All @@ -21,6 +22,11 @@ import {
workInfoDecoder,
} from '../ts';

// Serialized `EnhancedAddressInfo` for the values used below (borsh: 4-byte length-prefixed strings).
const enhancedAddressInfoLength = 4 + 5 + 1 + 4 + 8 + 4 + 7 + 4 + 8 + 4;
// LiteSVM's default fee for a single-signature transaction.
const TRANSACTION_FEE = 5_000n;

describe('Realloc!', () => {
const svm = new LiteSVM();
let programId: Address;
Expand Down Expand Up @@ -80,6 +86,52 @@ describe('Realloc!', () => {
printWorkInfo(testAccount.address);
});

it('Reallocate WITHOUT zero init when the account already holds more than the new rent-exempt minimum', async () => {
const overfundedAccount = await generateKeyPairSigner();
await sendTransaction(
createCreateInstruction(overfundedAccount, payer, programId, 'Jacob', 123, 'Main St.', 'Chicago'),
);

// Anyone can push the account above the enlarged rent-exempt minimum with a plain system transfer.
const enlargedMinimum = svm.minimumBalanceForRentExemption(BigInt(enhancedAddressInfoLength));
await sendTransaction(
getTransferSolInstruction({
amount: lamports(enlargedMinimum),
destination: overfundedAccount.address,
source: payer,
}),
);

const targetLamportsBefore = svm.getBalance(overfundedAccount.address);
const payerLamportsBefore = svm.getBalance(payer.address);
assert(targetLamportsBefore !== null && payerLamportsBefore !== null);
assert(targetLamportsBefore > enlargedMinimum);

const ix = createReallocateWithoutZeroInitInstruction(
overfundedAccount.address,
payer,
programId,
'Illinois',
12345,
);
await sendTransaction(ix);

const account = svm.getAccount(overfundedAccount.address);
assert(account.exists, 'test account not found');
assert.strictEqual(account.data.length, enhancedAddressInfoLength);
const enhancedAddressInfo = enhancedAddressInfoDecoder.decode(account.data);
assert.strictEqual(enhancedAddressInfo.name, 'Jacob');
assert.strictEqual(enhancedAddressInfo.house_number, 123);
assert.strictEqual(enhancedAddressInfo.street, 'Main St.');
assert.strictEqual(enhancedAddressInfo.city, 'Chicago');
assert.strictEqual(enhancedAddressInfo.state, 'Illinois');
assert.strictEqual(enhancedAddressInfo.zip, 12345);

// No top-up was needed: the target keeps its balance and the payer only pays the transaction fee.
assert.strictEqual(svm.getBalance(overfundedAccount.address), targetLamportsBefore);
assert.strictEqual(payerLamportsBefore - svm.getBalance(payer.address)!, TRANSACTION_FEE);
});

function printAddressInfo(address: Address): void {
const account = svm.getAccount(address);
if (account.exists) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ pub fn reallocate_without_zero_init(accounts: &mut [AccountView], instruction_da
let account_span = EnhancedAddressInfo::LEN;
let lamports_required = (Rent::get()?).try_minimum_balance(account_span)?;

let diff = lamports_required - target_account.lamports();

Transfer { from: payer, to: target_account, lamports: diff }.invoke()?;
let diff = lamports_required.saturating_sub(target_account.lamports());
if diff > 0 {
Transfer { from: payer, to: target_account, lamports: diff }.invoke()?;
}

target_account.resize(account_span)?;

Expand Down
68 changes: 67 additions & 1 deletion basics/realloc/pinocchio/tests/realloc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
setTransactionMessageFeePayerSigner,
signTransactionMessageWithSigners,
} from '@solana/kit';
import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system';
import { getTransferSolInstruction, SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system';
import { assert } from 'chai';
import { FailedTransactionMetadata, LiteSVM } from 'litesvm';

Expand Down Expand Up @@ -73,6 +73,10 @@ const workInfoDecoder = getStructDecoder([
['yearsEmployed', getU8Decoder()],
]);

const ENHANCED_ADDRESS_INFO_LEN = 37;
// LiteSVM's default fee for a single-signature transaction.
const TRANSACTION_FEE = 5_000n;

describe('Realloc!', () => {
const svm = new LiteSVM();
let programId: Address;
Expand Down Expand Up @@ -182,4 +186,66 @@ describe('Realloc!', () => {
assert.strictEqual(workInfo.company, 'Anza');
assert.strictEqual(workInfo.yearsEmployed, 2);
});
it('Reallocate WITHOUT zero init when the account already holds more than the new rent-exempt minimum', async () => {
const overfundedAccount = await generateKeyPairSigner();
const createIx = {
programAddress: programId,
accounts: [
{ address: overfundedAccount.address, role: AccountRole.WRITABLE_SIGNER, signer: overfundedAccount },
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
],
data: createEncoder.encode({
discriminator: 0,
name: 'Jacob',
houseNumber: 123,
street: 'Main St.',
city: 'Chicago',
}),
};
await sendInstruction(createIx);

// Anyone can push the account above the enlarged rent-exempt minimum with a plain system transfer.
const enlargedMinimum = svm.minimumBalanceForRentExemption(BigInt(ENHANCED_ADDRESS_INFO_LEN));
await sendInstruction(
getTransferSolInstruction({
amount: lamports(enlargedMinimum),
destination: overfundedAccount.address,
source: payer,
}),
);

const targetLamportsBefore = svm.getBalance(overfundedAccount.address);
const payerLamportsBefore = svm.getBalance(payer.address);
assert(targetLamportsBefore !== null && payerLamportsBefore !== null);
assert(targetLamportsBefore > enlargedMinimum);

const reallocIx = {
programAddress: programId,
accounts: [
{ address: overfundedAccount.address, role: AccountRole.WRITABLE },
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
],
data: reallocWithoutZeroInitEncoder.encode({ discriminator: 1, state: 'Illinois', zip: 12345 }),
};
await sendInstruction(reallocIx);

const account = svm.getAccount(overfundedAccount.address);
assert(account.exists, 'test account not found');
const data = new Uint8Array(account.data);
assert.strictEqual(data.length, ENHANCED_ADDRESS_INFO_LEN);

const addressInfo = enhancedAddressInfoDecoder.decode(data);
assert.strictEqual(addressInfo.name, 'Jacob');
assert.strictEqual(addressInfo.houseNumber, 123);
assert.strictEqual(addressInfo.street, 'Main St.');
assert.strictEqual(addressInfo.city, 'Chicago');
assert.strictEqual(addressInfo.state, 'Illinois');
assert.strictEqual(addressInfo.zip, 12345);

// No top-up was needed: the target keeps its balance and the payer only pays the transaction fee.
assert.strictEqual(svm.getBalance(overfundedAccount.address), targetLamportsBefore);
assert.strictEqual(payerLamportsBefore - svm.getBalance(payer.address)!, TRANSACTION_FEE);
});
});