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
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
Expand All @@ -12,18 +14,32 @@ pub fn create_new_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> Prog
let accounts_iter = &mut accounts.iter();
let new_account = next_account_info(accounts_iter)?;
let rent_vault = next_account_info(accounts_iter)?;
let _system_program = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;

let (rent_vault_pda, _rent_vault_bump) =
if !new_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}

let (rent_vault_pda, rent_vault_bump) =
Pubkey::find_program_address(&[RentVault::SEED_PREFIX.as_bytes()], program_id);
assert!(rent_vault.key.eq(&rent_vault_pda));

// Assuming this account has no inner data (size 0)
//
let lamports_required_for_rent = (Rent::get()?).minimum_balance(0);

**rent_vault.lamports.borrow_mut() -= lamports_required_for_rent;
**new_account.lamports.borrow_mut() += lamports_required_for_rent;
// Create the new account, transferring lamports from the rent vault to the new account
invoke_signed(
&solana_system_interface::instruction::create_account(
rent_vault.key,
new_account.key,
lamports_required_for_rent,
0,
&solana_system_interface::program::ID,
),
&[rent_vault.clone(), new_account.clone(), system_program.clone()],
&[&[RentVault::SEED_PREFIX.as_bytes(), &[rent_vault_bump]]],
)?;

Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub fn init_rent_vault(program_id: &Pubkey, accounts: &[AccountInfo], args: Init
rent_vault.key,
lamports_required,
0,
program_id,
&solana_system_interface::program::ID,
),
&[payer.clone(), rent_vault.clone(), system_program.clone()],
&[&[RentVault::SEED_PREFIX.as_bytes(), &[rent_vault_bump]]],
Expand Down
99 changes: 79 additions & 20 deletions basics/pda-rent-payer/native/tests/test.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,128 @@
import {
AccountRole,
type Address,
appendTransactionMessageInstruction,
createTransactionMessage,
generateKeyPairSigner,
getProgramDerivedAddress,
type Instruction,
type KeyPairSigner,
lamports,
pipe,
setTransactionMessageFeePayerSigner,
signTransactionMessageWithSigners,
} from '@solana/kit';
import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system';
import { assert } from 'chai';
import { FailedTransactionMetadata, LiteSVM } from 'litesvm';
import { createCreateNewAccountInstruction, createInitRentVaultInstruction } from '../ts';
import {
createCreateNewAccountInstruction,
createInitRentVaultInstruction,
createNewAccountEncoder,
MyInstruction,
} from '../ts';

