forge_script/
transaction.rs

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