Skip to content

Commit b657906

Browse files
Jpatchingclaude
andcommitted
test: add 27 unit tests across 5 modules for hackathon judging
Cover ABI decoding (uint256 boundaries, string encoding, fallback), risk scoring (features, holders, bridge, full integration), NTT compatibility (mode, decimals, rebasing), bytecode analysis (capabilities, fee detection, boundaries), and deployment config (JSON output, CLI commands, rate limit defaults). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fe42fca commit b657906

6 files changed

Lines changed: 549 additions & 7 deletions

File tree

src/analyzers/compatibility.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,137 @@ mod tests {
264264
.iter()
265265
.any(|i| i.code == "FEE_ON_TRANSFER" && i.severity == IssueSeverity::Error));
266266
}
267+
268+
// ── Mode determination ─────────────────────────────────
269+
270+
#[test]
271+
fn test_determine_mode_burning() {
272+
let caps = TokenCapabilities {
273+
has_burn: true,
274+
..Default::default()
275+
};
276+
assert_eq!(
277+
CompatibilityChecker::determine_mode(&caps),
278+
NttMode::Burning
279+
);
280+
}
281+
282+
#[test]
283+
fn test_determine_mode_locking() {
284+
assert_eq!(
285+
CompatibilityChecker::determine_mode(&TokenCapabilities::default()),
286+
NttMode::Locking
287+
);
288+
}
289+
290+
// ── Decimal handling ───────────────────────────────────
291+
292+
#[test]
293+
fn test_no_trimming_for_6_decimals() {
294+
let token = TokenInfo {
295+
address: "0x0".to_string(),
296+
chain: Chain::Ethereum,
297+
name: "Test".to_string(),
298+
symbol: "TEST".to_string(),
299+
decimals: 6,
300+
total_supply: "1000000".to_string(),
301+
};
302+
let result = CompatibilityChecker::check(
303+
&token,
304+
&TokenCapabilities::default(),
305+
&BytecodeAnalysis::default(),
306+
);
307+
assert!(!result.decimal_trimming_required);
308+
assert_eq!(result.solana_decimals, 6);
309+
}
310+
311+
#[test]
312+
fn test_trimming_for_9_decimals() {
313+
let token = TokenInfo {
314+
address: "0x0".to_string(),
315+
chain: Chain::Ethereum,
316+
name: "Test".to_string(),
317+
symbol: "TEST".to_string(),
318+
decimals: 9,
319+
total_supply: "1000000".to_string(),
320+
};
321+
let result = CompatibilityChecker::check(
322+
&token,
323+
&TokenCapabilities::default(),
324+
&BytecodeAnalysis::default(),
325+
);
326+
assert!(result.decimal_trimming_required);
327+
assert_eq!(result.solana_decimals, 8);
328+
}
329+
330+
// ── Rebasing → incompatible ────────────────────────────
331+
332+
#[test]
333+
fn test_rebasing_incompatible() {
334+
let token = TokenInfo {
335+
address: "0x0".to_string(),
336+
chain: Chain::Ethereum,
337+
name: "stETH".to_string(),
338+
symbol: "stETH".to_string(),
339+
decimals: 18,
340+
total_supply: "1000000".to_string(),
341+
};
342+
let caps = TokenCapabilities {
343+
is_rebasing: true,
344+
..Default::default()
345+
};
346+
let result = CompatibilityChecker::check(&token, &caps, &BytecodeAnalysis::default());
347+
assert!(!result.is_compatible);
348+
assert!(result
349+
.issues
350+
.iter()
351+
.any(|i| i.code == "REBASING" && i.severity == IssueSeverity::Error));
352+
}
353+
354+
// ── Combined features: pausable + blacklistable ────────
355+
356+
#[test]
357+
fn test_pausable_blacklistable_compatible_with_warnings() {
358+
let token = TokenInfo {
359+
address: "0x0".to_string(),
360+
chain: Chain::Ethereum,
361+
name: "Test".to_string(),
362+
symbol: "TEST".to_string(),
363+
decimals: 6,
364+
total_supply: "1000000".to_string(),
365+
};
366+
let caps = TokenCapabilities {
367+
has_pause: true,
368+
has_blacklist: true,
369+
..Default::default()
370+
};
371+
let result = CompatibilityChecker::check(&token, &caps, &BytecodeAnalysis::default());
372+
assert!(result.is_compatible); // warnings don't block
373+
assert!(result.issues.iter().any(|i| i.code == "PAUSABLE"));
374+
assert!(result.issues.iter().any(|i| i.code == "BLACKLIST"));
375+
}
376+
377+
// ── Burnable token produces Info issue ──────────────────
378+
379+
#[test]
380+
fn test_burnable_info_issue() {
381+
let token = TokenInfo {
382+
address: "0x0".to_string(),
383+
chain: Chain::Ethereum,
384+
name: "Test".to_string(),
385+
symbol: "TEST".to_string(),
386+
decimals: 6,
387+
total_supply: "1000000".to_string(),
388+
};
389+
let caps = TokenCapabilities {
390+
has_burn: true,
391+
..Default::default()
392+
};
393+
let result = CompatibilityChecker::check(&token, &caps, &BytecodeAnalysis::default());
394+
assert!(result.is_compatible);
395+
assert!(result
396+
.issues
397+
.iter()
398+
.any(|i| i.code == "BURNABLE" && i.severity == IssueSeverity::Info));
399+
}
267400
}

