use clear_signing::decoder::parse_signature;
use clear_signing::provider::EmptyDataProvider;
use clear_signing::resolver::ResolvedDescriptor;
use clear_signing::token::{StaticTokenSource, TokenMeta};
use clear_signing::types::descriptor::Descriptor;
use clear_signing::{format_calldata, DisplayEntry, TransactionContext};
fn load_descriptor(fixture: &str) -> Descriptor {
let path = format!("{}/tests/fixtures/{fixture}", env!("CARGO_MANIFEST_DIR"));
let json = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
Descriptor::from_json(&json).unwrap_or_else(|e| panic!("parse {path}: {e}"))
}
fn address_word(hex_addr: &str) -> Vec<u8> {
let hex_str = hex_addr
.strip_prefix("0x")
.or_else(|| hex_addr.strip_prefix("0X"))
.unwrap_or(hex_addr);
let addr_bytes = hex::decode(hex_str).expect("valid hex address");
let mut word = vec![0u8; 12];
word.extend_from_slice(&addr_bytes);
assert_eq!(word.len(), 32);
word
}
fn uint_word(val: u128) -> Vec<u8> {
let mut word = vec![0u8; 16];
word.extend_from_slice(&val.to_be_bytes());
assert_eq!(word.len(), 32);
word
}
fn build_exec_transaction_calldata(
to: &str,
value: u128,
inner_calldata: &[u8],
operation: u8,
) -> Vec<u8> {
let sig = parse_signature(
"execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
)
.unwrap();
let mut calldata = Vec::new();
calldata.extend_from_slice(&sig.selector);
calldata.extend_from_slice(&address_word(to));
calldata.extend_from_slice(&uint_word(value));
calldata.extend_from_slice(&uint_word(320));
calldata.extend_from_slice(&uint_word(operation as u128));
calldata.extend_from_slice(&uint_word(0));
calldata.extend_from_slice(&uint_word(21000));
calldata.extend_from_slice(&uint_word(0));
calldata.extend_from_slice(&[0u8; 32]);
calldata.extend_from_slice(&[0u8; 32]);
let data_offset = 320 + 32 + pad32(inner_calldata.len());
calldata.extend_from_slice(&uint_word(data_offset as u128));
calldata.extend_from_slice(&uint_word(inner_calldata.len() as u128)); calldata.extend_from_slice(inner_calldata);
let padding = pad32(inner_calldata.len()) - inner_calldata.len();
calldata.extend_from_slice(&vec![0u8; padding]);
calldata.extend_from_slice(&uint_word(0));
calldata
}
fn pad32(len: usize) -> usize {
len.div_ceil(32) * 32
}
fn build_erc20_transfer_calldata(to: &str, amount: u128) -> Vec<u8> {
let sig = parse_signature("transfer(address,uint256)").unwrap();
let mut calldata = Vec::new();
calldata.extend_from_slice(&sig.selector);
calldata.extend_from_slice(&address_word(to));
calldata.extend_from_slice(&uint_word(amount));
calldata
}
#[tokio::test]
async fn safe_exec_transaction_wrapping_erc20_transfer() {
let safe_descriptor = load_descriptor("common-Safe.json");
let erc20_descriptor = load_descriptor("erc20-transfer.json");
let safe_addr = "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552";
let usdc_addr = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
let recipient = "0x1234567890123456789012345678901234567890";
let amount = 1_000_000u128;
let inner_calldata = build_erc20_transfer_calldata(recipient, amount);
let outer_calldata = build_exec_transaction_calldata(usdc_addr, 0, &inner_calldata, 0);
let descriptors = vec![
ResolvedDescriptor {
descriptor: safe_descriptor,
chain_id: 1,
address: safe_addr.to_string(),
},
ResolvedDescriptor {
descriptor: erc20_descriptor,
chain_id: 1,
address: usdc_addr.to_string(),
},
];
let mut tokens = StaticTokenSource::new();
tokens.insert(
1,
usdc_addr,
TokenMeta {
symbol: "USDC".to_string(),
decimals: 6,
name: "USD Coin".to_string(),
},
);
let tx = TransactionContext {
chain_id: 1,
to: safe_addr,
calldata: &outer_calldata,
value: None,
from: None,
implementation_address: None,
};
let result = format_calldata(&descriptors, &tx, &tokens).await.unwrap();
assert_eq!(result.intent, "sign multisig operation");
if let DisplayEntry::Item(ref item) = result.entries[0] {
assert_eq!(item.label, "Operation type");
assert_eq!(item.value, "Call");
} else {
panic!(
"expected Item for Operation type, got {:?}",
result.entries[0]
);
}
if let DisplayEntry::Item(ref item) = result.entries[1] {
assert_eq!(item.label, "From Safe");
} else {
panic!("expected Item for From Safe, got {:?}", result.entries[1]);
}
let nested_idx = result
.entries
.iter()
.position(|e| matches!(e, DisplayEntry::Nested { label, .. } if label == "Transaction"))
.expect("expected Nested entry for Transaction");
match &result.entries[nested_idx] {
DisplayEntry::Nested {
label,
intent,
entries,
..
} => {
assert_eq!(label, "Transaction");
assert_eq!(intent, "Transfer tokens");
assert!(
entries.len() >= 2,
"expected at least 2 inner entries, got {}",
entries.len()
);
if let DisplayEntry::Item(ref item) = entries[0] {
assert_eq!(item.label, "To");
} else {
panic!("expected Item for inner To");
}
if let DisplayEntry::Item(ref item) = entries[1] {
assert_eq!(item.label, "Amount");
assert_eq!(item.value, "1 USDC");
} else {
panic!("expected Item for inner Amount");
}
}
other => {
panic!("expected Nested for Transaction, got {:?}", other);
}
}
if let DisplayEntry::Item(ref item) = result.entries[nested_idx + 1] {
assert_eq!(item.label, "Gas amount");
assert_eq!(item.value, "21000");
} else {
panic!("expected Item for Gas amount");
}
if let DisplayEntry::Item(ref item) = result.entries[nested_idx + 2] {
assert_eq!(item.label, "Gas price");
assert_eq!(item.value, "0.0 ETH");
} else {
panic!("expected Item for Gas price");
}
}
#[tokio::test]
async fn safe_exec_transaction_no_inner_descriptor() {
let safe_descriptor = load_descriptor("common-Safe.json");
let safe_addr = "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552";
let unknown_contract = "0x0000000000000000000000000000000000000042";
let inner_calldata =
hex::decode("12345678000000000000000000000000000000000000000000000000000000000000002a")
.unwrap();
let outer_calldata = build_exec_transaction_calldata(unknown_contract, 0, &inner_calldata, 0);
let descriptors = vec![ResolvedDescriptor {
descriptor: safe_descriptor,
chain_id: 1,
address: safe_addr.to_string(),
}];
let tx = TransactionContext {
chain_id: 1,
to: safe_addr,
calldata: &outer_calldata,
value: None,
from: None,
implementation_address: None,
};
let result = format_calldata(&descriptors, &tx, &EmptyDataProvider)
.await
.unwrap();
assert_eq!(result.intent, "sign multisig operation");
assert_eq!(
result.fallback_reason(),
Some(&clear_signing::FallbackReason::NestedCallNotClearSigned)
);
let nested = result
.entries
.iter()
.find(|e| matches!(e, DisplayEntry::Nested { label, .. } if label == "Transaction"))
.expect("expected Nested entry for Transaction");
match nested {
DisplayEntry::Nested { label, intent, .. } => {
assert_eq!(label, "Transaction");
assert!(
intent.contains("Unknown function"),
"expected raw fallback intent, got: {intent}"
);
}
other => {
panic!("expected Nested for Transaction, got {:?}", other);
}
}
}
#[tokio::test]
async fn safe_exec_transaction_container_value_propagation() {
let safe_descriptor = load_descriptor("common-Safe.json");
let erc20_descriptor = load_descriptor("erc20-transfer.json");
let safe_addr = "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552";
let usdc_addr = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
let recipient = "0x1234567890123456789012345678901234567890";
let inner_calldata = build_erc20_transfer_calldata(recipient, 500_000);
let outer_calldata =
build_exec_transaction_calldata(usdc_addr, 1_000_000_000_000_000_000, &inner_calldata, 0);
let descriptors = vec![
ResolvedDescriptor {
descriptor: safe_descriptor,
chain_id: 1,
address: safe_addr.to_string(),
},
ResolvedDescriptor {
descriptor: erc20_descriptor,
chain_id: 1,
address: usdc_addr.to_string(),
},
];
let mut tokens = StaticTokenSource::new();
tokens.insert(
1,
usdc_addr,
TokenMeta {
symbol: "USDC".to_string(),
decimals: 6,
name: "USD Coin".to_string(),
},
);
let tx = TransactionContext {
chain_id: 1,
to: safe_addr,
calldata: &outer_calldata,
value: None,
from: None,
implementation_address: None,
};
let result = format_calldata(&descriptors, &tx, &tokens).await.unwrap();
let nested = result
.entries
.iter()
.find(|e| matches!(e, DisplayEntry::Nested { label, .. } if label == "Transaction"))
.expect("expected Nested entry for Transaction");
match nested {
DisplayEntry::Nested {
intent, entries, ..
} => {
assert_eq!(intent, "Transfer tokens");
if let DisplayEntry::Item(ref item) = entries[1] {
assert_eq!(item.label, "Amount");
assert_eq!(item.value, "0.5 USDC");
}
}
_ => panic!("expected Nested"),
}
}
#[tokio::test]
async fn safe_exec_transaction_depth_limit() {
let safe_descriptor = load_descriptor("common-Safe.json");
let safe_addr = "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552";
let erc20_descriptor = load_descriptor("erc20-transfer.json");
let usdc_addr = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
let recipient = "0x1234567890123456789012345678901234567890";
let erc20_calldata = build_erc20_transfer_calldata(recipient, 1_000_000);
let level3_calldata = build_exec_transaction_calldata(usdc_addr, 0, &erc20_calldata, 0);
let level2_calldata = build_exec_transaction_calldata(safe_addr, 0, &level3_calldata, 0);
let level1_calldata = build_exec_transaction_calldata(safe_addr, 0, &level2_calldata, 0);
let outer_calldata = build_exec_transaction_calldata(safe_addr, 0, &level1_calldata, 0);
let descriptors = vec![
ResolvedDescriptor {
descriptor: safe_descriptor,
chain_id: 1,
address: safe_addr.to_string(),
},
ResolvedDescriptor {
descriptor: erc20_descriptor,
chain_id: 1,
address: usdc_addr.to_string(),
},
];
let tx = TransactionContext {
chain_id: 1,
to: safe_addr,
calldata: &outer_calldata,
value: None,
from: None,
implementation_address: None,
};
let result = format_calldata(&descriptors, &tx, &EmptyDataProvider)
.await
.unwrap();
assert_eq!(result.intent, "sign multisig operation");
assert_eq!(
result.fallback_reason(),
Some(&clear_signing::FallbackReason::NestedCallNotClearSigned)
);
fn find_depth_limited_nested(entries: &[DisplayEntry], depth: usize) -> bool {
for entry in entries {
if let DisplayEntry::Nested {
intent, entries, ..
} = entry
{
if intent == "Unknown"
&& entries.iter().any(|inner| {
matches!(
inner,
DisplayEntry::Item(item) if item.label == "Raw data"
)
})
{
return true;
}
if depth < 10 && find_depth_limited_nested(entries, depth + 1) {
return true;
}
}
}
false
}
assert!(
result
.diagnostics()
.iter()
.any(|diagnostic| diagnostic.code == "nested_calldata_degraded"),
"expected nested depth-limit diagnostic code"
);
assert!(
result
.diagnostics()
.iter()
.any(|diagnostic| diagnostic.message.contains("depth limit")),
"expected nested depth-limit diagnostic"
);
assert!(
find_depth_limited_nested(&result.entries, 0),
"expected raw nested fallback somewhere in nested structure"
);
}