use {
crate::{
cpi::request::CpiRequest,
errors::{RuntimeError, RuntimeResult},
},
solana_account::Account,
solana_address::Address,
solana_instruction::AccountMeta,
};
pub fn check_privileges(
request: &CpiRequest,
caller_account_metas: &[AccountMeta],
) -> RuntimeResult<()> {
for cpi_meta in &request.accounts {
let caller_meta = caller_account_metas
.iter()
.find(|m| m.pubkey == cpi_meta.pubkey);
match caller_meta {
Some(cm) => {
if cpi_meta.is_signer
&& !cm.is_signer
&& !request.signers.contains(&cpi_meta.pubkey)
{
return Err(RuntimeError::PrivilegeEscalation(
"signer".to_string(),
cpi_meta.pubkey.to_string(),
));
}
if cpi_meta.is_writable && !cm.is_writable {
return Err(RuntimeError::PrivilegeEscalation(
"writable".to_string(),
cpi_meta.pubkey.to_string(),
));
}
}
None => {
return Err(RuntimeError::MissingAccount(cpi_meta.pubkey.to_string()));
}
}
}
Ok(())
}
pub fn check_account_change(
callee_program_id: &Address,
pubkey: &Address,
account: &Account,
new_owner: &Address,
new_lamports: u64,
new_data: &[u8],
) -> RuntimeResult<()> {
let is_owner = account.owner == *callee_program_id;
if *new_owner != account.owner && !is_owner {
return Err(RuntimeError::PrivilegeEscalation(
"non-owner changed owner".to_string(),
pubkey.to_string(),
));
}
if new_lamports < account.lamports && !is_owner {
return Err(RuntimeError::ExternalAccountLamportSpend(
pubkey.to_string(),
));
}
if new_data != account.data.as_slice() && !is_owner {
return Err(RuntimeError::PrivilegeEscalation(
"non-owner modified data".to_string(),
pubkey.to_string(),
));
}
if account.executable && new_data != account.data.as_slice() {
return Err(RuntimeError::PrivilegeEscalation(
"executable account modified".to_string(),
pubkey.to_string(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::cpi::request::{CpiAccountMeta, CpiRequest},
};
const PROGRAM_A: Address = Address::new_from_array([1u8; 32]);
const PROGRAM_B: Address = Address::new_from_array([2u8; 32]);
const ACCT_1: Address = Address::new_from_array([10u8; 32]);
const ACCT_2: Address = Address::new_from_array([11u8; 32]);
const PDA: Address = Address::new_from_array([20u8; 32]);
fn make_request(accounts: Vec<CpiAccountMeta>, signers: Vec<Address>) -> CpiRequest {
CpiRequest {
program_id: PROGRAM_B,
accounts,
data: vec![],
caller_accounts: vec![],
signers,
}
}
fn make_account(owner: Address, lamports: u64, data: &[u8]) -> Account {
Account {
lamports,
data: data.to_vec(),
owner,
executable: false,
rent_epoch: 0,
}
}
#[test]
fn check_privileges_ok() {
let request = make_request(
vec![CpiAccountMeta {
pubkey: ACCT_1,
is_signer: true,
is_writable: true,
}],
vec![],
);
let caller_metas = vec![AccountMeta {
pubkey: ACCT_1,
is_signer: true,
is_writable: true,
}];
assert!(check_privileges(&request, &caller_metas).is_ok());
}
#[test]
fn check_privileges_signer_escalation() {
let request = make_request(
vec![CpiAccountMeta {
pubkey: ACCT_1,
is_signer: true,
is_writable: false,
}],
vec![],
);
let caller_metas = vec![AccountMeta {
pubkey: ACCT_1,
is_signer: false,
is_writable: false,
}];
assert!(check_privileges(&request, &caller_metas).is_err());
}
#[test]
fn check_privileges_signer_via_pda() {
let request = make_request(
vec![CpiAccountMeta {
pubkey: PDA,
is_signer: true,
is_writable: false,
}],
vec![PDA],
);
let caller_metas = vec![AccountMeta {
pubkey: PDA,
is_signer: false,
is_writable: false,
}];
assert!(check_privileges(&request, &caller_metas).is_ok());
}
#[test]
fn check_privileges_writable_escalation() {
let request = make_request(
vec![CpiAccountMeta {
pubkey: ACCT_1,
is_signer: false,
is_writable: true,
}],
vec![],
);
let caller_metas = vec![AccountMeta {
pubkey: ACCT_1,
is_signer: false,
is_writable: false,
}];
assert!(check_privileges(&request, &caller_metas).is_err());
}
#[test]
fn check_privileges_missing_account() {
let request = make_request(
vec![CpiAccountMeta {
pubkey: ACCT_2,
is_signer: false,
is_writable: false,
}],
vec![],
);
let caller_metas = vec![AccountMeta {
pubkey: ACCT_1,
is_signer: false,
is_writable: false,
}];
assert!(check_privileges(&request, &caller_metas).is_err());
}
#[test]
fn check_account_change_owner_modifies_data() {
let acct = make_account(PROGRAM_A, 100, b"hello");
assert!(
check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 100, b"world").is_ok()
);
}
#[test]
fn check_account_change_non_owner_modifies_data() {
let acct = make_account(PROGRAM_A, 100, b"hello");
assert!(
check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 100, b"world").is_err()
);
}
#[test]
fn check_account_change_owner_changes_owner() {
let acct = make_account(PROGRAM_A, 100, b"");
assert!(check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_B, 100, b"").is_ok());
}
#[test]
fn check_account_change_non_owner_changes_owner() {
let acct = make_account(PROGRAM_A, 100, b"");
assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_B, 100, b"").is_err());
}
#[test]
fn check_account_change_owner_debits_lamports() {
let acct = make_account(PROGRAM_A, 100, b"");
assert!(check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 50, b"").is_ok());
}
#[test]
fn check_account_change_non_owner_debits_lamports() {
let acct = make_account(PROGRAM_A, 100, b"");
assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 50, b"").is_err());
}
#[test]
fn check_account_change_non_owner_credits_lamports() {
let acct = make_account(PROGRAM_A, 100, b"");
assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 200, b"").is_ok());
}
#[test]
fn check_account_change_executable_data_rejected() {
let mut acct = make_account(PROGRAM_A, 100, b"code");
acct.executable = true;
assert!(
check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 100, b"hack").is_err()
);
}
#[test]
fn check_account_change_no_modifications() {
let acct = make_account(PROGRAM_A, 100, b"data");
assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 100, b"data").is_ok());
}
}