describe('PDA Rent-Payer', () => {
const svm = new LiteSVM();
let programId: Address;
let payer: KeyPairSigner;
let rentVaultPda: Address;
let rentExemptBalance: bigint;

before(async () => {
programId = (await generateKeyPairSigner()).address;
svm.addProgramFromFile(programId, 'tests/fixtures/pda_rent_payer_program.so');
payer = await generateKeyPairSigner();
svm.airdrop(payer.address, lamports(2_000_000_000n));
});

async function deriveRentVaultPda() {
const [pda] = await getProgramDerivedAddress({
[rentVaultPda] = await getProgramDerivedAddress({
programAddress: programId,
seeds: ['rent_vault'],
});
return pda;
}
rentExemptBalance = svm.getRent().minimumBalance(0n);
});

it('Initialize the Rent Vault', async () => {
const rentVaultPda = await deriveRentVaultPda();
const ix = createInitRentVaultInstruction(rentVaultPda, payer, programId, 1_000_000_000n);
function balance(address: Address): bigint {
const value = svm.getBalance(address);
assert(value !== null, `expected ${address} to exist`);
return value;
}

async function sendInstruction(ix: Instruction) {
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageFeePayerSigner(payer, m),
m => svm.setTransactionMessageLifetimeUsingLatestBlockhash(m),
m => appendTransactionMessageInstruction(ix, m),
);
const signedTx = await signTransactionMessageWithSigners(transactionMessage);
return svm.sendTransaction(signedTx);
}

const result = svm.sendTransaction(signedTx);
// Same layout as `createCreateNewAccountInstruction`, but with `new_account` passed as a plain writable account.
function createNewAccountWithoutSignature(newAccount: Address): Instruction {
return {
programAddress: programId,
accounts: [
{ address: newAccount, role: AccountRole.WRITABLE },
{ address: rentVaultPda, role: AccountRole.WRITABLE },
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
],
data: createNewAccountEncoder.encode({ instruction: MyInstruction.CreateNewAccount }),
};
}

it('Initialize the Rent Vault', async () => {
const ix = createInitRentVaultInstruction(rentVaultPda, payer, programId, 1_000_000_000n);

const result = await sendInstruction(ix);
assert(!(result instanceof FailedTransactionMetadata), `transaction failed: ${result.toString()}`);

assert.equal(balance(rentVaultPda), rentExemptBalance + 1_000_000_000n);
const vault = svm.getAccount(rentVaultPda);
assert(vault.exists, 'rent vault was not created');
assert.equal(vault.programAddress, SYSTEM_PROGRAM_ADDRESS);
});

it('Create a new account using the Rent Vault', async () => {
const newAccount = await generateKeyPairSigner();
const rentVaultPda = await deriveRentVaultPda();
const vaultBalanceBefore = balance(rentVaultPda);
const ix = createCreateNewAccountInstruction(newAccount, rentVaultPda, programId);

const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageFeePayerSigner(payer, m),
m => svm.setTransactionMessageLifetimeUsingLatestBlockhash(m),
m => appendTransactionMessageInstruction(ix, m),
);
const signedTx = await signTransactionMessageWithSigners(transactionMessage);

const result = svm.sendTransaction(signedTx);
const result = await sendInstruction(ix);
assert(!(result instanceof FailedTransactionMetadata), `transaction failed: ${result.toString()}`);

const created = svm.getAccount(newAccount.address);
assert(created.exists, 'new account was not created');
assert.equal(created.lamports, rentExemptBalance);
assert.equal(created.programAddress, SYSTEM_PROGRAM_ADDRESS);
assert.equal(created.data.length, 0);
assert.equal(balance(rentVaultPda), vaultBalanceBefore - rentExemptBalance);
});

it('Rejects paying vault lamports into an existing wallet that does not sign', async () => {
const wallet = await generateKeyPairSigner();
svm.airdrop(wallet.address, lamports(500_000_000n));
const walletBalanceBefore = balance(wallet.address);
const vaultBalanceBefore = balance(rentVaultPda);

const result = await sendInstruction(createNewAccountWithoutSignature(wallet.address));
assert(result instanceof FailedTransactionMetadata, 'unsigned new_account must be rejected');

assert.equal(balance(wallet.address), walletBalanceBefore);
assert.equal(balance(rentVaultPda), vaultBalanceBefore);
});

it('Rejects paying vault lamports into an existing wallet even when it signs', async () => {
const wallet = await generateKeyPairSigner();
svm.airdrop(wallet.address, lamports(500_000_000n));
const walletBalanceBefore = balance(wallet.address);
const vaultBalanceBefore = balance(rentVaultPda);

const result = await sendInstruction(createCreateNewAccountInstruction(wallet, rentVaultPda, programId));
assert(result instanceof FailedTransactionMetadata, 'an already existing new_account must be rejected');

assert.equal(balance(wallet.address), walletBalanceBefore);
assert.equal(balance(rentVaultPda), vaultBalanceBefore);
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use pinocchio::{
cpi::{Seed, Signer},
error::ProgramError,
sysvars::{rent::Rent, Sysvar},
AccountView, Address, ProgramResult,
};
use pinocchio_system::instructions::CreateAccount;

use crate::state::RentVault;

Expand All @@ -15,19 +17,38 @@ pub fn create_new_account(
return Err(ProgramError::NotEnoughAccountKeys);
};

let bump = instruction_data[0];
if !new_account.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}

let rent_vault_pda = Address::create_program_address(&[RentVault::SEED_PREFIX.as_bytes(), &[bump]], program_id)
.map_err(|_| ProgramError::InvalidSeeds)?;
let bump = *instruction_data.first().ok_or(ProgramError::InvalidInstructionData)?;

assert!(rent_vault.address().eq(&rent_vault_pda));
// Only the canonical bump is accepted, so a client cannot point at an alternate vault PDA.
let (rent_vault_pda, canonical_bump) =
Address::find_program_address(&[RentVault::SEED_PREFIX.as_bytes()], program_id);
if bump != canonical_bump || rent_vault.address() != &rent_vault_pda {
return Err(ProgramError::InvalidSeeds);
}

// Assuming this account has no inner data (size 0)
//
let lamports_required_for_rent = (Rent::get()?).try_minimum_balance(0)?;

rent_vault.set_lamports(rent_vault.lamports() - lamports_required_for_rent);
new_account.set_lamports(new_account.lamports() + lamports_required_for_rent);
let bump_bytes = bump.to_le_bytes();

let seeds = [Seed::from(RentVault::SEED_PREFIX.as_bytes()), Seed::from(&bump_bytes)];

let signer_seed = Signer::from(&seeds);

// Create the new account, transferring lamports from the rent vault to the new account
CreateAccount {
from: rent_vault,
to: new_account,
lamports: lamports_required_for_rent,
space: 0,
owner: &pinocchio_system::ID,
}
.invoke_signed(&[signer_seed])?;

Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub fn init_rent_vault(program_id: &Address, accounts: &mut [AccountView], instr

let signer_seed = Signer::from(&seeds);

CreateAccount { from: payer, to: rent_vault, lamports: lamports_required, space: 0, owner: program_id }
CreateAccount { from: payer, to: rent_vault, lamports: lamports_required, space: 0, owner: &pinocchio_system::ID }
.invoke_signed(&[signer_seed])?;

Ok(())
Expand Down
Loading