use crate::chain::{MetaplexMeta, MintInfo};
use crate::config;
use crate::types::{Flag, Severity};
use serde_json::{json, Value};
pub struct MintAnalysis {
pub flags: Vec<Flag>,
pub section: Value,
pub name: Option<String>,
pub symbol: Option<String>,
}
fn ext<'a>(mint: &'a MintInfo, name: &str) -> Option<&'a Value> {
mint.extensions
.iter()
.find(|e| e.get("extension").and_then(|x| x.as_str()) == Some(name))
}
pub fn analyze(mint: &MintInfo, meta: Option<&MetaplexMeta>) -> MintAnalysis {
let mut flags = vec![];
let is_2022 = mint.program == config::TOKEN_2022;
match &mint.mint_authority {
Some(a) => flags.push(Flag::new(
"mint_authority",
Severity::Danger,
"Mint authority still active",
format!(
"The wallet {} can create unlimited new tokens at any time, diluting every holder to zero. Safe tokens revoke this.",
short(a)
),
30,
)),
None => flags.push(Flag::new(
"mint_authority",
Severity::Good,
"Mint authority revoked",
"No one can ever create more of this token — the supply is fixed.".into(),
0,
)),
}
match &mint.freeze_authority {
Some(a) => flags.push(
Flag::new(
"freeze_authority",
Severity::Danger,
"Freeze authority active — sell-block risk",
format!(
"The wallet {} can FREEZE any holder's tokens, instantly blocking them from selling. This is the #1 Solana honeypot lever.",
short(a)
),
25,
)
.hard(),
),
None => flags.push(Flag::new(
"freeze_authority",
Severity::Good,
"Freeze authority revoked",
"No one can freeze your tokens — you can always sell.".into(),
0,
)),
}
let mut ext_summary = vec![];
if is_2022 {
if ext(mint, "nonTransferable").is_some() {
ext_summary.push("nonTransferable");
flags.push(
Flag::new(
"non_transferable",
Severity::Critical,
"🍯 Token is NON-TRANSFERABLE",
"The mint is flagged non-transferable — you can receive it but never send or sell it. This is a honeypot by construction.".into(),
60,
)
.hard(),
);
}
if let Some(e) = ext(mint, "defaultAccountState") {
let state = e
.pointer("/state/accountState")
.and_then(|s| s.as_str())
.unwrap_or("");
if state == "frozen" {
ext_summary.push("defaultAccountState=frozen");
flags.push(
Flag::new(
"default_frozen",
Severity::Critical,
"🍯 New accounts start FROZEN",
"Every buyer's token account is frozen by default — you cannot sell unless the team manually unfreezes you. Honeypot mechanics.".into(),
60,
)
.hard(),
);
}
}
if let Some(e) = ext(mint, "permanentDelegate") {
let d = e
.pointer("/state/delegate")
.and_then(|s| s.as_str())
.unwrap_or("?");
ext_summary.push("permanentDelegate");
flags.push(
Flag::new(
"permanent_delegate",
Severity::Critical,
"🍯 Permanent delegate can seize tokens",
format!(
"The address {} is a permanent delegate — it can transfer or burn tokens OUT of any holder's wallet without permission.",
short(d)
),
60,
)
.hard(),
);
}
if let Some(e) = ext(mint, "transferHook") {
let prog = e.pointer("/state/programId").and_then(|s| s.as_str());
if let Some(p) = prog.filter(|p| !p.is_empty()) {
ext_summary.push("transferHook");
flags.push(Flag::new(
"transfer_hook",
Severity::Danger,
"Transfer hook — programmable transfer control",
format!(
"Every transfer calls custom program {} which can reject sells for any reason (allowlists, trading hours, honeypot switches). Trust requires reading that program.",
short(p)
),
20,
));
}
}
if let Some(e) = ext(mint, "transferFeeConfig") {
let bps = e
.pointer("/state/newerTransferFee/transferFeeBasisPoints")
.or_else(|| e.pointer("/state/olderTransferFee/transferFeeBasisPoints"))
.and_then(|b| b.as_u64())
.unwrap_or(0);
let pct = bps as f64 / 100.0;
if bps > 0 {
ext_summary.push("transferFee");
}
if pct >= 25.0 {
flags.push(
Flag::new(
"transfer_fee",
Severity::Critical,
"🍯 Confiscatory transfer tax",
format!("A {pct}% fee is skimmed from EVERY transfer — selling costs you {pct}% off the top. Effectively a honeypot."),
40,
)
.hard(),
);
} else if pct >= 10.0 {
flags.push(Flag::new(
"transfer_fee",
Severity::Danger,
"High transfer tax",
format!("A {pct}% fee is taken from every transfer (buys AND sells)."),
20,
));
} else if bps > 0 {
flags.push(Flag::new(
"transfer_fee",
Severity::Warning,
"Transfer tax",
format!("A {pct}% fee is taken from every transfer."),
8,
));
}
if bps == 0 {
if let Some(a) = e
.pointer("/state/transferFeeConfigAuthority")
.and_then(|s| s.as_str())
.filter(|s| !s.is_empty())
{
flags.push(Flag::new(
"transfer_fee_authority",
Severity::Warning,
"Transfer fee can be switched on",
format!(
"The fee is 0% today, but {} still controls it and can raise the transfer tax — up to 100% — at any time. Safe tokens revoke the fee authority.",
short(a)
),
8,
));
}
}
}
if ext(mint, "mintCloseAuthority").is_some() {
ext_summary.push("mintCloseAuthority");
flags.push(Flag::new(
"mint_close_authority",
Severity::Warning,
"Mint can be closed",
"A close authority exists on the mint — an unusual power for a community token."
.into(),
5,
));
}
if let Some(e) = ext(mint, "pausableConfig") {
let paused = e
.pointer("/state/paused")
.and_then(|b| b.as_bool())
.unwrap_or(false);
let authority = e
.pointer("/state/authority")
.and_then(|s| s.as_str())
.filter(|s| !s.is_empty());
if paused {
ext_summary.push("paused");
flags.push(
Flag::new(
"paused",
Severity::Critical,
"🍯 All transfers are PAUSED",
"Every transfer of this token is currently paused — nobody can sell until the team unpauses. Honeypot mechanics.".into(),
60,
)
.hard(),
);
} else if let Some(a) = authority {
ext_summary.push("pausable");
flags.push(
Flag::new(
"pausable",
Severity::Danger,
"Transfers can be paused",
format!(
"The wallet {} can pause ALL transfers at any moment — a global freeze that blocks every holder from selling. Equivalent to a freeze authority.",
short(a)
),
25,
)
.hard(),
);
}
}
}
let (mut name, mut symbol, mut mutable, mut update_auth) = (None, None, None, None);
if let Some(m) = meta {
name = Some(m.name.clone()).filter(|s| !s.is_empty());
symbol = Some(m.symbol.clone()).filter(|s| !s.is_empty());
mutable = Some(m.is_mutable);
update_auth = Some(m.update_authority.clone());
} else if let Some(e) = ext(mint, "tokenMetadata") {
name = e
.pointer("/state/name")
.and_then(|s| s.as_str())
.map(String::from);
symbol = e
.pointer("/state/symbol")
.and_then(|s| s.as_str())
.map(String::from);
update_auth = e
.pointer("/state/updateAuthority")
.and_then(|s| s.as_str())
.map(String::from);
mutable = update_auth.as_ref().map(|_| true);
}
if mutable == Some(true) {
flags.push(Flag::new(
"mutable_metadata",
Severity::Info,
"Metadata can still be changed",
"The team can rename the token or swap its image/links at any time.".into(),
2,
));
}
let section = json!({
"tokenProgram": mint.program,
"isToken2022": is_2022,
"decimals": mint.decimals,
"supply": mint.supply_raw,
"mintAuthority": mint.mint_authority,
"freezeAuthority": mint.freeze_authority,
"extensions": ext_summary,
"metadata": {
"name": name,
"symbol": symbol,
"isMutable": mutable,
"updateAuthority": update_auth,
},
});
MintAnalysis {
flags,
section,
name,
symbol,
}
}
fn short(a: &str) -> String {
if a.len() < 12 {
a.to_string()
} else {
format!("{}…{}", &a[..4], &a[a.len() - 4..])
}
}