Skip to main content

tycho_simulation/evm/protocol/vm/
utils.rs

1use std::{collections::HashMap, env, fmt::Debug, str::FromStr};
2
3use alloy::{
4    primitives::{Address, Bytes, Keccak256, U256},
5    providers::{Provider, ProviderBuilder},
6    sol_types::SolValue,
7    transports::{RpcError, TransportErrorKind},
8};
9use hex::FromHex;
10use num_bigint::BigInt;
11use revm::{
12    state::{AccountInfo, Bytecode},
13    DatabaseRef,
14};
15use serde_json::Value;
16use tycho_common::simulation::errors::SimulationError;
17
18use crate::evm::{
19    engine_db::engine_db_interface::EngineDatabaseInterface,
20    simulation::{SimulationEngine, SimulationEngineError, SimulationParameters},
21    ContractCompiler, SlotId,
22};
23
24pub(crate) fn coerce_error(
25    err: &SimulationEngineError,
26    pool_state: &str,
27    gas_limit: Option<u64>,
28) -> SimulationError {
29    match err {
30        // Check for revert situation (if error message starts with "0x")
31        SimulationEngineError::TransactionError { ref data, ref gas_used }
32            if data.starts_with("0x") =>
33        {
34            let reason = parse_solidity_error_message(data);
35            let err = SimulationEngineError::TransactionError {
36                data: format!("Revert! Reason: {reason}"),
37                gas_used: *gas_used,
38            };
39
40            // Check if we are running out of gas
41            if let (Some(gas_limit), Some(gas_used)) = (gas_limit, gas_used) {
42                // if we used up 97% or more issue a OutOfGas error.
43                let usage = *gas_used as f64 / gas_limit as f64;
44                if usage >= 0.97 {
45                    return SimulationError::InvalidInput(
46                        format!(
47                            "SimulationError: Likely out-of-gas. Used: {:.2}% of gas limit. \
48                            Original error: {}. \
49                            Pool state: {}",
50                            usage * 100.0,
51                            err,
52                            pool_state,
53                        ),
54                        None,
55                    );
56                }
57            }
58            SimulationError::FatalError(format!("Simulation reverted for unknown reason: {reason}"))
59        }
60        // Check if "OutOfGas" is part of the error message
61        SimulationEngineError::TransactionError { ref data, ref gas_used }
62            if data.contains("OutOfGas") =>
63        {
64            let usage_msg = if let (Some(gas_limit), Some(gas_used)) = (gas_limit, gas_used) {
65                let usage = *gas_used as f64 / gas_limit as f64;
66                format!("Used: {:.2}% of gas limit. ", usage * 100.0)
67            } else {
68                String::new()
69            };
70
71            SimulationError::InvalidInput(
72                format!(
73                    "SimulationError: out-of-gas. {usage_msg} Original error: {data}. Pool state: {pool_state}"
74                ),
75                None,
76            )
77        }
78        SimulationEngineError::TransactionError { ref data, .. } => {
79            SimulationError::FatalError(format!("TransactionError: {data}"))
80        }
81        SimulationEngineError::StorageError(message) => {
82            SimulationError::RecoverableError(message.clone())
83        }
84        _ => SimulationError::FatalError(err.clone().to_string()), /* Otherwise return the
85                                                                    * original error */
86    }
87}
88
89fn parse_solidity_error_message(data: &str) -> String {
90    // 10 for "0x" + 8 hex chars error signature
91    if data.len() >= 10 {
92        let data_bytes = match Vec::from_hex(&data[2..]) {
93            Ok(bytes) => bytes,
94            Err(_) => return format!("Failed to decode: {data}"),
95        };
96
97        // Check for specific error selectors:
98        // Solidity Error(string) signature: 0x08c379a0
99        if data_bytes.starts_with(&[0x08, 0xc3, 0x79, 0xa0]) {
100            if let Ok(decoded) = String::abi_decode(&data_bytes[4..]) {
101                return decoded;
102            }
103
104            // Solidity Panic(uint256) signature: 0x4e487b71
105        } else if data_bytes.starts_with(&[0x4e, 0x48, 0x7b, 0x71]) {
106            if let Ok(decoded) = U256::abi_decode(&data_bytes[4..]) {
107                let panic_codes = get_solidity_panic_codes();
108                return panic_codes
109                    .get(&decoded.as_limbs()[0])
110                    .cloned()
111                    .unwrap_or_else(|| format!("Panic({decoded})"));
112            }
113        }
114
115        // Try decoding as a string (old Solidity revert case)
116        if let Ok(decoded) = String::abi_decode(&data_bytes) {
117            return decoded;
118        }
119
120        // Custom error, try to decode string again with offset
121        if let Ok(decoded) = String::abi_decode(&data_bytes[4..]) {
122            return decoded;
123        }
124    }
125    // Fallback if no decoding succeeded
126    format!("Failed to decode: {data}")
127}
128
129/// Get storage slot index of a value stored at a certain key in a mapping
130///
131/// # Arguments
132///
133/// * `key`: Key in a mapping. Can be any H160 value (such as an address).
134/// * `mapping_slot`: An `U256` representing the storage slot at which the mapping itself is stored.
135///   See the examples for more explanation.
136/// * `compiler`: The compiler with which the target contract was compiled. Solidity and Vyper
137///   handle maps differently.
138///
139/// # Returns
140///
141/// An `U256` representing the  index of a storage slot where the value at the given
142/// key is stored.
143///
144/// # Examples
145///
146/// If a mapping is declared as a first variable in Solidity code, its storage slot
147/// is 0 (e.g. `balances` in our mocked ERC20 contract). Here's how to compute
148/// a storage slot where balance of a given account is stored:
149///
150/// ```
151/// use alloy::primitives::{U256, Address};
152/// use tycho_simulation::evm::ContractCompiler;
153/// use tycho_simulation::evm::protocol::vm::utils::get_storage_slot_index_at_key;
154/// let address: Address = "0xC63135E4bF73F637AF616DFd64cf701866BB2628".parse().expect("Invalid address");
155/// get_storage_slot_index_at_key(address, U256::from(0), ContractCompiler::Solidity);
156/// ```
157///
158/// For nested mappings, we need to apply the function twice. An example of this is
159/// `allowances` in ERC20. It is a mapping of form:
160/// `HashMap<Owner, HashMap<Spender, U256>>`. In our mocked ERC20 contract, `allowances`
161/// is a second variable, so it is stored at slot 1. Here's how to get a storage slot
162/// where an allowance of `address_spender` to spend `address_owner`'s money is stored:
163///
164/// ```
165/// use alloy::primitives::{U256, Address};
166/// use tycho_simulation::evm::ContractCompiler;
167/// use tycho_simulation::evm::protocol::vm::utils::get_storage_slot_index_at_key;
168/// let address_spender: Address = "0xC63135E4bF73F637AF616DFd64cf701866BB2628".parse().expect("Invalid address");
169/// let address_owner: Address = "0x6F4Feb566b0f29e2edC231aDF88Fe7e1169D7c05".parse().expect("Invalid address");
170/// get_storage_slot_index_at_key(address_spender, get_storage_slot_index_at_key(address_owner, U256::from(1), ContractCompiler::Solidity), ContractCompiler::Solidity);
171/// ```
172///
173/// # See Also
174///
175/// [Solidity Storage Layout documentation](https://docs.soliditylang.org/en/v0.8.13/internals/layout_in_storage.html#mappings-and-dynamic-arrays)
176pub fn get_storage_slot_index_at_key(
177    key: Address,
178    mapping_slot: SlotId,
179    compiler: ContractCompiler,
180) -> SlotId {
181    let mut key_bytes = key.as_slice().to_vec();
182    if key_bytes.len() < 32 {
183        let padding = vec![0u8; 32 - key_bytes.len()];
184        key_bytes.splice(0..0, padding); // Prepend zeros to the start
185    }
186
187    let mapping_slot_bytes: [u8; 32] = mapping_slot.to_be_bytes();
188    compiler.compute_map_slot(&mapping_slot_bytes, &key_bytes)
189}
190
191fn get_solidity_panic_codes() -> HashMap<u64, String> {
192    let mut panic_codes = HashMap::new();
193    panic_codes.insert(0, "GenericCompilerPanic".to_string());
194    panic_codes.insert(1, "AssertionError".to_string());
195    panic_codes.insert(17, "ArithmeticOver/Underflow".to_string());
196    panic_codes.insert(18, "ZeroDivisionError".to_string());
197    panic_codes.insert(33, "UnknownEnumMember".to_string());
198    panic_codes.insert(34, "BadStorageByteArrayEncoding".to_string());
199    panic_codes.insert(51, "EmptyArray".to_string());
200    panic_codes.insert(0x32, "OutOfBounds".to_string());
201    panic_codes.insert(0x41, "OutOfMemory".to_string());
202    panic_codes.insert(0x51, "BadFunctionPointer".to_string());
203    panic_codes
204}
205
206/// Fetches the bytecode for a specified contract address, returning an error if the address is
207/// an Externally Owned Account (EOA) or if no code is associated with it.
208///
209/// This function checks the specified address on the blockchain, attempting to retrieve any
210/// contract bytecode deployed at that address. If the address corresponds to an EOA or any
211/// other address without associated bytecode, an `RpcError::EmptyResponse` error is returned.
212///
213/// # Parameters
214/// - `address`: The address of the account or contract to query, as a string.
215/// - `connection_string`: An optional RPC connection string. If not provided, the function will
216///   default to the `RPC_URL` environment variable.
217///
218/// # Returns
219/// - `Ok(Bytecode)`: The bytecode of the contract at the specified address, if present.
220/// - `Err(RpcError)`: An error if the address does not have associated bytecode, if there is an
221///   issue with the RPC connection, or if the address is invalid.
222///
223/// # Errors
224/// - Returns `RpcError::InvalidRequest` if `address` is not parsable or if no RPC URL is set.
225/// - Returns `RpcError::EmptyResponse` if the address has no associated bytecode (e.g., EOA).
226/// - Returns `RpcError::InvalidResponse` for issues with the RPC provider response.
227pub(crate) async fn get_code_for_contract(
228    address: &str,
229    connection_string: Option<String>,
230) -> Result<Bytecode, SimulationError> {
231    // Get the connection string, defaulting to the RPC_URL environment variable
232    let connection_string = connection_string.or_else(|| env::var("RPC_URL").ok());
233
234    let connection_string = match connection_string {
235        Some(url) => url,
236        None => {
237            return Err(SimulationError::FatalError(
238                "RPC_URL environment variable is not set".to_string(),
239            ))
240        }
241    };
242
243    let addr = Address::from_str(address)
244        .map_err(|_| SimulationError::FatalError(format!("Invalid address format: {address}")))?;
245    // Call eth_getCode to get the bytecode of the contract
246    match sync_get_code(&connection_string, addr) {
247        Ok(code) if code.is_empty() => {
248            Err(SimulationError::FatalError("Empty code response from RPC".to_string()))
249        }
250        Ok(code) => {
251            let bytecode = Bytecode::new_raw(Bytes::from(code.to_vec()));
252            Ok(bytecode)
253        }
254        Err(e) => match e {
255            RpcError::Transport(err) => Err(SimulationError::RecoverableError(format!(
256                "Failed to get code for contract due to internal RPC error: {err:?}"
257            ))),
258            _ => Err(SimulationError::FatalError(format!(
259                "Failed to get code for contract. Invalid response from RPC: {e:?}"
260            ))),
261        },
262    }
263}
264
265fn sync_get_code(
266    connection_string: &str,
267    addr: Address,
268) -> Result<Bytes, RpcError<TransportErrorKind>> {
269    tokio::task::block_in_place(|| {
270        tokio::runtime::Handle::current().block_on(async {
271            // Create a provider with the URL
272            let provider = ProviderBuilder::new()
273                .connect(connection_string)
274                .await?;
275            provider.get_code_at(addr).await
276        })
277    })
278}
279
280/// Converts a hexadecimal string into a fixed-size 32-byte array.
281///
282/// This function takes a string slice (e.g., a pool ID) that may or may not have
283/// a `0x` prefix. It decodes the hex string into bytes, ensuring it does not exceed
284/// 32 bytes in length. If the string is valid and fits within 32 bytes, the bytes
285/// are copied into a `[u8; 32]` array, with right zero-padding for unused bytes.
286///
287/// # Arguments
288///
289/// * `pool_id` - A string slice representing a hexadecimal pool ID. It can optionally start with
290///   the `0x` prefix.
291///
292/// # Returns
293///
294/// * `Ok([u8; 32])` - On success, returns a 32-byte array with the decoded bytes. If the input is
295///   shorter than 32 bytes, the rest of the array is right padded with zeros.
296/// * `Err(SimulationError)` - Returns an error if:
297///     - The input string is not a valid hexadecimal string.
298///     - The decoded bytes exceed 32 bytes in length.
299///
300/// # Example
301/// ```
302/// use tycho_simulation::evm::protocol::vm::utils::string_to_bytes32;
303///
304/// let pool_id = "0x1234abcd";
305/// match string_to_bytes32(pool_id) {
306///     Ok(bytes32) => println!("Bytes32: {:?}", bytes32),
307///     Err(e) => eprintln!("Error: {}", e),
308/// }
309pub fn string_to_bytes32(pool_id: &str) -> Result<[u8; 32], SimulationError> {
310    let pool_id_no_prefix =
311        if let Some(stripped) = pool_id.strip_prefix("0x") { stripped } else { pool_id };
312    let bytes = hex::decode(pool_id_no_prefix)
313        .map_err(|e| SimulationError::FatalError(format!("Invalid hex string: {e}")))?;
314    if bytes.len() > 32 {
315        return Err(SimulationError::FatalError(format!(
316            "Hex string exceeds 32 bytes: length {}",
317            bytes.len()
318        )));
319    }
320    let mut array = [0u8; 32];
321    array[..bytes.len()].copy_from_slice(&bytes);
322    Ok(array)
323}
324
325/// Decodes a JSON-encoded list of hexadecimal strings into a `Vec<Vec<u8>>`.
326///
327/// This function parses a JSON array where each element is a string representing a hexadecimal
328/// value. It converts each hex string into a vector of bytes (`Vec<u8>`), and aggregates them into
329/// a `Vec<Vec<u8>>`.
330///
331/// # Arguments
332///
333/// * `input` - A byte slice (`&[u8]`) containing JSON-encoded data. The JSON must be a valid array
334///   of hex strings.
335///
336/// # Returns
337///
338/// * `Ok(Vec<Vec<u8>>)` - On success, returns a vector of byte vectors.
339/// * `Err(SimulationError)` - Returns an error if:
340///     - The input is not valid JSON.
341///     - The JSON is not an array.
342///     - Any array element is not a string.
343///     - Any string is not a valid hexadecimal string.
344///
345/// # Example
346/// ```
347/// use tycho_simulation::evm::protocol::vm::utils::json_deserialize_address_list;
348///
349/// let json_input = br#"["0x1234", "0xc0ffee"]"#;
350/// match json_deserialize_address_list(json_input) {
351///     Ok(result) => println!("Decoded: {:?}", result),
352///     Err(e) => eprintln!("Error: {}", e),
353/// }
354/// ```
355pub fn json_deserialize_address_list(input: &[u8]) -> Result<Vec<Vec<u8>>, SimulationError> {
356    let json_value: Value = serde_json::from_slice(input)
357        .map_err(|_| SimulationError::FatalError(format!("Invalid JSON: {input:?}")))?;
358
359    if let Value::Array(hex_strings) = json_value {
360        let mut result = Vec::new();
361
362        for val in hex_strings {
363            if let Value::String(hexstring) = val {
364                let bytes = hex::decode(hexstring.trim_start_matches("0x")).map_err(|_| {
365                    SimulationError::FatalError(format!("Invalid hex string: {hexstring}"))
366                })?;
367                result.push(bytes);
368            } else {
369                return Err(SimulationError::FatalError("Array contains a non-string value".into()));
370            }
371        }
372
373        Ok(result)
374    } else {
375        Err(SimulationError::FatalError("Input is not a JSON array".into()))
376    }
377}
378
379/// Decodes a JSON-encoded list of hexadecimal strings into a `Vec<BigInt>`.
380///
381/// This function parses a JSON array where each element is a string representing a hexadecimal
382/// value. It converts each hex string into a `BigInt` using big-endian byte interpretation, and
383/// aggregates them into a `Vec<BigInt>`.
384///
385/// # Arguments
386///
387/// * `input` - A byte slice (`&[u8]`) containing JSON-encoded data. The JSON must be a valid array
388///   of hex strings.
389///
390/// # Returns
391///
392/// * `Ok(Vec<BigInt>)` - On success, returns a vector of `BigInt` values.
393/// * `Err(SimulationError)` - Returns an error if:
394///     - The input is not valid JSON.
395///     - The JSON is not an array.
396///     - Any array element is not a string.
397///     - Any string is not a valid hexadecimal string.
398///
399/// # Example
400/// ```
401/// use tycho_simulation::evm::protocol::vm::utils::json_deserialize_be_bigint_list;
402/// use num_bigint::BigInt;
403/// use tycho_simulation::evm;
404/// let json_input = br#"["0x1234", "0xdeadbeef"]"#;
405/// match json_deserialize_be_bigint_list(json_input) {
406///     Ok(result) => println!("Decoded BigInts: {:?}", result),
407///     Err(e) => eprintln!("Error: {}", e),
408/// }
409/// ```
410pub fn json_deserialize_be_bigint_list(input: &[u8]) -> Result<Vec<BigInt>, SimulationError> {
411    let json_value: Value = serde_json::from_slice(input)
412        .map_err(|_| SimulationError::FatalError(format!("Invalid JSON: {input:?}")))?;
413
414    if let Value::Array(hex_strings) = json_value {
415        let mut result = Vec::new();
416
417        for val in hex_strings {
418            if let Value::String(hexstring) = val {
419                let bytes = hex::decode(hexstring.trim_start_matches("0x")).map_err(|_| {
420                    SimulationError::FatalError(format!("Invalid hex string: {hexstring}"))
421                })?;
422                let bigint = BigInt::from_signed_bytes_be(&bytes);
423                result.push(bigint);
424            } else {
425                return Err(SimulationError::FatalError("Array contains a non-string value".into()));
426            }
427        }
428
429        Ok(result)
430    } else {
431        Err(SimulationError::FatalError("Input is not a JSON array".into()))
432    }
433}
434
435/// Load the pool's stateless/implementation contracts (the `stateless_contract_addr_{i}` state
436/// attributes) into the engine's DB so getter calls on proxy pools resolve their delegatecall
437/// targets. Curve pools are commonly EIP-1167 proxies whose implementation code is not part of the
438/// indexed pool storage; without this, getters delegatecall into empty code and revert.
439///
440/// Addresses may be static (`0x…`) or dynamic (`call:0x<factory>:method()`), and code is fetched
441/// via RPC unless provided inline as `stateless_contract_code_{i}`. The engine shares its DB with
442/// `SHARED_TYCHO_DB`, so accounts loaded here persist for later `delta_transition` rebuilds.
443pub(crate) async fn load_stateless_contracts<D: EngineDatabaseInterface + Clone + Debug>(
444    engine: &SimulationEngine<D>,
445    attributes: &HashMap<String, tycho_common::Bytes>,
446) -> Result<(), SimulationError>
447where
448    <D as DatabaseRef>::Error: Debug,
449    <D as EngineDatabaseInterface>::Error: Debug,
450{
451    let mut index = 0;
452    while let Some(encoded) = attributes.get(&format!("stateless_contract_addr_{index}")) {
453        let address = String::from_utf8(encoded.to_vec()).map_err(|e| {
454            SimulationError::FatalError(format!("stateless contract address is not UTF-8: {e}"))
455        })?;
456        let inline_code = attributes
457            .get(&format!("stateless_contract_code_{index}"))
458            .map(|value| value.to_vec());
459        index += 1;
460
461        let (account, code) = match inline_code {
462            Some(bytecode) => (address, Bytecode::new_raw(bytecode.into())),
463            None => {
464                let resolved = if address.starts_with("call") {
465                    resolve_call_address(engine, &address)?
466                } else {
467                    address
468                };
469                let code = get_code_for_contract(&resolved, None).await?;
470                (resolved, code)
471            }
472        };
473        let account: Address = account.parse().map_err(|_| {
474            SimulationError::FatalError(format!(
475                "stateless contract has an invalid address {account}"
476            ))
477        })?;
478        engine
479            .state
480            .init_account(
481                account,
482                AccountInfo {
483                    balance: U256::ZERO,
484                    nonce: 0,
485                    code_hash: code.hash_slow(),
486                    code: Some(code),
487                },
488                None,
489                false,
490            )
491            .map_err(|e| {
492                SimulationError::FatalError(format!(
493                    "stateless contract init_account failed: {e:?}"
494                ))
495            })?;
496    }
497    Ok(())
498}
499
500/// Resolve a dynamic `call:0x<address>:method()` directive to a concrete implementation address by
501/// simulating the parameterless `method()` view and decoding its returned address.
502pub(crate) fn resolve_call_address<D: EngineDatabaseInterface + Clone + Debug>(
503    engine: &SimulationEngine<D>,
504    directive: &str,
505) -> Result<String, SimulationError>
506where
507    <D as DatabaseRef>::Error: Debug,
508    <D as EngineDatabaseInterface>::Error: Debug,
509{
510    let method = directive
511        .split(':')
512        .next_back()
513        .ok_or_else(|| {
514            SimulationError::FatalError(format!("malformed stateless call directive {directive}"))
515        })?;
516    let to: Address = directive
517        .split(':')
518        .nth(1)
519        .ok_or_else(|| {
520            SimulationError::FatalError(format!(
521                "stateless call directive is missing its target {directive}"
522            ))
523        })?
524        .parse()
525        .map_err(|_| {
526            SimulationError::FatalError(format!(
527                "stateless call directive has an invalid target {directive}"
528            ))
529        })?;
530    let mut hasher = Keccak256::new();
531    hasher.update(method.as_bytes());
532    let selector = hasher.finalize()[..4].to_vec();
533    let res = engine
534        .simulate(&SimulationParameters {
535            caller: Address::ZERO,
536            to,
537            data: selector,
538            value: U256::ZERO,
539            overrides: None,
540            gas_limit: None,
541            transient_storage: None,
542            block_overrides: None,
543        })
544        .map_err(|e| SimulationError::FatalError(format!("stateless call failed: {e}")))?;
545    let address = Address::abi_decode(res.result.as_ref())
546        .map_err(|e| SimulationError::FatalError(format!("stateless call decode failed: {e}")))?;
547    Ok(address.to_string())
548}
549
550#[cfg(test)]
551mod tests {
552    use dotenv::dotenv;
553
554    use super::*;
555    use crate::utils::hexstring_to_vec;
556
557    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
558    #[cfg_attr(not(feature = "network_tests"), ignore)]
559    async fn test_get_code_for_address() {
560        let rpc_url = env::var("RPC_URL").unwrap_or_else(|_| {
561            dotenv().expect("Missing .env file");
562            env::var("RPC_URL").expect("Missing RPC_URL in .env file")
563        });
564
565        let address = "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640";
566        let result = get_code_for_contract(address, Some(rpc_url)).await;
567
568        assert!(result.is_ok(), "Network call should not fail");
569
570        let code = result.unwrap();
571        assert!(!code.bytes().is_empty(), "Code should not be empty");
572    }
573
574    #[test]
575    fn test_maybe_coerce_error_revert_no_gas_info() {
576        let err = SimulationEngineError::TransactionError{
577            data: "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000011496e76616c6964206f7065726174696f6e000000000000000000000000000000".to_string(),
578            gas_used: None
579        };
580
581        let result = coerce_error(&err, "test_pool", None);
582
583        if let SimulationError::FatalError(msg) = result {
584            assert!(msg.contains("Simulation reverted for unknown reason: Invalid operation"));
585        } else {
586            panic!("Expected SolidityError error");
587        }
588    }
589
590    #[test]
591    fn test_maybe_coerce_error_out_of_gas() {
592        // Test out-of-gas situation with gas limit and gas used provided
593        let err = SimulationEngineError::TransactionError{
594            data: "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000011496e76616c6964206f7065726174696f6e000000000000000000000000000000".to_string(),
595            gas_used: Some(980)
596        };
597
598        let result = coerce_error(&err, "test_pool", Some(1000));
599
600        if let SimulationError::InvalidInput(message, _partial_result) = result {
601            assert!(message.contains("Used: 98.00% of gas limit."));
602            assert!(message.contains("test_pool"));
603        } else {
604            panic!("Expected OutOfGas error");
605        }
606    }
607
608    #[test]
609    fn test_maybe_coerce_error_no_gas_limit_info() {
610        // Test out-of-gas situation without gas limit info
611        let err = SimulationEngineError::TransactionError {
612            data: "OutOfGas".to_string(),
613            gas_used: None,
614        };
615
616        let result = coerce_error(&err, "test_pool", None);
617
618        if let SimulationError::InvalidInput(message, _partial_result) = result {
619            assert!(message.contains("Original error: OutOfGas"));
620            assert!(message.contains("Pool state: test_pool"));
621        } else {
622            panic!("Expected RetryDifferentInput error");
623        }
624    }
625
626    #[test]
627    fn test_maybe_coerce_error_storage_error() {
628        let err = SimulationEngineError::StorageError("Storage error:".to_string());
629
630        let result = coerce_error(&err, "test_pool", None);
631
632        if let SimulationError::RecoverableError(message) = result {
633            assert_eq!(message, "Storage error:");
634        } else {
635            println!("{result:?}");
636            panic!("Expected RetryLater error");
637        }
638    }
639
640    #[test]
641    fn test_maybe_coerce_error_no_match() {
642        // Test for non-revert, non-out-of-gas, non-storage errors
643        let err = SimulationEngineError::TransactionError {
644            data: "Some other error".to_string(),
645            gas_used: None,
646        };
647
648        let result = coerce_error(&err, "test_pool", None);
649
650        if let SimulationError::FatalError(message) = result {
651            assert_eq!(message, "TransactionError: Some other error");
652        } else {
653            panic!("Expected solidity error");
654        }
655    }
656
657    #[test]
658    fn test_parse_solidity_error_message_error_string() {
659        // Test parsing Solidity Error(string) message
660        let data = "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e416d6f756e7420746f6f206c6f77000000000000000000000000000000000000";
661
662        let result = parse_solidity_error_message(data);
663
664        assert_eq!(result, "Amount too low");
665    }
666
667    #[test]
668    fn test_parse_solidity_error_message_panic_code() {
669        // Test parsing Solidity Panic(uint256) message
670        let data = "0x4e487b710000000000000000000000000000000000000000000000000000000000000001";
671
672        let result = parse_solidity_error_message(data);
673
674        assert_eq!(result, "AssertionError");
675    }
676
677    #[test]
678    fn test_parse_solidity_error_message_failed_to_decode() {
679        // Test failed decoding with invalid data
680        let data = "0x1234567890";
681
682        let result = parse_solidity_error_message(data);
683
684        assert!(result.contains("Failed to decode"));
685    }
686
687    #[test]
688    fn test_hexstring_to_vec() {
689        let hexstring = "0x68656c6c6f";
690        let expected = vec![0x68, 0x65, 0x6c, 0x6c, 0x6f];
691        let result = hexstring_to_vec(hexstring).unwrap();
692        assert_eq!(result, expected);
693    }
694
695    #[test]
696    fn test_hexstring_to_vec_no_prefix() {
697        let hexstring = "68656c6c6f";
698        let expected = vec![0x68, 0x65, 0x6c, 0x6c, 0x6f];
699        let result = hexstring_to_vec(hexstring).unwrap();
700        assert_eq!(result, expected);
701    }
702
703    #[test]
704    fn test_hexstring_to_vec_invalid_characters() {
705        let hexstring = "0x68656c6c6z"; // Invalid character 'z'
706        let result = hexstring_to_vec(hexstring);
707        assert!(result.is_err());
708        if let Err(SimulationError::FatalError(msg)) = result {
709            assert!(msg.contains("Invalid hex string"));
710        } else {
711            panic!("Expected EncodingError");
712        }
713    }
714
715    #[test]
716    fn test_json_deserialize_address_list() {
717        let json_input = r#"["0x1234","0xabcd"]"#.as_bytes();
718        let result = json_deserialize_address_list(json_input).unwrap();
719        assert_eq!(result, vec![vec![0x12, 0x34], vec![0xab, 0xcd]]);
720    }
721
722    #[test]
723    fn test_json_deserialize_bigint_list() {
724        let json_input = r#"["0x0b1a2bc2ec500000","0x02c68af0bb140000"]"#.as_bytes();
725        let result = json_deserialize_be_bigint_list(json_input).unwrap();
726        assert_eq!(
727            result,
728            vec![BigInt::from(800000000000000000u64), BigInt::from(200000000000000000u64)]
729        );
730    }
731
732    #[test]
733    fn test_invalid_deserialize_address_list() {
734        let json_input = r#"["invalid_hex"]"#.as_bytes();
735        let result = json_deserialize_address_list(json_input);
736        assert!(result.is_err());
737    }
738
739    #[test]
740    fn test_invalid_deserialize_bigint_list() {
741        let json_input = r#"["invalid_hex"]"#.as_bytes();
742        let result = json_deserialize_be_bigint_list(json_input);
743        assert!(result.is_err());
744    }
745}