forge_script/
transaction.rs

1use super::ScriptResult;
2use alloy_dyn_abi::JsonAbiExt;
3use alloy_primitives::{hex, Address, TxKind, B256};
4use eyre::Result;
5use forge_script_sequence::TransactionWithMetadata;
6use foundry_common::{fmt::format_token_raw, ContractData, TransactionMaybeSigned, SELECTOR_LEN};
7use foundry_evm::traces::CallTraceDecoder;
8use itertools::Itertools;
9use revm_inspectors::tracing::types::CallKind;
10use std::collections::BTreeMap;
11
12#[derive(Debug)]
13pub struct ScriptTransactionBuilder {
14    transaction: TransactionWithMetadata,
15}
16
17impl ScriptTransactionBuilder {
18    pub fn new(transaction: TransactionMaybeSigned, rpc: String) -> Self {
19        let mut transaction = TransactionWithMetadata::from_tx_request(transaction);
20        transaction.rpc = rpc;
21        // If tx.gas is already set that means it was specified in script
22        transaction.is_fixed_gas_limit = transaction.tx().gas().is_some();
23
24        Self { transaction }
25    }
26
27    /// Populate the transaction as CALL tx
28    pub fn set_call(
29        &mut self,
30        local_contracts: &BTreeMap<Address, &ContractData>,
31        decoder: &CallTraceDecoder,
32        create2_deployer: Address,
33    ) -> Result<()> {
34        if let Some(TxKind::Call(to)) = self.transaction.transaction.to() {
35            if to == create2_deployer {
36                if let Some(input) = self.transaction.transaction.input() {
37                    let (salt, init_code) = input.split_at(32);
38
39                    self.set_create(
40                        true,
41                        create2_deployer.create2_from_code(B256::from_slice(salt), init_code),
42                        local_contracts,
43                    )?;
44                }
45            } else {
46                self.transaction.opcode = CallKind::Call;
47                self.transaction.contract_address = Some(to);
48
49                let Some(data) = self.transaction.transaction.input() else { return Ok(()) };
50
51                if data.len() < SELECTOR_LEN {
52                    return Ok(());
53                }
54
55                let (selector, data) = data.split_at(SELECTOR_LEN);
56
57                let function = if let Some(info) = local_contracts.get(&to) {
58                    // This CALL is made to a local contract.
59                    self.transaction.contract_name = Some(info.name.clone());
60                    info.abi.functions().find(|function| function.selector() == selector)
61                } else {
62                    // This CALL is made to an external contract; try to decode it from the given
63                    // decoder.
64                    decoder.functions.get(selector).and_then(|v| v.first())
65                };
66
67                if let Some(function) = function {
68                    self.transaction.function = Some(function.signature());
69
70                    let values = function.abi_decode_input(data, false).inspect_err(|_| {
71                        error!(
72                            contract=?self.transaction.contract_name,
73                            signature=?function,
74                            data=hex::encode(data),
75                            "Failed to decode function arguments",
76                        );
77                    })?;
78                    self.transaction.arguments =
79                        Some(values.iter().map(format_token_raw).collect());
80                }
81            }
82        }
83
84        Ok(())
85    }
86
87    /// Populate the transaction as CREATE tx
88    ///
89    /// If this is a CREATE2 transaction this attempt to decode the arguments from the CREATE2
90    /// deployer's function
91    pub fn set_create(
92        &mut self,
93        is_create2: bool,
94        address: Address,
95        contracts: &BTreeMap<Address, &ContractData>,
96    ) -> Result<()> {
97        if is_create2 {
98            self.transaction.opcode = CallKind::Create2;
99        } else {
100            self.transaction.opcode = CallKind::Create;
101        }
102
103        let info = contracts.get(&address);
104        self.transaction.contract_name = info.map(|info| info.name.clone());
105        self.transaction.contract_address = Some(address);
106
107        let Some(data) = self.transaction.transaction.input() else { return Ok(()) };
108        let Some(info) = info else { return Ok(()) };
109        let Some(bytecode) = info.bytecode() else { return Ok(()) };
110
111        // `create2` transactions are prefixed by a 32 byte salt.
112        let creation_code = if is_create2 {
113            if data.len() < 32 {
114                return Ok(())
115            }
116            &data[32..]
117        } else {
118            data
119        };
120
121        // The constructor args start after bytecode.
122        let contains_constructor_args = creation_code.len() > bytecode.len();
123        if !contains_constructor_args {
124            return Ok(());
125        }
126        let constructor_args = &creation_code[bytecode.len()..];
127
128        let Some(constructor) = info.abi.constructor() else { return Ok(()) };
129        let values = constructor.abi_decode_input(constructor_args, false).inspect_err(|_| {
130                error!(
131                    contract=?self.transaction.contract_name,
132                    signature=%format!("constructor({})", constructor.inputs.iter().map(|p| &p.ty).format(",")),
133                    is_create2,
134                    constructor_args=%hex::encode(constructor_args),
135                    "Failed to decode constructor arguments",
136                );
137                debug!(full_data=%hex::encode(data), bytecode=%hex::encode(creation_code));
138            })?;
139        self.transaction.arguments = Some(values.iter().map(format_token_raw).collect());
140
141        Ok(())
142    }
143
144    /// Populates additional data from the transaction execution result.
145    pub fn with_execution_result(
146        mut self,
147        result: &ScriptResult,
148        gas_estimate_multiplier: u64,
149    ) -> Self {
150        let mut created_contracts = result.get_created_contracts();
151
152        // Add the additional contracts created in this transaction, so we can verify them later.
153        created_contracts.retain(|contract| {
154            // Filter out the contract that was created by the transaction itself.
155            self.transaction.contract_address != Some(contract.address)
156        });
157
158        self.transaction.additional_contracts = created_contracts;
159
160        if !self.transaction.is_fixed_gas_limit {
161            if let Some(unsigned) = self.transaction.transaction.as_unsigned_mut() {
162                // We inflate the gas used by the user specified percentage
163                unsigned.gas = Some(result.gas_used * gas_estimate_multiplier / 100);
164            }
165        }
166
167        self
168    }
169
170    pub fn build(self) -> TransactionWithMetadata {
171        self.transaction
172    }
173}
174
175impl From<TransactionWithMetadata> for ScriptTransactionBuilder {
176    fn from(transaction: TransactionWithMetadata) -> Self {
177        Self { transaction }
178    }
179}