src/analyzers/evm/bytecode.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,84 @@ mod tests {
209209
BytecodeComplexity::Complex
210210
);
211211
}
212+
213+
// ── Capability detection ───────────────────────────────
214+
215+
#[test]
216+
fn test_detect_capabilities_mint_burn() {
217+
// Bytecode containing mint and burn selectors
218+
let bytecode = format!(
219+
"608060405234{}00{}00{}",
220+
capability_selectors::MINT,
221+
capability_selectors::BURN,
222+
"deadbeef"
223+
);
224+
let analyzer = BytecodeAnalyzer::new();
225+
let caps = analyzer.detect_capabilities(&bytecode);
226+
assert!(caps.has_mint);
227+
assert!(caps.has_burn);
228+
assert!(!caps.is_rebasing);
229+
}
230+
231+
#[test]
232+
fn test_detect_capabilities_one_rebase_selector_not_rebasing() {
233+
// Only 1 rebase selector → not flagged as rebasing
234+
let bytecode = format!("608060405234{}00deadbeef", capability_selectors::REBASE);
235+
let analyzer = BytecodeAnalyzer::new();
236+
let caps = analyzer.detect_capabilities(&bytecode);
237+
assert!(!caps.is_rebasing);
238+
}
239+
240+
#[test]
241+
fn test_detect_capabilities_two_rebase_selectors_is_rebasing() {
242+
let bytecode = format!(
243+
"608060405234{}00{}00deadbeef",
244+
capability_selectors::REBASE,
245+
capability_selectors::SHARES_OF,
246+
);
247+
let analyzer = BytecodeAnalyzer::new();
248+
let caps = analyzer.detect_capabilities(&bytecode);
249+
assert!(caps.is_rebasing);
250+
}
251+
252+
// ── Fee pattern detection ──────────────────────────────
253+
254+
#[test]
255+
fn test_detect_fee_pattern() {
256+
let analyzer = BytecodeAnalyzer::new();
257+
// setFee(uint256) selector = 69fe0e2d
258+
assert!(analyzer.detect_fee_pattern("6080604069fe0e2d0000"));
259+
assert!(!analyzer.detect_fee_pattern("608060400000000000"));
260+
}
261+
262+
// ── Full analyze ───────────────────────────────────────
263+
264+
#[test]
265+
fn test_analyze_empty_bytecode() {
266+
let analyzer = BytecodeAnalyzer::new();
267+
let result = analyzer.analyze("0x");
268+
assert_eq!(result.size_bytes, 0);
269+
assert_eq!(result.complexity, BytecodeComplexity::Simple);
270+
assert!(!result.is_proxy);
271+
}
272+
273+
// ── Complexity boundary values ─────────────────────────
274+
275+
#[test]
276+
fn test_complexity_boundary_5k() {
277+
// Exactly 5*1024 = 5120 → Moderate (not Simple)
278+
assert_eq!(
279+
BytecodeAnalyzer::calculate_complexity(5 * 1024),
280+
BytecodeComplexity::Moderate
281+
);
282+
}
283+
284+
#[test]
285+
fn test_complexity_boundary_15k() {
286+
// Exactly 15*1024 = 15360 → Complex (not Moderate)
287+
assert_eq!(
288+
BytecodeAnalyzer::calculate_complexity(15 * 1024),
289+
BytecodeComplexity::Complex
290+
);
291+
}
212292
}

