use crate::action::{Action, Request, SolanaInstruction, u256_is_unlimited};
use crate::audit::{DenyReason, HardDeny};
use crate::policy::Policy;
pub const SEL_APPROVE: u32 = 0x095e_a7b3;
pub const SEL_SET_APPROVAL_FOR_ALL: u32 = 0xa22c_b465;
pub const SEL_INCREASE_ALLOWANCE: u32 = 0x3950_9351;
pub const SEL_PERMIT2_APPROVE: u32 = 0x8751_7c45;
pub const SEL_PERMIT_2612: u32 = 0xd505_accf;
pub const SEL_PERMIT_DAI: u32 = 0x8fcb_af0c;
pub const SEL_PERMIT2_SINGLE: u32 = 0x2b67_b570;
pub const SEL_PERMIT2_BATCH: u32 = 0x2a2d_80d1;
pub const SEL_TRANSFER: u32 = 0xa905_9cbb;
pub const SEL_TRANSFER_FROM: u32 = 0x23b8_72dd;
pub const SYSTEM_PROGRAM: &str = concat!(
"1111", "1111", "1111", "1111", "1111", "1111", "1111", "1111"
);
pub const SPL_TOKEN_PROGRAM: &str = concat!(
"Tokenke", "gQfeZyi", "NwAJbNb", "GKPFXCW", "uBvf9Ss", "623VQ5DA"
);
pub const SPL_TOKEN_2022_PROGRAM: &str = concat!(
"TokenzQ", "dBNbLqP", "5VEhdkA", "S6EPFLC", "1PHnBqC", "XEpPxuEb"
);
pub const SYS_ASSIGN: u32 = 1;
pub const SYS_TRANSFER: u32 = 2;
pub const SYS_ADVANCE_NONCE: u32 = 4;
pub const SYS_ASSIGN_WITH_SEED: u32 = 10;
pub const TOK_TRANSFER: u8 = 3;
pub const TOK_APPROVE: u8 = 4;
pub const TOK_REVOKE: u8 = 5;
pub const TOK_SET_AUTHORITY: u8 = 6;
pub const TOK_TRANSFER_CHECKED: u8 = 12;
pub const TOK_APPROVE_CHECKED: u8 = 13;
#[must_use]
pub fn system_discriminant(ins: &crate::action::SolanaInstruction) -> u32 {
ins.data
.first_chunk::<4>()
.map_or(0, |b| u32::from_le_bytes(*b))
}
#[must_use]
pub fn tok_discriminant(ins: &crate::action::SolanaInstruction) -> u8 {
ins.data.first().copied().unwrap_or(0)
}
#[must_use]
fn tok_max_approve(ins: &SolanaInstruction) -> bool {
ins.data
.get(1..9)
.and_then(|b| <[u8; 8]>::try_from(b).ok())
.map(u64::from_le_bytes)
== Some(u64::MAX)
}
#[must_use]
pub fn word(data: &[u8], at: usize) -> Option<&[u8]> {
data.get(at..at + 32)
}
#[must_use]
pub fn word_addr(data: &[u8], at: usize) -> Option<String> {
let w = word(data, at)?;
Some(format!("0x{}", hex::encode(&w[12..])))
}
#[must_use]
fn word_unlimited(data: &[u8], at: usize) -> bool {
word(data, at).is_some_and(|w| w[..16].iter().any(|&b| b != 0))
}
const PERMIT_PRIMARY_TYPES: [&str; 5] = [
"Permit",
"PermitSingle",
"PermitBatch",
"PermitTransferFrom",
"PermitBatchTransferFrom",
];
pub fn hard_denies(req: &Request, policy: &Policy) -> Vec<DenyReason> {
match &req.action {
Action::EvmTx {
to,
data,
authorizations,
..
} => {
let mut out = Vec::new();
for a in authorizations {
if a.chain_id == 0 {
out.push(DenyReason::HardDeny(HardDeny::Eip7702ZeroChain));
}
}
let _ = to;
if let Some(sel) = data.first_chunk::<4>().map(|b| u32::from_be_bytes(*b)) {
out.extend(evmdeny(req, policy, sel, data));
}
out
}
Action::EvmTypedData {
primary_type,
spender,
amount,
..
} => {
if !PERMIT_PRIMARY_TYPES.contains(&primary_type.as_str()) {
return Vec::new();
}
match spender {
None => vec![unknown_spender("<unset>".to_owned())],
Some(s) if !policy.spender_allowed(&req.subject, s) => {
vec![unknown_spender(s.clone())]
}
Some(_) if amount.is_none_or(|a| u256_is_unlimited(&a)) => {
vec![DenyReason::HardDeny(HardDeny::PermitUnlimited)]
}
Some(_) => Vec::new(),
}
}
Action::SolanaTx { instructions, .. } => {
let mut out = Vec::new();
for ins in instructions {
match ins.program_id.as_str() {
SYSTEM_PROGRAM => match system_discriminant(ins) {
d if d == SYS_ASSIGN || d == SYS_ASSIGN_WITH_SEED => {
out.push(DenyReason::HardDeny(HardDeny::OwnerReassignment));
}
d if d == SYS_ADVANCE_NONCE => {
out.push(DenyReason::HardDeny(HardDeny::NonceAdvance));
}
_ => {}
},
p if p == SPL_TOKEN_PROGRAM || p == SPL_TOKEN_2022_PROGRAM => {
match tok_discriminant(ins) {
d if d == TOK_SET_AUTHORITY => {
out.push(DenyReason::HardDeny(HardDeny::OwnerReassignment));
}
d if (d == TOK_APPROVE || d == TOK_APPROVE_CHECKED)
&& tok_max_approve(ins) =>
{
out.push(DenyReason::HardDeny(HardDeny::UnlimitedApproval));
}
_ => {}
}
}
_ => {}
}
}
out
}
}
}
fn unknown_spender(addr: String) -> DenyReason {
DenyReason::HardDeny(HardDeny::PermitUnknownSpender(addr))
}
fn evmdeny(req: &Request, policy: &Policy, sel: u32, data: &[u8]) -> Vec<DenyReason> {
match sel {
SEL_SET_APPROVAL_FOR_ALL => vec![DenyReason::HardDeny(HardDeny::ApprovalForAll)],
SEL_APPROVE | SEL_INCREASE_ALLOWANCE => {
unlimited(data, 36)
}
SEL_PERMIT2_APPROVE => {
unlimited(data, 68)
}
SEL_PERMIT_2612 | SEL_PERMIT_DAI => {
permit(req, policy, data, 36, 68)
}
SEL_PERMIT2_SINGLE => {
permit(req, policy, data, 164, 68)
}
SEL_PERMIT2_BATCH => {
match word_addr(data, 68) {
Some(spender) if !policy.spender_allowed(&req.subject, &spender) => {
vec![unknown_spender(spender)]
}
_ => vec![DenyReason::HardDeny(HardDeny::PermitUnlimited)],
}
}
_ => Vec::new(),
}
}
fn unlimited(data: &[u8], amount_at: usize) -> Vec<DenyReason> {
if word_unlimited(data, amount_at) {
vec![DenyReason::HardDeny(HardDeny::UnlimitedApproval)]
} else {
Vec::new()
}
}
fn permit(
req: &Request,
policy: &Policy,
data: &[u8],
spender_at: usize,
amount_at: usize,
) -> Vec<DenyReason> {
match word_addr(data, spender_at) {
Some(spender) if !policy.spender_allowed(&req.subject, &spender) => {
vec![unknown_spender(spender)]
}
Some(_) if word(data, amount_at).is_none() || word_unlimited(data, amount_at) => {
vec![DenyReason::HardDeny(HardDeny::PermitUnlimited)]
}
Some(_) => Vec::new(),
None => vec![unknown_spender("<truncated calldata>".to_owned())],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::action::{u256_from_u128, u256_to_u128};
use sha3::{Digest, Keccak256};
#[test]
fn every_selector_constant_is_its_signature() {
for (constant, signature) in [
(SEL_APPROVE, "approve(address,uint256)"),
(SEL_SET_APPROVAL_FOR_ALL, "setApprovalForAll(address,bool)"),
(SEL_INCREASE_ALLOWANCE, "increaseAllowance(address,uint256)"),
(
SEL_PERMIT2_APPROVE,
"approve(address,address,uint160,uint48)",
),
(
SEL_PERMIT_2612,
"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)",
),
(
SEL_PERMIT_DAI,
"permit(address,address,uint256,uint256,bool,uint8,bytes32,bytes32)",
),
(
SEL_PERMIT2_SINGLE,
"permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)",
),
(
SEL_PERMIT2_BATCH,
"permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)",
),
(SEL_TRANSFER, "transfer(address,uint256)"),
(SEL_TRANSFER_FROM, "transferFrom(address,address,uint256)"),
] {
let d = Keccak256::digest(signature.as_bytes());
let selector = u32::from_be_bytes(d[..4].try_into().expect("4 bytes"));
assert_eq!(constant, selector, "{signature}");
}
}
#[test]
fn solana_program_id_constants_keep_their_well_known_values() {
assert_eq!(SYSTEM_PROGRAM, "1".repeat(32));
assert_eq!(
SPL_TOKEN_PROGRAM,
concat!(
"TokenkegQ",
"feZyiNwAJ",
"bNbGKPFXC",
"WuBvf9Ss6",
"23VQ5DA"
)
);
assert_eq!(
SPL_TOKEN_2022_PROGRAM,
concat!(
"TokenzQdB",
"NbLqP5VEh",
"dkAS6EPFL",
"C1PHnBqCX",
"EpPxuEb"
)
);
}
#[test]
fn u256_helpers_agree_on_the_unlimited_line() {
assert!(!u256_is_unlimited(&u256_from_u128(u128::MAX)));
let mut low_half_only = [0_u8; 32];
low_half_only[16] = 1; assert!(!u256_is_unlimited(&low_half_only));
let mut across = [0_u8; 32];
across[16] = 1;
across[0] = 1; assert!(u256_is_unlimited(&across));
assert_eq!(u256_to_u128(&u256_from_u128(12_345)), Some(12_345));
assert_eq!(u256_to_u128(&across), None);
}
}