use solana_address::Address;
use solana_rent::Rent;
use solana_transaction_context::IndexOfAccount;
use solana_transaction_error::{TransactionError, TransactionResult};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum RentState {
Uninitialized,
RentPaying {
lamports: u64, data_size: usize, },
RentExempt,
}
pub(crate) fn check_rent_state_with_account(
pre_rent_state: &RentState,
post_rent_state: &RentState,
address: &Address,
account_index: IndexOfAccount,
) -> TransactionResult<()> {
if !solana_sdk_ids::incinerator::check_id(address) &&
!transition_allowed(pre_rent_state, post_rent_state)
{
let account_index = account_index as u8;
Err(TransactionError::InsufficientFundsForRent { account_index })
} else {
Ok(())
}
}
pub(crate) fn get_account_rent_state(
rent: &Rent,
account_lamports: u64,
account_size: usize,
) -> RentState {
if account_lamports == 0 {
RentState::Uninitialized
} else if rent.is_exempt(account_lamports, account_size) {
RentState::RentExempt
} else {
RentState::RentPaying { data_size: account_size, lamports: account_lamports }
}
}
pub(crate) fn transition_allowed(pre_rent_state: &RentState, post_rent_state: &RentState) -> bool {
match post_rent_state {
RentState::Uninitialized | RentState::RentExempt => true,
RentState::RentPaying { data_size: post_data_size, lamports: post_lamports } => {
match pre_rent_state {
RentState::Uninitialized | RentState::RentExempt => false,
RentState::RentPaying { data_size: pre_data_size, lamports: pre_lamports } => {
post_data_size == pre_data_size && post_lamports <= pre_lamports
}
}
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
#[test]
fn transition_uninitialized_to_rent_exempt() {
let pre = RentState::Uninitialized;
let post = RentState::RentExempt;
assert!(transition_allowed(&pre, &post));
}
#[test]
fn transition_rent_paying_debit() {
let pre = RentState::RentPaying { lamports: 1000, data_size: 100 };
let post = RentState::RentPaying { lamports: 900, data_size: 100 };
assert!(transition_allowed(&pre, &post));
}
#[test]
fn transition_rent_paying_credit() {
let pre = RentState::RentPaying { lamports: 1000, data_size: 100 };
let post = RentState::RentPaying { lamports: 1100, data_size: 100 };
assert!(!transition_allowed(&pre, &post));
}
#[test]
fn transition_rent_paying_resize() {
let pre = RentState::RentPaying { lamports: 1000, data_size: 100 };
let post = RentState::RentPaying { lamports: 1000, data_size: 200 };
assert!(!transition_allowed(&pre, &post));
}
#[test]
fn transition_rent_exempt_to_uninitialized() {
let pre = RentState::RentExempt;
let post = RentState::Uninitialized;
assert!(transition_allowed(&pre, &post));
}
#[test]
fn transition_any_to_rent_exempt() {
let cases = vec![
(RentState::Uninitialized, RentState::RentExempt),
(RentState::RentExempt, RentState::RentExempt),
(RentState::RentPaying { lamports: 500, data_size: 50 }, RentState::RentExempt),
];
for (pre, post) in cases {
assert!(transition_allowed(&pre, &post), "failed for {pre:?} -> {post:?}");
}
}
#[test]
fn check_rent_state_incinerator_bypass() {
let incinerator = solana_sdk_ids::incinerator::id();
let cases = vec![
(RentState::Uninitialized, RentState::RentExempt),
(RentState::Uninitialized, RentState::Uninitialized),
(RentState::Uninitialized, RentState::RentPaying { lamports: 100, data_size: 50 }),
(RentState::RentExempt, RentState::Uninitialized),
(RentState::RentExempt, RentState::RentExempt),
(RentState::RentExempt, RentState::RentPaying { lamports: 100, data_size: 50 }),
(RentState::RentPaying { lamports: 1000, data_size: 100 }, RentState::Uninitialized),
(RentState::RentPaying { lamports: 1000, data_size: 100 }, RentState::RentExempt),
(
RentState::RentPaying { lamports: 1000, data_size: 100 },
RentState::RentPaying { lamports: 2000, data_size: 100 },
),
];
for (pre, post) in cases {
let result = check_rent_state_with_account(&pre, &post, &incinerator, 0);
assert!(result.is_ok(), "incinerator should bypass rent check for {pre:?} -> {post:?}");
}
}
#[test]
fn check_rent_state_invalid_transition() {
let address = Address::new_unique();
let pre = RentState::RentPaying { lamports: 1000, data_size: 100 };
let post = RentState::RentPaying { lamports: 1100, data_size: 100 };
let result = check_rent_state_with_account(&pre, &post, &address, 7);
match result {
Err(TransactionError::InsufficientFundsForRent { account_index }) => {
assert_eq!(account_index, 7);
}
other => panic!("expected InsufficientFundsForRent, got {other:?}"),
}
}
#[test]
fn get_rent_state_uninitialized() {
let rent = Rent::default();
let state = get_account_rent_state(&rent, 0, 100);
assert_eq!(state, RentState::Uninitialized);
}
#[test]
fn get_rent_state_rent_exempt() {
let rent = Rent::default();
let data_size: usize = 100;
let exempt_lamports = rent.minimum_balance(data_size);
let state = get_account_rent_state(&rent, exempt_lamports, data_size);
assert_eq!(state, RentState::RentExempt);
}
#[test]
fn get_rent_state_rent_paying() {
let rent = Rent::default();
let data_size: usize = 100;
let state = get_account_rent_state(&rent, 1, data_size);
assert_eq!(state, RentState::RentPaying { data_size, lamports: 1 });
}
#[test]
fn transition_into_rent_paying_is_rejected_from_every_non_paying_pre_state() {
let post = RentState::RentPaying { lamports: 1, data_size: 10 };
for pre in [
RentState::Uninitialized,
RentState::RentExempt,
RentState::RentPaying { lamports: 1, data_size: 10 },
] {
let expected = matches!(pre, RentState::RentPaying { .. });
assert_eq!(transition_allowed(&pre, &post), expected, "pre {pre:?} -> post {post:?}");
}
}
#[test]
fn allowed_transitions_are_accepted_for_a_regular_account() {
let address = Address::new_unique();
let cases = vec![
(RentState::Uninitialized, RentState::RentExempt),
(RentState::RentExempt, RentState::Uninitialized),
(RentState::RentPaying { lamports: 1000, data_size: 100 }, RentState::Uninitialized),
(RentState::RentPaying { lamports: 1000, data_size: 100 }, RentState::RentExempt),
(
RentState::RentPaying { lamports: 1000, data_size: 100 },
RentState::RentPaying { lamports: 999, data_size: 100 },
),
];
for (pre, post) in cases {
assert!(transition_allowed(&pre, &post), "expected {pre:?} -> {post:?} to be allowed");
assert_eq!(
check_rent_state_with_account(&pre, &post, &address, 3),
Ok(()),
"check_rent_state_with_account {pre:?} -> {post:?}"
);
}
}
#[test]
fn rejected_transitions_report_the_caller_supplied_account_index() {
let address = Address::new_unique();
let pre = RentState::Uninitialized;
let post = RentState::RentPaying { lamports: 5, data_size: 5 };
for index in 0..=u8::MAX {
match check_rent_state_with_account(&pre, &post, &address, index as IndexOfAccount) {
Err(TransactionError::InsufficientFundsForRent { account_index }) => {
assert_eq!(account_index, index);
}
other => panic!("index {index}: expected InsufficientFundsForRent, got {other:?}"),
}
}
}
#[test]
fn incinerator_bypasses_rejected_transitions() {
let incinerator = solana_sdk_ids::incinerator::id();
assert!(!solana_sdk_ids::incinerator::check_id(&Address::new_unique()));
for pre in [RentState::Uninitialized, RentState::RentExempt] {
let post = RentState::RentPaying { lamports: 7, data_size: 7 };
assert!(!transition_allowed(&pre, &post));
assert_eq!(check_rent_state_with_account(&pre, &post, &incinerator, 0), Ok(()));
}
}
#[test]
fn get_account_rent_state_always_echoes_its_inputs() {
let rent = Rent::default();
for lamports in [0, 1, 2, 1000, 1_000_000, u64::MAX] {
for data_size in [0, 1, 165, 10_000, 1_000_000] {
let state = get_account_rent_state(&rent, lamports, data_size);
if lamports == 0 {
assert_eq!(
state,
RentState::Uninitialized,
"lamports {lamports}, size {data_size}"
);
} else if rent.is_exempt(lamports, data_size) {
assert_eq!(
state,
RentState::RentExempt,
"lamports {lamports}, size {data_size}"
);
} else {
assert_eq!(
state,
RentState::RentPaying { data_size, lamports },
"lamports {lamports}, size {data_size}"
);
}
}
}
}
proptest! {
#[test]
fn allowed_transitions_never_increase_lamports_or_change_size(
pre_lamports in 1u64..1_000_000,
pre_size in 0usize..4096,
delta in -1000i64..1000,
size_delta in -512i64..512,
) {
let pre = RentState::RentPaying { lamports: pre_lamports, data_size: pre_size };
let post_lamports = pre_lamports.wrapping_add(delta as u64);
let post_size = (pre_size as i64 + size_delta).max(0) as usize;
let post = RentState::RentPaying { lamports: post_lamports, data_size: post_size };
if transition_allowed(&pre, &post) {
prop_assert_eq!(post_size, pre_size);
prop_assert!(post_lamports <= pre_lamports);
}
}
}
proptest! {
#[test]
fn closing_or_exempting_is_always_allowed(
pre_lamports in 0u64..1_000_000,
pre_size in 0usize..4096,
) {
let pre = if pre_lamports == 0 {
RentState::Uninitialized
} else {
RentState::RentPaying { lamports: pre_lamports, data_size: pre_size }
};
prop_assert!(transition_allowed(&pre, &RentState::Uninitialized));
prop_assert!(transition_allowed(&pre, &RentState::RentExempt));
}
}
proptest! {
#[test]
fn check_matches_the_pure_predicate_for_regular_accounts(
pre_lamports in 0u64..1_000_000,
pre_size in 0usize..4096,
post_lamports in 0u64..1_000_000,
post_size in 0usize..4096,
) {
let pre = if pre_lamports == 0 {
RentState::Uninitialized
} else {
RentState::RentPaying { lamports: pre_lamports, data_size: pre_size }
};
let post = if post_lamports == 0 {
RentState::Uninitialized
} else {
RentState::RentPaying { lamports: post_lamports, data_size: post_size }
};
let checked = check_rent_state_with_account(&pre, &post, &Address::new_unique(), 0);
prop_assert_eq!(checked.is_ok(), transition_allowed(&pre, &post));
}
}
}