src/analyzers/evm/decoder.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ impl AbiDecoder {
155155
mod tests {
156156
use super::*;
157157

158+
// ── uint8 ──────────────────────────────────────────────
159+
158160
#[test]
159161
fn test_decode_uint8() {
160162
// Standard 32-byte padded response for decimals = 18
@@ -167,4 +169,92 @@ mod tests {
167169
let hex = "0x0000000000000000000000000000000000000000000000000000000000000006";
168170
assert_eq!(AbiDecoder::decode_uint8(hex).unwrap(), 6);
169171
}
172+
173+
#[test]
174+
fn test_decode_uint8_zero() {
175+
let hex = "0x0000000000000000000000000000000000000000000000000000000000000000";
176+
assert_eq!(AbiDecoder::decode_uint8(hex).unwrap(), 0);
177+
}
178+
179+
#[test]
180+
fn test_decode_uint8_empty() {
181+
assert_eq!(AbiDecoder::decode_uint8("0x").unwrap(), 0);
182+
assert_eq!(AbiDecoder::decode_uint8("").unwrap(), 0);
183+
}
184+
185+
// ── uint256 ────────────────────────────────────────────
186+
187+
#[test]
188+
fn test_decode_uint256_zero() {
189+
let hex = "0x0000000000000000000000000000000000000000000000000000000000000000";
190+
assert_eq!(AbiDecoder::decode_uint256(hex).unwrap(), "0");
191+
}
192+
193+
#[test]
194+
fn test_decode_uint256_small() {
195+
// 1000000 in hex = 0xF4240
196+
let hex = "0x00000000000000000000000000000000000000000000000000000000000f4240";
197+
assert_eq!(AbiDecoder::decode_uint256(hex).unwrap(), "1000000");
198+
}
199+
200+
#[test]
201+
fn test_decode_uint256_u64_boundary() {
202+
// u64::MAX = 18446744073709551615 = 0xFFFFFFFFFFFFFFFF (16 hex chars)
203+
let hex = "0x000000000000000000000000000000000000000000000000ffffffffffffffff";
204+
assert_eq!(
205+
AbiDecoder::decode_uint256(hex).unwrap(),
206+
"18446744073709551615"
207+
);
208+
}
209+
210+
#[test]
211+
fn test_decode_uint256_u128_range() {
212+
// A value that exceeds u64 but fits in u128
213+
// 10^20 = 100000000000000000000 = 0x56BC75E2D63100000 (17 hex chars)
214+
let hex = "0x0000000000000000000000000000000000000000000000056bc75e2d63100000";
215+
assert_eq!(
216+
AbiDecoder::decode_uint256(hex).unwrap(),
217+
"100000000000000000000"
218+
);
219+
}
220+
221+
#[test]
222+
fn test_decode_uint256_large_hex_to_decimal() {
223+
// Exercises hex_to_decimal: value > 128 bits
224+
// 2^160 = 1461501637330902918203684832716283019655932542976
225+
// hex: 10000000000000000000000000000000000000000 (41 hex chars after trim)
226+
let hex = "0x0000000000000000000000010000000000000000000000000000000000000000";
227+
let result = AbiDecoder::decode_uint256(hex).unwrap();
228+
assert_eq!(result, "1461501637330902918203684832716283019655932542976");
229+
}
230+
231+
// ── string ─────────────────────────────────────────────
232+
233+
#[test]
234+
fn test_decode_string_standard() {
235+
// ABI-encoded "USDC": offset=0x20, length=4, data="USDC" padded
236+
let hex = "0x\
237+
0000000000000000000000000000000000000000000000000000000000000020\
238+
0000000000000000000000000000000000000000000000000000000000000004\
239+
5553444300000000000000000000000000000000000000000000000000000000";
240+
assert_eq!(AbiDecoder::decode_string(hex).unwrap(), "USDC");
241+
}
242+
243+
#[test]
244+
fn test_decode_string_empty() {
245+
// ABI-encoded empty string: offset=0x20, length=0
246+
let hex = "0x\
247+
0000000000000000000000000000000000000000000000000000000000000020\
248+
0000000000000000000000000000000000000000000000000000000000000000";
249+
assert_eq!(AbiDecoder::decode_string(hex).unwrap(), "");
250+
}
251+
252+
#[test]
253+
fn test_decode_string_short_fallback() {
254+
// Non-standard short response — triggers extract_ascii fallback
255+
// "MKR" in hex = 4d4b52
256+
let hex = "4d4b5200000000000000000000000000000000000000000000000000000000";
257+
let result = AbiDecoder::decode_string(hex).unwrap();
258+
assert!(result.contains("MKR"));
259+
}
170260
}

src/commands/list.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,7 @@ pub async fn run_list(
4444
discovery.curated_fallback(max_tokens)
4545
};
4646

47-
let source = if discover
48-
&& discovered
49-
.first()
50-
.and_then(|t| t.market_cap_rank)
51-
.is_some()
52-
{
47+
let source = if discover && discovered.first().and_then(|t| t.market_cap_rank).is_some() {
5348
"CoinGecko"
5449
} else {
5550
"curated list"

0 commit comments

Comments
 (0)