diff --git a/tools/usdt-bridge/src/main.rs b/tools/usdt-bridge/src/main.rs index 3b2b02d1..7b65ea54 100644 --- a/tools/usdt-bridge/src/main.rs +++ b/tools/usdt-bridge/src/main.rs @@ -289,6 +289,38 @@ fn convert_ss58_prefix(addr: &str, target_prefix: u16) -> Result { Ok(account.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(target_prefix))) } +/// Decodes an AccountId32 out of a dynamic event field, replacing the previous +/// Debug-string-contains-substring matching (never verified against a live node, and provably +/// wrong: it fed a `{:?}`-formatted value into convert_ss58_prefix, which can only parse real +/// SS58 text - that call could never have succeeded). AccountId32 fields decode as a composite +/// of 32 byte-sized primitives, sometimes wrapped in one extra single-item composite layer (the +/// same nesting observed in Tiki storage this session) - this peels exactly that. +fn account_id_from_dynamic(value: &subxt::ext::scale_value::Value) -> Option<[u8; 32]> { + use subxt::ext::scale_value::{Composite, ValueDef}; + let mut current = value; + loop { + match ¤t.value { + ValueDef::Composite(Composite::Unnamed(items)) => { + if items.len() == 32 && items.iter().all(|v| v.as_u128().is_some()) { + let bytes: Vec = + items.iter().map(|v| v.as_u128().unwrap() as u8).collect(); + return bytes.try_into().ok(); + } else if items.len() == 1 { + current = &items[0]; + continue; + } + return None; + } + _ => return None, + } + } +} + +fn account_bytes_to_ss58(bytes: [u8; 32], prefix: u16) -> String { + sp_core::crypto::AccountId32::from(bytes) + .to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(prefix)) +} + fn load_wallet_addresses(seed_path: &PathBuf) -> Result<(String, String)> { let content = std::fs::read_to_string(seed_path).context("Failed to read seed file")?; let wallet: WalletSeed = serde_json::from_str(&content).context("Failed to parse seed")?; @@ -914,20 +946,20 @@ async fn listen_deposits(config: &BridgeConfig) -> Result<()> { .and_then(|v| v.as_u128()) .unwrap_or(0); - // Check if transfer is to bridge address - // - // CAVEAT (must be resolved before any mainnet fund flow): matching via a - // Debug-formatted string of the decoded dynamic Value against a 10-char - // slice of the bridge's SS58 text is fragile and unverified against a live - // node in this environment - a Debug rendering of a raw AccountId32/byte - // composite is not guaranteed to contain the address's SS58 text at all. - // This should be replaced with subxt's typed/codegen API - // (#[subxt::subxt(runtime_metadata_path = ...)]) decoding straight into - // AccountId32, tested against testnet, before this handles real value. - if let Some(to_value) = to_field { - let to_str = format!("{:?}", to_value); - if to_str.contains(&polkadot_addr[..10]) { - let from_str = from_field.map(|f| format!("{:?}", f)).unwrap_or_default(); + // Check if transfer is to bridge address - real byte comparison, not string + // matching (see account_id_from_dynamic's doc comment for why the previous + // Debug-string approach was not just fragile but provably broken). + let target_bytes = sp_core::crypto::AccountId32::from_ss58check(polkadot_addr) + .map(|a| Into::<[u8; 32]>::into(a)) + .ok(); + let to_bytes = to_field.and_then(account_id_from_dynamic); + + if to_bytes.is_some() && to_bytes == target_bytes { + { + let from_str = from_field + .and_then(account_id_from_dynamic) + .map(|b| account_bytes_to_ss58(b, 0)) + .unwrap_or_default(); let tx_hash = format!("block-{}-{}", block_number, from_str); info!("📥 DEPOSIT DETECTED!"); @@ -1044,10 +1076,17 @@ async fn listen_withdrawals(config: &BridgeConfig) -> Result<()> { .and_then(|v| v.as_u128()) .unwrap_or(0); - if let Some(to_value) = to_field { - let to_str = format!("{:?}", to_value); - if to_str.contains(&pezkuwi_bridge_addr[..10]) { - let from_str = from_field.map(|f| format!("{:?}", f)).unwrap_or_default(); + let target_bytes = sp_core::crypto::AccountId32::from_ss58check(pezkuwi_bridge_addr) + .map(|a| Into::<[u8; 32]>::into(a)) + .ok(); + let to_bytes = to_field.and_then(account_id_from_dynamic); + + if to_bytes.is_some() && to_bytes == target_bytes { + { + let from_str = from_field + .and_then(account_id_from_dynamic) + .map(|b| account_bytes_to_ss58(b, 42)) + .unwrap_or_default(); let tx_hash = format!("block-{}-{}", block_number, from_str); info!("📤 WITHDRAWAL DETECTED!");