refactor: rename parse_erc20 function to parse_erc20_transfer for clarity; update function signatures to accept slices instead of vectors for efficiency
What changed, and why it matters
This commit is a routine code cleanup in the Ethereum module of the Keystone 3 firmware. It renames a function for clarity, changes some function signatures to accept data slices instead of owned vectors for efficiency, and replaces a few direct `.unwrap()` calls with safer `.unwrap_or_default()` handling. There is no indication this fixes a security vulnerability.
No security action required. Treat as normal refactoring; review as part of standard code-quality assurance if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff refactors parse_erc20 to parse_erc20_transfer and updates callers. It changes several public functions in rust/apps/ethereum/src/lib.rs to take &[u8] slices rather than Vec<u8>, reducing unnecessary cloning. In rust/rust_c/src/ethereum/mod.rs, it introduces a helper extract_sign_tx_from_payload to consolidate payload extraction and replaces some .unwrap() calls on fingerprint decoding with .unwrap_or_default(). It also removes a small test module. No security-relevant bug fixes or vulnerability mitigations are evident from the diff.
Changed components
rust/apps/ethereum/src/erc20.rsrust/apps/ethereum/src/lib.rsrust/rust_c/src/ethereum/mod.rsInspect captured patch +71 / −91
diff --git a/rust/apps/ethereum/src/erc20.rs b/rust/apps/ethereum/src/erc20.rs
index 270da42..66613f6 100644
--- a/rust/apps/ethereum/src/erc20.rs
+++ b/rust/apps/ethereum/src/erc20.rs
@@ -41,7 +41,10 @@ pub fn encode_erc20_transfer_calldata(to: H160, amount: U256) -> String {
}
// parse erc20 transfer calldata
-pub fn parse_erc20(input: &str, decimal: u32) -> Result<ParsedErc20Transaction, &'static str> {
+pub fn parse_erc20_transfer(
+ input: &str,
+ decimal: u32,
+) -> Result<ParsedErc20Transaction, &'static str> {
validate_calldata_length(input)?;
if &input[0..SELECTOR_LEN] != TRANSFER_SELECTOR {
return Err("Invalid transfer function selector");
@@ -117,7 +120,7 @@ mod tests {
let decimal = 18;
// Call the function
- let result = parse_erc20(input, decimal);
+ let result = parse_erc20_transfer(input, decimal);
// Check the result
match result {
@@ -140,7 +143,7 @@ mod tests {
let input1 = "a9059cbb0000000000000000000000005df9b87991262f6ba471f09758cde1c0fc1de7340000000000000000000000000000000000000000000000008ac7230489e80000";
let decimal = 18;
- let result1 = parse_erc20(input1, decimal);
+ let result1 = parse_erc20_transfer(input1, decimal);
match result1 {
Ok(transaction) => {
@@ -151,7 +154,7 @@ mod tests {
}
let input2 = "a9059cbb0000000000000000000000005df9b87991262f6ba471f09758cde1c0fc1de7340000000000000000000000000000000000000000000000000000000000000000";
- let result2 = parse_erc20(input2, decimal);
+ let result2 = parse_erc20_transfer(input2, decimal);
match result2 {
Ok(transaction) => {
@@ -163,7 +166,7 @@ mod tests {
let input3 = "a9059cbb0000000000000000000000005df9b87991262f6ba471f09758cde1c0fc1de734ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
- let result3 = parse_erc20(input3, decimal);
+ let result3 = parse_erc20_transfer(input3, decimal);
match result3 {
Ok(transaction) => {
diff --git a/rust/apps/ethereum/src/lib.rs b/rust/apps/ethereum/src/lib.rs
index 1ab75c6..78831ad 100644
--- a/rust/apps/ethereum/src/lib.rs
+++ b/rust/apps/ethereum/src/lib.rs
@@ -52,11 +52,11 @@ pub fn parse_fee_market_tx(
}
pub fn parse_personal_message(
- tx_hex: Vec<u8>,
+ tx_hex: &[u8],
from_key: Option<PublicKey>,
) -> Result<PersonalMessage> {
- let raw_message = hex::encode(tx_hex.clone());
- let utf8_message = match String::from_utf8(tx_hex) {
+ let raw_message = hex::encode(tx_hex);
+ let utf8_message = match String::from_utf8(tx_hex.to_vec()) {
Ok(utf8_message) => {
if app_utils::is_cjk(&utf8_message) {
"".to_string()
@@ -69,17 +69,17 @@ pub fn parse_personal_message(
PersonalMessage::from(raw_message, utf8_message, from_key)
}
-pub fn parse_typed_data_message(tx_hex: Vec<u8>, from_key: Option<PublicKey>) -> Result<TypedData> {
- let utf8_message =
- String::from_utf8(tx_hex).map_err(|e| EthereumError::InvalidUtf8Error(e.to_string()))?;
+pub fn parse_typed_data_message(tx_hex: &[u8], from_key: Option<PublicKey>) -> Result<TypedData> {
+ let utf8_message = String::from_utf8(tx_hex.to_vec())
+ .map_err(|e| EthereumError::InvalidUtf8Error(e.to_string()))?;
let typed_data: Eip712TypedData = serde_json::from_str(&utf8_message)
.map_err(|e| EthereumError::InvalidTypedData(e.to_string(), utf8_message))?;
TypedData::from_raw(typed_data, from_key)
}
-pub fn sign_legacy_tx(sign_data: Vec<u8>, seed: &[u8], path: &String) -> Result<EthereumSignature> {
- let tx = LegacyTransaction::decode_raw(sign_data.as_slice())?;
- let hash = keccak256(sign_data.as_slice());
+pub fn sign_legacy_tx(sign_data: &[u8], seed: &[u8], path: &String) -> Result<EthereumSignature> {
+ let tx = LegacyTransaction::decode_raw(sign_data)?;
+ let hash = keccak256(sign_data);
let message = Message::from_digest_slice(&hash).unwrap();
keystore::algorithms::secp256k1::sign_message_by_seed(seed, path, &message)
.map_err(|e| EthereumError::SignFailure(e.to_string()))
@@ -95,12 +95,12 @@ pub fn sign_legacy_tx(sign_data: Vec<u8>, seed: &[u8], path: &String) -> Result<
/// Only used by hot wallet version2
pub fn sign_legacy_tx_v2(
- sign_data: Vec<u8>,
+ sign_data: &[u8],
seed: &[u8],
path: &String,
) -> Result<EthereumSignature> {
- let tx = LegacyTransaction::decode_raw(sign_data.as_slice())?;
- let hash = keccak256(sign_data.as_slice());
+ let tx = LegacyTransaction::decode_raw(sign_data)?;
+ let hash = keccak256(sign_data);
let message = Message::from_digest_slice(&hash).unwrap();
keystore::algorithms::secp256k1::sign_message_by_seed(seed, path, &message)
.map_err(|e| EthereumError::SignFailure(e.to_string()))
@@ -115,7 +115,7 @@ pub fn sign_legacy_tx_v2(
}
pub fn sign_fee_market_tx(
- sign_data: Vec<u8>,
+ sign_data: &[u8],
seed: &[u8],
path: &String,
) -> Result<EthereumSignature> {
@@ -125,7 +125,7 @@ pub fn sign_fee_market_tx(
return Err(EthereumError::InvalidTransaction);
}
- let hash = keccak256(sign_data.as_slice());
+ let hash = keccak256(sign_data);
let message = Message::from_digest_slice(&hash).unwrap();
keystore::algorithms::secp256k1::sign_message_by_seed(seed, path, &message)
.map_err(|e| EthereumError::SignFailure(e.to_string()))
@@ -135,7 +135,7 @@ pub fn sign_fee_market_tx(
const PREFIX_PERSONAL_MESSAGE: &str = "\u{0019}Ethereum Signed Message:\n";
pub fn sign_personal_message(
- sign_data: Vec<u8>,
+ sign_data: &[u8],
seed: &[u8],
path: &String,
) -> Result<EthereumSignature> {
@@ -152,12 +152,12 @@ pub fn sign_personal_message(
}
pub fn sign_typed_data_message(
- sign_data: Vec<u8>,
+ sign_data: &[u8],
seed: &[u8],
path: &String,
) -> Result<EthereumSignature> {
- let utf8_message =
- String::from_utf8(sign_data).map_err(|e| EthereumError::InvalidUtf8Error(e.to_string()))?;
+ let utf8_message = String::from_utf8(sign_data.to_vec())
+ .map_err(|e| EthereumError::InvalidUtf8Error(e.to_string()))?;
let typed_data: Eip712TypedData = serde_json::from_str(&utf8_message)
.map_err(|e| EthereumError::InvalidTypedData(e.to_string(), utf8_message))?;
@@ -194,7 +194,7 @@ mod tests {
let pubkey = get_public_key_by_seed(&seed, &path).unwrap();
let sign_data =
hex::decode("4578616d706c652060706572736f6e616c5f7369676e60206d657373616765").unwrap();
- let result = parse_personal_message(sign_data, Some(pubkey)).unwrap();
+ let result = parse_personal_message(&sign_data, Some(pubkey)).unwrap();
assert_eq!(
"4578616d706c652060706572736f6e616c5f7369676e60206d657373616765",
result.raw_message
@@ -212,7 +212,7 @@ mod tests {
hex::decode("4578616d706c652060706572736f6e616c5f7369676e60206d657373616765").unwrap();
let path = "m/44'/60'/0'/0/0".to_string();
let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
- let message = sign_personal_message(sign_data, &seed, &path).unwrap();
+ let message = sign_personal_message(&sign_data, &seed, &path).unwrap();
assert_eq!("b836ae2bac525ae9d2799928cf6f52919cb2ed5e5e52ca26e3b3cdbeb136ca2f618da0e6413a6aa3aaa722fbc2bcc87f591b8b427ee6915916f257de8125810e1b",
hex::encode(message.serialize()));
}
@@ -279,7 +279,7 @@ mod tests {
let pubkey = get_public_key_by_seed(&seed, &path).unwrap();
let sign_data = hex::decode(sign_data).unwrap();
- let result = parse_typed_data_message(sign_data, Some(pubkey)).unwrap();
+ let result = parse_typed_data_message(&sign_data, Some(pubkey)).unwrap();
assert_eq!("Seaport", &result.name);
assert_eq!("1.1", &result.version);
assert_eq!("1", &result.chain_id);
@@ -296,7 +296,7 @@ mod tests {
hex::decode("7b227479706573223a7b22454950373132446f6d61696e223a5b7b226e616d65223a226e616d65222c2274797065223a22737472696e67227d2c7b226e616d65223a2276657273696f6e222c2274797065223a22737472696e67227d2c7b226e616d65223a22636861696e4964222c2274797065223a2275696e74323536227d2c7b226e616d65223a22766572696679696e67436f6e7472616374222c2274797065223a2261646472657373227d5d2c224f72646572436f6d706f6e656e7473223a5b7b226e616d65223a226f666665726572222c2274797065223a2261646472657373227d2c7b226e616d65223a227a6f6e65222c2274797065223a2261646472657373227d2c7b226e616d65223a226f66666572222c2274797065223a224f666665724974656d5b5d227d2c7b226e616d65223a22737461727454696d65222c2274797065223a2275696e74323536227d2c7b226e616d65223a22656e6454696d65222c2274797065223a2275696e74323536227d2c7b226e616d65223a227a6f6e6548617368222c2274797065223a2262797465733332227d2c7b226e616d65223a2273616c74222c2274797065223a2275696e74323536227d2c7b226e616d65223a22636f6e647569744b6579222c2274797065223a2262797465733332227d2c7b226e616d65223a22636f756e746572222c2274797065223a2275696e74323536227d5d2c224f666665724974656d223a5b7b226e616d65223a22746f6b656e222c2274797065223a2261646472657373227d5d2c22436f6e73696465726174696f6e4974656d223a5b7b226e616d65223a22746f6b656e222c2274797065223a2261646472657373227d2c7b226e616d65223a226964656e7469666965724f724372697465726961222c2274797065223a2275696e74323536227d2c7b226e616d65223a227374617274416d6f756e74222c2274797065223a2275696e74323536227d2c7b226e616d65223a22656e64416d6f756e74222c2274797065223a2275696e74323536227d2c7b226e616d65223a22726563697069656e74222c2274797065223a2261646472657373227d5d7d2c227072696d61727954797065223a224f72646572436f6d706f6e656e7473222c22646f6d61696e223a7b226e616d65223a22536561706f7274222c2276657273696f6e223a22312e31222c22636861696e4964223a2231222c22766572696679696e67436f6e7472616374223a22307830303030303030303030366333383532636245663365303845386446323839313639456445353831227d2c226d657373616765223a7b226f666665726572223a22307866333946643665353161616438384636463463653661423838323732373963666646623932323636222c226f66666572223a5b7b22746f6b656e223a22307841363034303630383930393233466634303065386336663532393034363141383341454441436563227d5d2c22737461727454696d65223a2231363538363435353931222c22656e6454696d65223a2231363539323530333836222c227a6f6e65223a22307830303443303035303030303061443130344437444264303065336165304135433030353630433030222c227a6f6e6548617368223a22307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030222c2273616c74223a223136313738323038383937313336363138222c22636f6e647569744b6579223a22307830303030303037623032323330303931613765643031323330303732663730303661303034643630613864346537316435393962383130343235306630303030222c22746f74616c4f726967696e616c436f6e73696465726174696f6e4974656d73223a2232222c22636f756e746572223a2230227d7d").unwrap();
let path = "m/44'/60'/0'/0/0".to_string();
let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
- let message = sign_typed_data_message(sign_data, &seed, &path).unwrap();
+ let message = sign_typed_data_message(&sign_data, &seed, &path).unwrap();
assert_eq!("042fd02150738ede751c43803d6d7bbbcf32c9afce40c861df87357639862c6a653d3307aa16aff363e3444cb418b72e9d715a6e8e479cb56f4ce3012eed87531b",
hex::encode(message.serialize()));
}
@@ -693,7 +693,7 @@ mod tests {
"#;
let path = "m/44'/60'/0'/0/0".to_string();
let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
- let message = sign_typed_data_message(sign_data.as_bytes().to_vec(), &seed, &path).unwrap();
+ let message = sign_typed_data_message(sign_data.as_bytes(), &seed, &path).unwrap();
assert_eq!(
"cbf0b0d6ef4b47e1624267fb41e00de27f5812d5ff324f1817e73791905554844a80df5ead72fec8ac2be5fa9eebbfddb953577ea6f6f9df3c9dbf490035dd3f1c",
hex::encode(message.serialize())
diff --git a/rust/rust_c/src/ethereum/mod.rs b/rust/rust_c/src/ethereum/mod.rs
index f2befec..212c4a8 100644
--- a/rust/rust_c/src/ethereum/mod.rs
+++ b/rust/rust_c/src/ethereum/mod.rs
@@ -4,7 +4,7 @@ use alloc::{format, slice};
use app_ethereum::address::derive_address;
use app_ethereum::batch_tx_rules::rule_swap;
-use app_ethereum::erc20::{parse_erc20, parse_erc20_approval};
+use app_ethereum::erc20::{parse_erc20_transfer, parse_erc20_approval};
use app_ethereum::errors::EthereumError;
use app_ethereum::{
parse_fee_market_tx, parse_legacy_tx, parse_personal_message, parse_typed_data_message,
@@ -44,6 +44,21 @@ pub mod address;
pub mod structs;
pub(crate) mod util;
+unsafe fn extract_sign_tx_from_payload(
+ ptr: PtrUR,
+) -> Result<ur_registry::pb::protoc::SignTransaction, KeystoneError> {
+ let payload = build_payload(ptr, QRCodeType::Bytes)?;
+ let content = payload
+ .content
+ .ok_or_else(|| KeystoneError::ProtobufError("empty payload content".to_string()))?;
+ match content {
+ Content::SignTx(sign_tx) => Ok(sign_tx),
+ _ => Err(KeystoneError::ProtobufError(
+ "Cant get sign tx struct data".to_string(),
+ )),
+ }
+}
+
#[no_mangle]
pub unsafe extern "C" fn eth_check_ur_bytes(
ptr: PtrUR,
@@ -58,10 +73,10 @@ pub unsafe extern "C" fn eth_check_ur_bytes(
match payload {
Ok(payload) => {
let mfp = extract_array!(master_fingerprint, u8, 4);
- let mfp: [u8; 4] = mfp.to_vec().try_into().unwrap();
+ let mfp: [u8; 4] = mfp.to_vec().try_into().unwrap_or_default();
let xfp = payload.xfp;
- let xfp_vec: [u8; 4] = hex::decode(xfp).unwrap().try_into().unwrap();
+ let xfp_vec: [u8; 4] = hex::decode(xfp).unwrap_or_default().try_into().unwrap_or_default();
if mfp == xfp_vec {
TransactionCheckResult::new().c_ptr()
} else {
@@ -109,13 +124,9 @@ pub unsafe extern "C" fn eth_check(
#[no_mangle]
pub unsafe extern "C" fn eth_get_root_path_bytes(ptr: PtrUR) -> PtrString {
- let payload = build_payload(ptr, QRCodeType::Bytes).unwrap();
- let content = payload.content.unwrap();
- let sign_tx = match content {
- Content::SignTx(sign_tx) => sign_tx,
- _ => {
- return convert_c_char("".to_string());
- }
+ let sign_tx = match extract_sign_tx_from_payload(ptr) {
+ Ok(sign_tx) => sign_tx,
+ Err(_) => return convert_c_char("".to_string()),
};
// convert "M/44'/60'/0'/0/0" to "/44'/60'/0'"
let root_path = sign_tx
@@ -187,15 +198,10 @@ pub unsafe extern "C" fn eth_parse_bytes_data(
ptr: PtrUR,
xpub: PtrString,
) -> PtrT<TransactionParseResult<DisplayETH>> {
- let payload = build_payload(ptr, QRCodeType::Bytes).unwrap();
- let content = payload.content.unwrap();
- let sign_tx = match content {
- Content::SignTx(sign_tx) => sign_tx,
- _ => {
- return TransactionParseResult::from(RustCError::InvalidData(
- "Cant get sign tx struct data".to_string(),
- ))
- .c_ptr();
+ let sign_tx = match extract_sign_tx_from_payload(ptr) {
+ Ok(sign_tx) => sign_tx,
+ Err(e) => {
+ return TransactionParseResult::from(KeystoneError::ProtobufError(e.to_string())).c_ptr();
}
};
let xpub = recover_c_char(xpub);
@@ -284,7 +290,7 @@ pub unsafe extern "C" fn eth_parse_personal_message(
match transaction_type {
TransactionType::PersonalMessage => {
- match parse_personal_message(crypto_eth.get_sign_data(), pubkey) {
+ match parse_personal_message(&crypto_eth.get_sign_data(), pubkey) {
Ok(tx) => {
TransactionParseResult::success(DisplayETHPersonalMessage::from(tx).c_ptr())
.c_ptr()
@@ -454,10 +460,10 @@ pub unsafe extern "C" fn eth_sign_batch_tx(
let sign_data = request.get_sign_data();
let signature = match TransactionType::from(request.get_data_type()) {
TransactionType::Legacy => {
- app_ethereum::sign_legacy_tx(sign_data.to_vec(), seed, &path)
+ app_ethereum::sign_legacy_tx(&sign_data, seed, &path)
}
TransactionType::TypedTransaction => match sign_data.first() {
- Some(0x02) => app_ethereum::sign_fee_market_tx(sign_data.to_vec(), seed, &path),
+ Some(0x02) => app_ethereum::sign_fee_market_tx(&sign_data, seed, &path),
Some(x) => {
return UREncodeResult::from(RustCError::UnsupportedTransaction(format!(
"ethereum tx type: {x}"
@@ -515,7 +521,7 @@ pub unsafe extern "C" fn eth_parse_typed_data(
match transaction_type {
TransactionType::TypedData => {
- let tx = parse_typed_data_message(crypto_eth.get_sign_data(), pubkey);
+ let tx = parse_typed_data_message(&crypto_eth.get_sign_data(), pubkey);
match tx {
Ok(t) => {
TransactionParseResult::success(DisplayETHTypedData::from(t).c_ptr()).c_ptr()
@@ -549,9 +555,9 @@ pub unsafe extern "C" fn eth_sign_tx_dynamic(
let sign_data = crypto_eth.get_sign_data();
let signature = match TransactionType::from(crypto_eth.get_data_type()) {
- TransactionType::Legacy => app_ethereum::sign_legacy_tx(sign_data.to_vec(), seed, &path),
+ TransactionType::Legacy => app_ethereum::sign_legacy_tx(&sign_data, seed, &path),
TransactionType::TypedTransaction => match sign_data.first() {
- Some(0x02) => app_ethereum::sign_fee_market_tx(sign_data.to_vec(), seed, &path),
+ Some(0x02) => app_ethereum::sign_fee_market_tx(&sign_data, seed, &path),
Some(x) => {
return UREncodeResult::from(RustCError::UnsupportedTransaction(format!(
"ethereum tx type: {x}"
@@ -563,10 +569,10 @@ pub unsafe extern "C" fn eth_sign_tx_dynamic(
}
},
TransactionType::PersonalMessage => {
- app_ethereum::sign_personal_message(sign_data.to_vec(), seed, &path)
+ app_ethereum::sign_personal_message(&sign_data, seed, &path)
}
TransactionType::TypedData => {
- app_ethereum::sign_typed_data_message(sign_data.to_vec(), seed, &path)
+ app_ethereum::sign_typed_data_message(&sign_data, seed, &path)
}
};
match signature {
@@ -598,15 +604,10 @@ pub unsafe extern "C" fn eth_sign_tx_bytes(
mfp: PtrBytes,
mfp_len: u32,
) -> PtrT<UREncodeResult> {
- let payload = build_payload(ptr, QRCodeType::Bytes).unwrap();
- let content = payload.content.unwrap();
- let sign_tx = match content {
- Content::SignTx(sign_tx) => sign_tx,
- _ => {
- return UREncodeResult::from(RustCError::InvalidData(
- "Cant get sign tx struct data".to_string(),
- ))
- .c_ptr();
+ let sign_tx = match extract_sign_tx_from_payload(ptr) {
+ Ok(sign_tx) => sign_tx,
+ Err(e) => {
+ return UREncodeResult::from(KeystoneError::ProtobufError(e.to_string())).c_ptr();
}
};
let tx = sign_tx.transaction.unwrap();
@@ -626,7 +627,7 @@ pub unsafe extern "C" fn eth_sign_tx_bytes(
let mfp = extract_array!(mfp, u8, mfp_len as usize);
let signature = app_ethereum::sign_legacy_tx_v2(
- legacy_transaction.encode_raw().to_vec(),
+ &legacy_transaction.encode_raw(),
seed,
&sign_tx.hd_path,
)
@@ -695,7 +696,8 @@ pub unsafe extern "C" fn eth_parse_erc20(
decimal: u32,
) -> PtrT<TransactionParseResult<EthParsedErc20Transaction>> {
let input = recover_c_char(input);
- let tx = parse_erc20(&input, decimal);
+ // just support transfer now
+ let tx = parse_erc20_transfer(&input, decimal);
match tx {
Ok(t) => {
TransactionParseResult::success(EthParsedErc20Transaction::from(t).c_ptr()).c_ptr()
@@ -722,28 +724,3 @@ pub unsafe extern "C" fn eth_parse_erc20_approval(
}
.c_ptr()
}
-
-#[cfg(test)]
-mod tests {
- extern crate std;
-
- use std::println;
-
- #[test]
- fn test() {
- let p = "m/44'/60'/0'/0/0";
- let prefix = "m/44'/60'/0'/";
- println!("{:?}", p.strip_prefix(prefix))
- }
-
- #[test]
- fn test_test() {
- let _path = "44'/60'/1'/0/0";
- let root_path = "44'/60'/";
- if let Some(path) = _path.strip_prefix(root_path) {
- if let Some(index) = path.find('/') {
- println!("{}", &path[index..]);
- }
- };
- }
-}
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.