use crate::decode::DecodedAccount;
use crate::error::Result;
use crate::replay::Mutation;
use crate::scope::Scope;
use crate::search::search_threshold;
use crate::Replay;
use serde::Serialize;
const SYSTEM: &str = "11111111111111111111111111111111";
const TOKEN: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
#[derive(Debug, Clone, Serialize)]
pub struct BreakingPoint {
pub account: String,
pub field: String,
pub condition: String,
pub current: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct ScanOptions {
pub max_accounts: usize,
pub max_fields_per_account: usize,
}
impl Default for ScanOptions {
fn default() -> Self {
ScanOptions {
max_accounts: 32,
max_fields_per_account: 24,
}
}
}
fn is_infra(address: &str) -> bool {
address.starts_with("Sysvar")
|| matches!(
address,
SYSTEM
| TOKEN
| "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
| "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
| "ComputeBudget111111111111111111111111111111"
| "NativeLoader1111111111111111111111111111111"
| "BPFLoader2111111111111111111111111111111111"
| "BPFLoaderUpgradeab1e11111111111111111111111"
)
}
pub fn scan_breaking_points(
scope: &Scope,
signature: &str,
accounts: &[String],
opts: ScanOptions,
) -> Result<Vec<BreakingPoint>> {
let replay = scope.replay(signature)?;
let baseline = replay.run()?.result.success;
let mut out: Vec<BreakingPoint> = Vec::new();
for account in accounts.iter().take(opts.max_accounts) {
if is_infra(account) {
continue;
}
let Ok(info) = scope.decode_account(account, None) else {
continue;
};
if info.executable {
continue;
}
if info.lamports > 0 {
let acct = account.clone();
if let Some(th) = search_threshold(0, info.lamports.saturating_mul(2), |v| {
sim(&replay, &[Mutation::lamports(acct.clone(), v)])
})? {
if th.flips_at <= 1 {
out.push(bp(account, "existence", "is closed", info.lamports));
} else {
out.push(bp(
account,
"SOL balance",
threshold_phrase(th.flips_at, !th.low_success),
info.lamports,
));
}
}
}
let other_owner = if info.owner == SYSTEM { TOKEN } else { SYSTEM };
if flips(
&replay,
baseline,
&[Mutation::owner(account.clone(), other_owner)],
)? {
out.push(bp(account, "owner program", "changes", 0));
}
let Some(decoded) = info.decoded else {
continue;
};
let mut tried = 0usize;
for f in &decoded.fields {
if tried >= opts.max_fields_per_account {
break;
}
if !f.editable {
continue; }
let probed = match f.ty.as_str() {
"u64" => probe_u64(&replay, account, f, &mut out)?,
"u8" => probe_u8(&replay, baseline, account, f, &mut out)?,
"pubkey" => probe_pubkey(&replay, baseline, account, f, &decoded, &mut out)?,
_ => false,
};
if probed {
tried += 1;
}
}
}
Ok(out)
}
fn probe_u64(
replay: &Replay,
account: &str,
f: &crate::decode::Field,
out: &mut Vec<BreakingPoint>,
) -> Result<bool> {
let Ok(current) = f.value.parse::<u64>() else {
return Ok(false);
};
if current == 0 {
return Ok(false);
}
let (acct, off) = (account.to_string(), f.offset);
if let Some(th) = search_threshold(0, current.saturating_mul(2), |v| {
sim(
replay,
&[Mutation::patch(acct.clone(), off, v.to_le_bytes().to_vec())],
)
})? {
out.push(bp(
account,
f.name.clone(),
threshold_phrase(th.flips_at, !th.low_success),
current,
));
}
Ok(true)
}
fn probe_u8(
replay: &Replay,
baseline: bool,
account: &str,
f: &crate::decode::Field,
out: &mut Vec<BreakingPoint>,
) -> Result<bool> {
let current = f.value.parse::<u8>().ok();
for cand in [2u8, 0, 255] {
if Some(cand) == current {
continue;
}
if flips(
replay,
baseline,
&[Mutation::patch(account.to_string(), f.offset, vec![cand])],
)? {
let cond = if f.name == "state" && cand == 2 {
"is frozen".to_string()
} else {
format!("changes (e.g. set to {cand})")
};
out.push(bp(
account,
f.name.clone(),
cond,
current.unwrap_or(0) as u64,
));
break;
}
}
Ok(true)
}
fn probe_pubkey(
replay: &Replay,
baseline: bool,
account: &str,
f: &crate::decode::Field,
_d: &DecodedAccount,
out: &mut Vec<BreakingPoint>,
) -> Result<bool> {
let sentinel = vec![0x11u8; f.size.max(32)];
if flips(
replay,
baseline,
&[Mutation::patch(account.to_string(), f.offset, sentinel)],
)? {
let label = if f.name == "owner" {
"authority"
} else {
f.name.as_str()
};
out.push(bp(account, label, "changes", 0));
}
Ok(true)
}
fn bp(
account: &str,
field: impl Into<String>,
condition: impl Into<String>,
current: u64,
) -> BreakingPoint {
BreakingPoint {
account: account.to_string(),
field: field.into(),
condition: condition.into(),
current,
}
}
fn sim(replay: &Replay, muts: &[Mutation]) -> Result<bool> {
Ok(replay.simulate(muts)?.result.success)
}
fn flips(replay: &Replay, baseline: bool, muts: &[Mutation]) -> Result<bool> {
Ok(sim(replay, muts)? != baseline)
}
fn threshold_phrase(flips_at: u64, breaks_below: bool) -> String {
if breaks_below {
format!("drops below {flips_at}")
} else {
format!("reaches {flips_at} or above")
}
}