Skip to main content

txgate_chain/
erc20.rs

1//! ERC-20 calldata parsing module.
2//!
3//! This module provides functionality to detect and parse ERC-20 token function calls
4//! from transaction calldata. It supports the three primary ERC-20 functions:
5//!
6//! - `transfer(address,uint256)` - Transfer tokens to a recipient
7//! - `approve(address,uint256)` - Approve a spender to transfer tokens
8//! - `transferFrom(address,address,uint256)` - Transfer tokens from one address to another
9//!
10//! # Function Selectors
11//!
12//! ERC-20 function selectors are the first 4 bytes of the keccak256 hash of the function signature:
13//!
14//! | Function | Selector |
15//! |----------|----------|
16//! | `transfer(address,uint256)` | `0xa9059cbb` |
17//! | `approve(address,uint256)` | `0x095ea7b3` |
18//! | `transferFrom(address,address,uint256)` | `0x23b872dd` |
19//!
20//! # ABI Encoding
21//!
22//! ERC-20 calldata follows Solidity ABI encoding:
23//! - First 4 bytes: function selector
24//! - Each parameter: 32 bytes (left-padded for addresses, big-endian for uint256)
25//! - Addresses occupy bytes 12-32 of their 32-byte word
26//!
27//! # Example
28//!
29//! ```rust
30//! use txgate_chain::erc20::{parse_erc20_call, Erc20Call};
31//! use alloy_primitives::hex;
32//!
33//! // ERC-20 transfer calldata: transfer(0x1234...5678, 1000000)
34//! let calldata = hex::decode(
35//!     "a9059cbb\
36//!      0000000000000000000000001234567890123456789012345678901234567890\
37//!      00000000000000000000000000000000000000000000000000000000000f4240"
38//! ).unwrap();
39//!
40//! if let Some(Erc20Call::Transfer { to, amount }) = parse_erc20_call(&calldata) {
41//!     // Handle transfer
42//! }
43//! ```
44
45use alloy_primitives::U256;
46
47/// ERC-20 function selector for `transfer(address,uint256)`.
48///
49/// Computed as: `keccak256("transfer(address,uint256)")[:4]`
50pub const TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb];
51
52/// ERC-20 function selector for `approve(address,uint256)`.
53///
54/// Computed as: `keccak256("approve(address,uint256)")[:4]`
55pub const APPROVE_SELECTOR: [u8; 4] = [0x09, 0x5e, 0xa7, 0xb3];
56
57/// ERC-20 function selector for `transferFrom(address,address,uint256)`.
58///
59/// Computed as: `keccak256("transferFrom(address,address,uint256)")[:4]`
60pub const TRANSFER_FROM_SELECTOR: [u8; 4] = [0x23, 0xb8, 0x72, 0xdd];
61
62/// Minimum calldata length for a transfer or approve call (4 + 32 + 32 = 68 bytes).
63const MIN_TWO_PARAM_LENGTH: usize = 68;
64
65/// Minimum calldata length for a transferFrom call (4 + 32 + 32 + 32 = 100 bytes).
66const MIN_THREE_PARAM_LENGTH: usize = 100;
67
68/// Parsed ERC-20 function call.
69///
70/// Represents one of the three primary ERC-20 functions with their decoded parameters.
71///
72/// # Examples
73///
74/// ```rust
75/// use txgate_chain::erc20::Erc20Call;
76/// use alloy_primitives::U256;
77///
78/// let transfer = Erc20Call::Transfer {
79///     to: [0x12; 20],
80///     amount: U256::from(1_000_000u64),
81/// };
82///
83/// let approve = Erc20Call::Approve {
84///     spender: [0x34; 20],
85///     amount: U256::MAX, // Unlimited approval
86/// };
87///
88/// let transfer_from = Erc20Call::TransferFrom {
89///     from: [0x12; 20],
90///     to: [0x34; 20],
91///     amount: U256::from(500_000u64),
92/// };
93/// ```
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum Erc20Call {
96    /// ERC-20 `transfer(address to, uint256 amount)` call.
97    ///
98    /// Transfers `amount` tokens from the caller to `to`.
99    Transfer {
100        /// Recipient address (20 bytes).
101        to: [u8; 20],
102        /// Amount of tokens to transfer (in smallest unit).
103        amount: U256,
104    },
105
106    /// ERC-20 `approve(address spender, uint256 amount)` call.
107    ///
108    /// Approves `spender` to transfer up to `amount` tokens on behalf of the caller.
109    Approve {
110        /// Spender address authorized to transfer tokens (20 bytes).
111        spender: [u8; 20],
112        /// Maximum amount the spender can transfer.
113        amount: U256,
114    },
115
116    /// ERC-20 `transferFrom(address from, address to, uint256 amount)` call.
117    ///
118    /// Transfers `amount` tokens from `from` to `to`, using the caller's allowance.
119    TransferFrom {
120        /// Source address to transfer from (20 bytes).
121        from: [u8; 20],
122        /// Destination address to transfer to (20 bytes).
123        to: [u8; 20],
124        /// Amount of tokens to transfer (in smallest unit).
125        amount: U256,
126    },
127}
128
129impl Erc20Call {
130    /// Returns the actual recipient address for this call.
131    ///
132    /// For `Transfer` and `TransferFrom`, returns the `to` address.
133    /// For `Approve`, returns the `spender` address.
134    #[must_use]
135    pub const fn recipient(&self) -> &[u8; 20] {
136        match self {
137            Self::Transfer { to, .. } | Self::TransferFrom { to, .. } => to,
138            Self::Approve { spender, .. } => spender,
139        }
140    }
141
142    /// Returns the amount involved in this call.
143    #[must_use]
144    pub const fn amount(&self) -> &U256 {
145        match self {
146            Self::Transfer { amount, .. }
147            | Self::Approve { amount, .. }
148            | Self::TransferFrom { amount, .. } => amount,
149        }
150    }
151
152    /// Returns `true` if this is a transfer operation (`Transfer` or `TransferFrom`).
153    #[must_use]
154    pub const fn is_transfer(&self) -> bool {
155        matches!(self, Self::Transfer { .. } | Self::TransferFrom { .. })
156    }
157
158    /// Returns `true` if this is an approval operation.
159    #[must_use]
160    pub const fn is_approval(&self) -> bool {
161        matches!(self, Self::Approve { .. })
162    }
163}
164
165/// Parse ERC-20 function call from transaction calldata.
166///
167/// Analyzes the calldata to detect and decode ERC-20 function calls.
168///
169/// # Arguments
170///
171/// * `data` - Raw transaction calldata bytes
172///
173/// # Returns
174///
175/// * `Some(Erc20Call)` - If calldata matches an ERC-20 function signature and has valid length
176/// * `None` - If calldata doesn't match ERC-20 signatures or is too short
177///
178/// # Safety
179///
180/// This function uses safe indexing (`.get()`) throughout to prevent panics.
181/// It validates calldata length before attempting to parse parameters.
182///
183/// # Example
184///
185/// ```rust
186/// use txgate_chain::erc20::{parse_erc20_call, Erc20Call};
187///
188/// // Empty calldata returns None
189/// assert!(parse_erc20_call(&[]).is_none());
190///
191/// // Too short calldata returns None
192/// assert!(parse_erc20_call(&[0xa9, 0x05, 0x9c, 0xbb]).is_none());
193///
194/// // Non-ERC20 calldata returns None
195/// let other_calldata = [0x12, 0x34, 0x56, 0x78];
196/// assert!(parse_erc20_call(&other_calldata).is_none());
197/// ```
198#[must_use]
199pub fn parse_erc20_call(data: &[u8]) -> Option<Erc20Call> {
200    // Need at least 4 bytes for the function selector
201    let selector = data.get(0..4)?;
202
203    // Convert selector slice to array for comparison
204    let selector_arr: [u8; 4] = selector.try_into().ok()?;
205
206    match selector_arr {
207        TRANSFER_SELECTOR => parse_transfer(data),
208        APPROVE_SELECTOR => parse_approve(data),
209        TRANSFER_FROM_SELECTOR => parse_transfer_from(data),
210        _ => None,
211    }
212}
213
214/// Parse `transfer(address,uint256)` calldata.
215fn parse_transfer(data: &[u8]) -> Option<Erc20Call> {
216    // Validate minimum length: 4 (selector) + 32 (address) + 32 (amount) = 68 bytes
217    if data.len() < MIN_TWO_PARAM_LENGTH {
218        return None;
219    }
220
221    // Extract address from bytes 4-36 (32-byte word, address in last 20 bytes)
222    let to = extract_address(data, 4)?;
223
224    // Extract amount from bytes 36-68 (32-byte word, big-endian uint256)
225    let amount = extract_u256(data, 36)?;
226
227    Some(Erc20Call::Transfer { to, amount })
228}
229
230/// Parse `approve(address,uint256)` calldata.
231fn parse_approve(data: &[u8]) -> Option<Erc20Call> {
232    // Validate minimum length: 4 (selector) + 32 (address) + 32 (amount) = 68 bytes
233    if data.len() < MIN_TWO_PARAM_LENGTH {
234        return None;
235    }
236
237    // Extract spender address from bytes 4-36
238    let spender = extract_address(data, 4)?;
239
240    // Extract amount from bytes 36-68
241    let amount = extract_u256(data, 36)?;
242
243    Some(Erc20Call::Approve { spender, amount })
244}
245
246/// Parse `transferFrom(address,address,uint256)` calldata.
247fn parse_transfer_from(data: &[u8]) -> Option<Erc20Call> {
248    // Validate minimum length: 4 (selector) + 32 + 32 + 32 = 100 bytes
249    if data.len() < MIN_THREE_PARAM_LENGTH {
250        return None;
251    }
252
253    // Extract from address from bytes 4-36
254    let from = extract_address(data, 4)?;
255
256    // Extract to address from bytes 36-68
257    let to = extract_address(data, 36)?;
258
259    // Extract amount from bytes 68-100
260    let amount = extract_u256(data, 68)?;
261
262    Some(Erc20Call::TransferFrom { from, to, amount })
263}
264
265/// Extract a 20-byte address from a 32-byte ABI-encoded word.
266///
267/// In Solidity ABI encoding, addresses are left-padded with zeros to fill 32 bytes.
268/// The actual address occupies the last 20 bytes (offset 12-32 within the word).
269fn extract_address(data: &[u8], offset: usize) -> Option<[u8; 20]> {
270    // Address is in bytes 12-32 of the 32-byte word (last 20 bytes)
271    let addr_start = offset + 12;
272    let addr_end = offset + 32;
273
274    let addr_slice = data.get(addr_start..addr_end)?;
275    let addr: [u8; 20] = addr_slice.try_into().ok()?;
276
277    Some(addr)
278}
279
280/// Extract a U256 from a 32-byte ABI-encoded word.
281///
282/// In Solidity ABI encoding, uint256 values are stored as big-endian 32-byte words.
283fn extract_u256(data: &[u8], offset: usize) -> Option<U256> {
284    let end = offset + 32;
285    let word_slice = data.get(offset..end)?;
286    let word: [u8; 32] = word_slice.try_into().ok()?;
287
288    Some(U256::from_be_bytes(word))
289}
290
291/// Check if the given calldata starts with an ERC-20 function selector.
292///
293/// This is a quick check without parsing the full calldata.
294///
295/// # Example
296///
297/// ```rust
298/// use txgate_chain::erc20::is_erc20_selector;
299///
300/// // Transfer selector
301/// assert!(is_erc20_selector(&[0xa9, 0x05, 0x9c, 0xbb]));
302///
303/// // Approve selector
304/// assert!(is_erc20_selector(&[0x09, 0x5e, 0xa7, 0xb3]));
305///
306/// // TransferFrom selector
307/// assert!(is_erc20_selector(&[0x23, 0xb8, 0x72, 0xdd]));
308///
309/// // Unknown selector
310/// assert!(!is_erc20_selector(&[0x12, 0x34, 0x56, 0x78]));
311///
312/// // Too short
313/// assert!(!is_erc20_selector(&[0xa9, 0x05]));
314/// ```
315#[must_use]
316pub fn is_erc20_selector(data: &[u8]) -> bool {
317    if data.len() < 4 {
318        return false;
319    }
320
321    let Some(selector) = data.get(0..4) else {
322        return false;
323    };
324
325    let Ok(selector_arr): Result<[u8; 4], _> = selector.try_into() else {
326        return false;
327    };
328
329    matches!(
330        selector_arr,
331        TRANSFER_SELECTOR | APPROVE_SELECTOR | TRANSFER_FROM_SELECTOR
332    )
333}
334
335#[cfg(test)]
336mod tests {
337    #![allow(
338        clippy::expect_used,
339        clippy::unwrap_used,
340        clippy::panic,
341        clippy::indexing_slicing,
342        clippy::similar_names,
343        clippy::redundant_clone,
344        clippy::manual_string_new,
345        clippy::needless_raw_string_hashes,
346        clippy::needless_collect,
347        clippy::unreadable_literal
348    )]
349
350    use super::*;
351    use alloy_primitives::hex;
352
353    // ========================================================================
354    // Function Selector Tests
355    // ========================================================================
356
357    #[test]
358    fn test_transfer_selector_is_correct() {
359        // keccak256("transfer(address,uint256)") = 0xa9059cbb...
360        assert_eq!(TRANSFER_SELECTOR, [0xa9, 0x05, 0x9c, 0xbb]);
361    }
362
363    #[test]
364    fn test_approve_selector_is_correct() {
365        // keccak256("approve(address,uint256)") = 0x095ea7b3...
366        assert_eq!(APPROVE_SELECTOR, [0x09, 0x5e, 0xa7, 0xb3]);
367    }
368
369    #[test]
370    fn test_transfer_from_selector_is_correct() {
371        // keccak256("transferFrom(address,address,uint256)") = 0x23b872dd...
372        assert_eq!(TRANSFER_FROM_SELECTOR, [0x23, 0xb8, 0x72, 0xdd]);
373    }
374
375    // ========================================================================
376    // is_erc20_selector Tests
377    // ========================================================================
378
379    #[test]
380    fn test_is_erc20_selector_transfer() {
381        assert!(is_erc20_selector(&TRANSFER_SELECTOR));
382    }
383
384    #[test]
385    fn test_is_erc20_selector_approve() {
386        assert!(is_erc20_selector(&APPROVE_SELECTOR));
387    }
388
389    #[test]
390    fn test_is_erc20_selector_transfer_from() {
391        assert!(is_erc20_selector(&TRANSFER_FROM_SELECTOR));
392    }
393
394    #[test]
395    fn test_is_erc20_selector_unknown() {
396        assert!(!is_erc20_selector(&[0x12, 0x34, 0x56, 0x78]));
397    }
398
399    #[test]
400    fn test_is_erc20_selector_too_short() {
401        assert!(!is_erc20_selector(&[]));
402        assert!(!is_erc20_selector(&[0xa9]));
403        assert!(!is_erc20_selector(&[0xa9, 0x05]));
404        assert!(!is_erc20_selector(&[0xa9, 0x05, 0x9c]));
405    }
406
407    #[test]
408    fn test_is_erc20_selector_with_extra_data() {
409        // Should still work with extra data after selector
410        let mut data = TRANSFER_SELECTOR.to_vec();
411        data.extend_from_slice(&[0x00; 64]);
412        assert!(is_erc20_selector(&data));
413    }
414
415    // ========================================================================
416    // parse_erc20_call Empty/Short Input Tests
417    // ========================================================================
418
419    #[test]
420    fn test_parse_empty_returns_none() {
421        assert!(parse_erc20_call(&[]).is_none());
422    }
423
424    #[test]
425    fn test_parse_too_short_for_selector_returns_none() {
426        assert!(parse_erc20_call(&[0xa9]).is_none());
427        assert!(parse_erc20_call(&[0xa9, 0x05]).is_none());
428        assert!(parse_erc20_call(&[0xa9, 0x05, 0x9c]).is_none());
429    }
430
431    #[test]
432    fn test_parse_selector_only_returns_none() {
433        // Just the selector, no parameters
434        assert!(parse_erc20_call(&TRANSFER_SELECTOR).is_none());
435        assert!(parse_erc20_call(&APPROVE_SELECTOR).is_none());
436        assert!(parse_erc20_call(&TRANSFER_FROM_SELECTOR).is_none());
437    }
438
439    #[test]
440    fn test_parse_unknown_selector_returns_none() {
441        let data = hex::decode(
442            "12345678\
443             0000000000000000000000001234567890123456789012345678901234567890\
444             0000000000000000000000000000000000000000000000000000000000000001",
445        )
446        .expect("valid hex");
447
448        assert!(parse_erc20_call(&data).is_none());
449    }
450
451    // ========================================================================
452    // Transfer Parsing Tests
453    // ========================================================================
454
455    #[test]
456    fn test_parse_transfer_valid() {
457        // transfer(0x1234567890123456789012345678901234567890, 1000000)
458        let data = hex::decode(
459            "a9059cbb\
460             0000000000000000000000001234567890123456789012345678901234567890\
461             00000000000000000000000000000000000000000000000000000000000f4240",
462        )
463        .expect("valid hex");
464
465        let result = parse_erc20_call(&data);
466        assert!(result.is_some());
467
468        let call = result.expect("should parse successfully");
469        match call {
470            Erc20Call::Transfer { to, amount } => {
471                let expected_to =
472                    hex::decode("1234567890123456789012345678901234567890").expect("valid hex");
473                let expected_to_arr: [u8; 20] = expected_to.try_into().expect("20 bytes");
474                assert_eq!(to, expected_to_arr);
475                assert_eq!(amount, U256::from(1_000_000u64));
476            }
477            _ => panic!("expected Transfer variant"),
478        }
479    }
480
481    #[test]
482    fn test_parse_transfer_zero_amount() {
483        let data = hex::decode(
484            "a9059cbb\
485             000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd\
486             0000000000000000000000000000000000000000000000000000000000000000",
487        )
488        .expect("valid hex");
489
490        let result = parse_erc20_call(&data);
491        assert!(result.is_some());
492
493        if let Some(Erc20Call::Transfer { amount, .. }) = result {
494            assert_eq!(amount, U256::ZERO);
495        } else {
496            panic!("expected Transfer variant");
497        }
498    }
499
500    #[test]
501    fn test_parse_transfer_max_amount() {
502        let data = hex::decode(
503            "a9059cbb\
504             000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd\
505             ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
506        )
507        .expect("valid hex");
508
509        let result = parse_erc20_call(&data);
510        assert!(result.is_some());
511
512        if let Some(Erc20Call::Transfer { amount, .. }) = result {
513            assert_eq!(amount, U256::MAX);
514        } else {
515            panic!("expected Transfer variant");
516        }
517    }
518
519    #[test]
520    fn test_parse_transfer_too_short() {
521        // Missing 1 byte from amount
522        let data = hex::decode(
523            "a9059cbb\
524             0000000000000000000000001234567890123456789012345678901234567890\
525             000000000000000000000000000000000000000000000000000000000000f4",
526        )
527        .expect("valid hex");
528
529        assert!(parse_erc20_call(&data).is_none());
530    }
531
532    #[test]
533    fn test_parse_transfer_with_extra_data() {
534        // Extra data after valid transfer calldata should still parse
535        let mut data = hex::decode(
536            "a9059cbb\
537             0000000000000000000000001234567890123456789012345678901234567890\
538             00000000000000000000000000000000000000000000000000000000000f4240",
539        )
540        .expect("valid hex");
541        data.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
542
543        let result = parse_erc20_call(&data);
544        assert!(result.is_some());
545        assert!(matches!(result, Some(Erc20Call::Transfer { .. })));
546    }
547
548    // ========================================================================
549    // Approve Parsing Tests
550    // ========================================================================
551
552    #[test]
553    fn test_parse_approve_valid() {
554        // approve(0xabcdefabcdefabcdefabcdefabcdefabcdefabcd, 5000000)
555        let data = hex::decode(
556            "095ea7b3\
557             000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd\
558             00000000000000000000000000000000000000000000000000000000004c4b40",
559        )
560        .expect("valid hex");
561
562        let result = parse_erc20_call(&data);
563        assert!(result.is_some());
564
565        let call = result.expect("should parse successfully");
566        match call {
567            Erc20Call::Approve { spender, amount } => {
568                let expected_spender =
569                    hex::decode("abcdefabcdefabcdefabcdefabcdefabcdefabcd").expect("valid hex");
570                let expected_spender_arr: [u8; 20] = expected_spender.try_into().expect("20 bytes");
571                assert_eq!(spender, expected_spender_arr);
572                assert_eq!(amount, U256::from(5_000_000u64));
573            }
574            _ => panic!("expected Approve variant"),
575        }
576    }
577
578    #[test]
579    fn test_parse_approve_unlimited() {
580        // Unlimited approval (max uint256)
581        let data = hex::decode(
582            "095ea7b3\
583             000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd\
584             ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
585        )
586        .expect("valid hex");
587
588        let result = parse_erc20_call(&data);
589        assert!(result.is_some());
590
591        if let Some(Erc20Call::Approve { amount, .. }) = result {
592            assert_eq!(amount, U256::MAX);
593        } else {
594            panic!("expected Approve variant");
595        }
596    }
597
598    #[test]
599    fn test_parse_approve_too_short() {
600        let data = hex::decode(
601            "095ea7b3\
602             000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefab",
603        )
604        .expect("valid hex");
605
606        assert!(parse_erc20_call(&data).is_none());
607    }
608
609    // ========================================================================
610    // TransferFrom Parsing Tests
611    // ========================================================================
612
613    #[test]
614    fn test_parse_transfer_from_valid() {
615        // transferFrom(0x1111..., 0x2222..., 1000000000000000000)
616        let data = hex::decode(
617            "23b872dd\
618             0000000000000000000000001111111111111111111111111111111111111111\
619             0000000000000000000000002222222222222222222222222222222222222222\
620             0000000000000000000000000000000000000000000000000de0b6b3a7640000",
621        )
622        .expect("valid hex");
623
624        let result = parse_erc20_call(&data);
625        assert!(result.is_some());
626
627        let call = result.expect("should parse successfully");
628        match call {
629            Erc20Call::TransferFrom { from, to, amount } => {
630                let expected_from =
631                    hex::decode("1111111111111111111111111111111111111111").expect("valid hex");
632                let expected_from_arr: [u8; 20] = expected_from.try_into().expect("20 bytes");
633
634                let expected_to =
635                    hex::decode("2222222222222222222222222222222222222222").expect("valid hex");
636                let expected_to_arr: [u8; 20] = expected_to.try_into().expect("20 bytes");
637
638                assert_eq!(from, expected_from_arr);
639                assert_eq!(to, expected_to_arr);
640                assert_eq!(amount, U256::from(1_000_000_000_000_000_000u64)); // 1 ETH equivalent
641            }
642            _ => panic!("expected TransferFrom variant"),
643        }
644    }
645
646    #[test]
647    fn test_parse_transfer_from_too_short() {
648        // Missing the amount parameter entirely
649        let data = hex::decode(
650            "23b872dd\
651             0000000000000000000000001111111111111111111111111111111111111111\
652             0000000000000000000000002222222222222222222222222222222222222222",
653        )
654        .expect("valid hex");
655
656        assert!(parse_erc20_call(&data).is_none());
657    }
658
659    #[test]
660    fn test_parse_transfer_from_partial_amount() {
661        // Amount is only 31 bytes instead of 32
662        let data = hex::decode(
663            "23b872dd\
664             0000000000000000000000001111111111111111111111111111111111111111\
665             0000000000000000000000002222222222222222222222222222222222222222\
666             00000000000000000000000000000000000000000000000000000000000001",
667        )
668        .expect("valid hex");
669
670        assert!(parse_erc20_call(&data).is_none());
671    }
672
673    // ========================================================================
674    // Erc20Call Helper Method Tests
675    // ========================================================================
676
677    #[test]
678    fn test_erc20call_recipient_transfer() {
679        let to = [0x12; 20];
680        let call = Erc20Call::Transfer {
681            to,
682            amount: U256::from(100u64),
683        };
684        assert_eq!(call.recipient(), &to);
685    }
686
687    #[test]
688    fn test_erc20call_recipient_approve() {
689        let spender = [0x34; 20];
690        let call = Erc20Call::Approve {
691            spender,
692            amount: U256::from(100u64),
693        };
694        assert_eq!(call.recipient(), &spender);
695    }
696
697    #[test]
698    fn test_erc20call_recipient_transfer_from() {
699        let from = [0x12; 20];
700        let to = [0x34; 20];
701        let call = Erc20Call::TransferFrom {
702            from,
703            to,
704            amount: U256::from(100u64),
705        };
706        assert_eq!(call.recipient(), &to);
707    }
708
709    #[test]
710    fn test_erc20call_amount() {
711        let amount = U256::from(12345u64);
712
713        let transfer = Erc20Call::Transfer {
714            to: [0; 20],
715            amount,
716        };
717        assert_eq!(*transfer.amount(), amount);
718
719        let approve = Erc20Call::Approve {
720            spender: [0; 20],
721            amount,
722        };
723        assert_eq!(*approve.amount(), amount);
724
725        let transfer_from = Erc20Call::TransferFrom {
726            from: [0; 20],
727            to: [0; 20],
728            amount,
729        };
730        assert_eq!(*transfer_from.amount(), amount);
731    }
732
733    #[test]
734    fn test_erc20call_is_transfer() {
735        let transfer = Erc20Call::Transfer {
736            to: [0; 20],
737            amount: U256::ZERO,
738        };
739        assert!(transfer.is_transfer());
740        assert!(!transfer.is_approval());
741
742        let transfer_from = Erc20Call::TransferFrom {
743            from: [0; 20],
744            to: [0; 20],
745            amount: U256::ZERO,
746        };
747        assert!(transfer_from.is_transfer());
748        assert!(!transfer_from.is_approval());
749
750        let approve = Erc20Call::Approve {
751            spender: [0; 20],
752            amount: U256::ZERO,
753        };
754        assert!(!approve.is_transfer());
755        assert!(approve.is_approval());
756    }
757
758    // ========================================================================
759    // Clone and Debug Tests
760    // ========================================================================
761
762    #[test]
763    fn test_erc20call_clone() {
764        let call = Erc20Call::Transfer {
765            to: [0x12; 20],
766            amount: U256::from(100u64),
767        };
768        let cloned = call.clone();
769        assert_eq!(call, cloned);
770    }
771
772    #[test]
773    fn test_erc20call_debug() {
774        let call = Erc20Call::Transfer {
775            to: [0x12; 20],
776            amount: U256::from(100u64),
777        };
778        let debug_str = format!("{call:?}");
779        assert!(debug_str.contains("Transfer"));
780    }
781
782    // ========================================================================
783    // Real-World Calldata Tests
784    // ========================================================================
785
786    #[test]
787    fn test_parse_usdc_transfer() {
788        // Real USDC transfer: transfer(0xRecipient, 1000000) = 1 USDC (6 decimals)
789        let data = hex::decode(
790            "a9059cbb\
791             000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7\
792             00000000000000000000000000000000000000000000000000000000000f4240",
793        )
794        .expect("valid hex");
795
796        let result = parse_erc20_call(&data);
797        assert!(result.is_some());
798
799        if let Some(Erc20Call::Transfer { amount, .. }) = result {
800            // 1 USDC = 1,000,000 (6 decimals)
801            assert_eq!(amount, U256::from(1_000_000u64));
802        } else {
803            panic!("expected Transfer variant");
804        }
805    }
806
807    #[test]
808    fn test_parse_dai_approve() {
809        // DAI approve with unlimited amount
810        let data = hex::decode(
811            "095ea7b3\
812             0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d\
813             ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
814        )
815        .expect("valid hex");
816
817        let result = parse_erc20_call(&data);
818        assert!(result.is_some());
819
820        if let Some(Erc20Call::Approve { spender, amount }) = result {
821            // Uniswap V2 Router address
822            let expected_spender =
823                hex::decode("7a250d5630b4cf539739df2c5dacb4c659f2488d").expect("valid hex");
824            let expected_spender_arr: [u8; 20] = expected_spender.try_into().expect("20 bytes");
825            assert_eq!(spender, expected_spender_arr);
826            assert_eq!(amount, U256::MAX);
827        } else {
828            panic!("expected Approve variant");
829        }
830    }
831
832    // ========================================================================
833    // Edge Cases
834    // ========================================================================
835
836    #[test]
837    fn test_address_with_leading_zeros() {
838        // Address that starts with zeros
839        let data = hex::decode(
840            "a9059cbb\
841             0000000000000000000000000000000000000000000000000000000000000001\
842             0000000000000000000000000000000000000000000000000000000000000064",
843        )
844        .expect("valid hex");
845
846        let result = parse_erc20_call(&data);
847        assert!(result.is_some());
848
849        if let Some(Erc20Call::Transfer { to, amount }) = result {
850            // Address should be all zeros except last byte
851            let expected: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
852            assert_eq!(to, expected);
853            assert_eq!(amount, U256::from(100u64));
854        } else {
855            panic!("expected Transfer variant");
856        }
857    }
858
859    #[test]
860    fn test_exactly_minimum_length() {
861        // Exactly 68 bytes for transfer
862        let data = hex::decode(
863            "a9059cbb\
864             0000000000000000000000001234567890123456789012345678901234567890\
865             0000000000000000000000000000000000000000000000000000000000000001",
866        )
867        .expect("valid hex");
868        assert_eq!(data.len(), 68);
869
870        let result = parse_erc20_call(&data);
871        assert!(result.is_some());
872    }
873
874    #[test]
875    fn test_one_byte_short() {
876        // 67 bytes (one short of minimum)
877        let data = hex::decode(
878            "a9059cbb\
879             0000000000000000000000001234567890123456789012345678901234567890\
880             00000000000000000000000000000000000000000000000000000000000001",
881        )
882        .expect("valid hex");
883        assert_eq!(data.len(), 67);
884
885        assert!(parse_erc20_call(&data).is_none());
886    }
887
888    // ========================================================================
889    // Phase 2: Truncated Calldata Edge Cases
890    // ========================================================================
891
892    #[test]
893    fn should_return_none_when_transfer_calldata_has_partial_address() {
894        // Arrange: Transfer selector with calldata truncated in address field (36 bytes total)
895        let data = hex::decode(
896            "a9059cbb\
897             0000000000000000000000001234567890123456789012345678901234567890",
898        )
899        .expect("valid hex");
900        assert_eq!(data.len(), 36); // 4 + 32, but missing amount
901
902        // Act
903        let result = parse_erc20_call(&data);
904
905        // Assert
906        assert!(
907            result.is_none(),
908            "Should return None for incomplete transfer calldata"
909        );
910    }
911
912    #[test]
913    fn should_return_none_when_transfer_calldata_missing_partial_amount() {
914        // Arrange: Transfer selector with complete address but partial amount (52 bytes)
915        let data = hex::decode(
916            "a9059cbb\
917             0000000000000000000000001234567890123456789012345678901234567890\
918             00000000000000000000000000000000",
919        )
920        .expect("valid hex");
921        assert_eq!(data.len(), 52); // 4 + 32 + 16 (partial amount)
922
923        // Act
924        let result = parse_erc20_call(&data);
925
926        // Assert
927        assert!(
928            result.is_none(),
929            "Should return None when amount field is truncated"
930        );
931    }
932
933    #[test]
934    fn should_return_none_when_approve_calldata_truncated_before_amount() {
935        // Arrange: Approve selector with complete spender but missing amount (36 bytes)
936        let data = hex::decode(
937            "095ea7b3\
938             000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd",
939        )
940        .expect("valid hex");
941        assert_eq!(data.len(), 36); // 4 + 32, missing amount
942
943        // Act
944        let result = parse_erc20_call(&data);
945
946        // Assert
947        assert!(
948            result.is_none(),
949            "Should return None for approve missing amount"
950        );
951    }
952
953    #[test]
954    fn should_return_none_when_approve_calldata_has_partial_amount() {
955        // Arrange: Approve selector with complete spender but partial amount (60 bytes)
956        let data = hex::decode(
957            "095ea7b3\
958             000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd\
959             000000000000000000000000000000000000000000000000",
960        )
961        .expect("valid hex");
962        assert_eq!(data.len(), 60); // 4 + 32 + 24 (partial amount)
963
964        // Act
965        let result = parse_erc20_call(&data);
966
967        // Assert
968        assert!(
969            result.is_none(),
970            "Should return None when approve amount is truncated"
971        );
972    }
973
974    #[test]
975    fn should_return_none_when_transfer_from_calldata_missing_to_address() {
976        // Arrange: TransferFrom selector with only 'from' address (36 bytes)
977        let data = hex::decode(
978            "23b872dd\
979             0000000000000000000000001111111111111111111111111111111111111111",
980        )
981        .expect("valid hex");
982        assert_eq!(data.len(), 36); // 4 + 32, missing 'to' and amount
983
984        // Act
985        let result = parse_erc20_call(&data);
986
987        // Assert
988        assert!(
989            result.is_none(),
990            "Should return None when transferFrom missing 'to' and amount"
991        );
992    }
993
994    #[test]
995    fn should_return_none_when_transfer_from_calldata_missing_amount() {
996        // Arrange: TransferFrom selector with 'from' and 'to' but no amount (68 bytes)
997        let data = hex::decode(
998            "23b872dd\
999             0000000000000000000000001111111111111111111111111111111111111111\
1000             0000000000000000000000002222222222222222222222222222222222222222",
1001        )
1002        .expect("valid hex");
1003        assert_eq!(data.len(), 68); // 4 + 32 + 32, missing amount
1004
1005        // Act
1006        let result = parse_erc20_call(&data);
1007
1008        // Assert
1009        assert!(
1010            result.is_none(),
1011            "Should return None when transferFrom missing amount"
1012        );
1013    }
1014
1015    #[test]
1016    fn should_return_none_when_transfer_from_calldata_has_partial_to_address() {
1017        // Arrange: TransferFrom with 'from' and partial 'to' address (52 bytes)
1018        let data = hex::decode(
1019            "23b872dd\
1020             0000000000000000000000001111111111111111111111111111111111111111\
1021             00000000000000000000000000000000",
1022        )
1023        .expect("valid hex");
1024        assert_eq!(data.len(), 52); // 4 + 32 + 16 (partial 'to')
1025
1026        // Act
1027        let result = parse_erc20_call(&data);
1028
1029        // Assert
1030        assert!(
1031            result.is_none(),
1032            "Should return None when 'to' address is truncated"
1033        );
1034    }
1035
1036    #[test]
1037    fn should_return_none_when_transfer_from_calldata_has_partial_amount() {
1038        // Arrange: TransferFrom with complete addresses but partial amount (84 bytes)
1039        let data = hex::decode(
1040            "23b872dd\
1041             0000000000000000000000001111111111111111111111111111111111111111\
1042             0000000000000000000000002222222222222222222222222222222222222222\
1043             00000000000000000000000000000000",
1044        )
1045        .expect("valid hex");
1046        assert_eq!(data.len(), 84); // 4 + 32 + 32 + 16 (partial amount)
1047
1048        // Act
1049        let result = parse_erc20_call(&data);
1050
1051        // Assert
1052        assert!(
1053            result.is_none(),
1054            "Should return None when amount is truncated"
1055        );
1056    }
1057
1058    #[test]
1059    fn should_handle_valid_selector_with_parameter_extraction_failure() {
1060        // Arrange: Valid transfer selector but calldata too short for extraction
1061        let data = vec![0xa9, 0x05, 0x9c, 0xbb, 0x00, 0x00, 0x00, 0x00]; // 8 bytes
1062
1063        // Act
1064        let result = parse_erc20_call(&data);
1065
1066        // Assert
1067        assert!(
1068            result.is_none(),
1069            "Should fail gracefully when extraction impossible"
1070        );
1071    }
1072
1073    #[test]
1074    fn should_identify_all_transfer_variants_correctly() {
1075        // Arrange & Act & Assert: Transfer variant
1076        let transfer = Erc20Call::Transfer {
1077            to: [0xAA; 20],
1078            amount: U256::from(1000u64),
1079        };
1080        assert!(transfer.is_transfer());
1081        assert!(!transfer.is_approval());
1082
1083        // Arrange & Act & Assert: TransferFrom variant
1084        let transfer_from = Erc20Call::TransferFrom {
1085            from: [0xBB; 20],
1086            to: [0xCC; 20],
1087            amount: U256::from(2000u64),
1088        };
1089        assert!(transfer_from.is_transfer());
1090        assert!(!transfer_from.is_approval());
1091
1092        // Arrange & Act & Assert: Approve variant (not a transfer)
1093        let approve = Erc20Call::Approve {
1094            spender: [0xDD; 20],
1095            amount: U256::from(3000u64),
1096        };
1097        assert!(!approve.is_transfer());
1098    }
1099
1100    #[test]
1101    fn should_identify_all_approval_variants_correctly() {
1102        // Arrange & Act & Assert: Approve variant
1103        let approve = Erc20Call::Approve {
1104            spender: [0xEE; 20],
1105            amount: U256::MAX,
1106        };
1107        assert!(approve.is_approval());
1108        assert!(!approve.is_transfer());
1109
1110        // Arrange & Act & Assert: Transfer variant (not an approval)
1111        let transfer = Erc20Call::Transfer {
1112            to: [0xFF; 20],
1113            amount: U256::from(500u64),
1114        };
1115        assert!(!transfer.is_approval());
1116
1117        // Arrange & Act & Assert: TransferFrom variant (not an approval)
1118        let transfer_from = Erc20Call::TransferFrom {
1119            from: [0x11; 20],
1120            to: [0x22; 20],
1121            amount: U256::from(600u64),
1122        };
1123        assert!(!transfer_from.is_approval());
1124    }
1125
1126    // ========================================================================
1127    // Phase 2: Debug Trait Coverage for Erc20Call
1128    // ========================================================================
1129
1130    #[test]
1131    fn should_format_transfer_debug_output_correctly() {
1132        // Arrange: Transfer variant
1133        let transfer = Erc20Call::Transfer {
1134            to: [
1135                0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB,
1136                0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78,
1137            ],
1138            amount: U256::from(1_000_000u64),
1139        };
1140
1141        // Act: Format with Debug
1142        let debug_output = format!("{transfer:?}");
1143
1144        // Assert: Contains variant name and shows structure
1145        assert!(debug_output.contains("Transfer"));
1146        assert!(debug_output.contains("to"));
1147        assert!(debug_output.contains("amount"));
1148    }
1149
1150    #[test]
1151    fn should_format_approve_debug_output_correctly() {
1152        // Arrange: Approve variant with max amount
1153        let approve = Erc20Call::Approve {
1154            spender: [0xFF; 20],
1155            amount: U256::MAX,
1156        };
1157
1158        // Act: Format with Debug
1159        let debug_output = format!("{approve:?}");
1160
1161        // Assert: Contains variant name and field names
1162        assert!(debug_output.contains("Approve"));
1163        assert!(debug_output.contains("spender"));
1164        assert!(debug_output.contains("amount"));
1165    }
1166
1167    #[test]
1168    fn should_format_transfer_from_debug_output_correctly() {
1169        // Arrange: TransferFrom variant
1170        let transfer_from = Erc20Call::TransferFrom {
1171            from: [0xAA; 20],
1172            to: [0xBB; 20],
1173            amount: U256::from(500_000_000u64),
1174        };
1175
1176        // Act: Format with Debug
1177        let debug_output = format!("{transfer_from:?}");
1178
1179        // Assert: Contains variant name and all field names
1180        assert!(debug_output.contains("TransferFrom"));
1181        assert!(debug_output.contains("from"));
1182        assert!(debug_output.contains("to"));
1183        assert!(debug_output.contains("amount"));
1184    }
1185
1186    #[test]
1187    fn should_format_erc20call_debug_with_zero_amount() {
1188        // Arrange: Transfer with zero amount
1189        let transfer = Erc20Call::Transfer {
1190            to: [0x00; 20],
1191            amount: U256::ZERO,
1192        };
1193
1194        // Act: Format with Debug
1195        let debug_output = format!("{transfer:?}");
1196
1197        // Assert: Debug output generated
1198        assert!(debug_output.contains("Transfer"));
1199        assert!(!debug_output.is_empty());
1200    }
1201
1202    #[test]
1203    fn should_format_erc20call_debug_with_max_amount() {
1204        // Arrange: Approve with U256::MAX
1205        let approve = Erc20Call::Approve {
1206            spender: [0x00; 20],
1207            amount: U256::MAX,
1208        };
1209
1210        // Act: Format with Debug
1211        let debug_output = format!("{approve:?}");
1212
1213        // Assert: Debug output generated
1214        assert!(debug_output.contains("Approve"));
1215        assert!(!debug_output.is_empty());
1216    }
1217}