Skip to main content

forge_script/
transaction.rs

1use super::ScriptResult;
2use crate::build::LinkedBuildData;
3use alloy_dyn_abi::JsonAbiExt;
4use alloy_network::{Network, TransactionBuilder};
5use alloy_primitives::{Address, B256, Selector, hex};
6use eyre::Result;
7use forge_script_sequence::TransactionWithMetadata;
8use foundry_common::{ContractData, SELECTOR_LEN, TransactionMaybeSigned, fmt::format_token_raw};
9use foundry_evm::traces::CallTraceDecoder;
10use itertools::Itertools;
11use revm_inspectors::tracing::types::CallKind;
12use std::collections::BTreeMap;
13
14#[derive(Debug)]
15pub struct ScriptTransactionBuilder<N: Network> {
16    transaction: TransactionWithMetadata<N>,
17}
18
19impl<N: Network> ScriptTransactionBuilder<N> {
20    pub fn new(transaction: TransactionMaybeSigned<N>, rpc: String) -> Self {
21        let mut transaction = TransactionWithMetadata::from_tx_request(transaction);
22        transaction.rpc = rpc;
23        // If tx.gas is already set that means it was specified in script
24        transaction.is_fixed_gas_limit = transaction.tx().gas().is_some();
25
26        Self { transaction }
27    }
28
29    /// Populate the transaction as CALL tx
30    pub fn set_call(
31        &mut self,
32        local_contracts: &BTreeMap<Address, &ContractData>,
33        decoder: &CallTraceDecoder,
34        create2_deployer: Address,
35    ) -> Result<()> {
36        if let Some(to) = self.transaction.transaction.to() {
37            if to == create2_deployer {
38                if let Some(input) = self.transaction.transaction.input() {
39                    let (salt, init_code) = input.split_at(32);
40
41                    self.set_create(
42                        true,
43                        create2_deployer.create2_from_code(B256::from_slice(salt), init_code),
44                        local_contracts,
45                    )?;
46                }
47            } else {
48                self.transaction.call_kind = CallKind::Call;
49                self.transaction.contract_address = Some(to);
50
51                let Some(data) = self.transaction.transaction.input() else { return Ok(()) };
52
53                if data.len() < SELECTOR_LEN {
54                    return Ok(());
55                }
56
57                let (selector, data) = data.split_at(SELECTOR_LEN);
58                let selector = Selector::from_slice(selector);
59
60                let function = if let Some(info) = local_contracts.get(&to) {
61                    // This CALL is made to a local contract.
62                    self.transaction.contract_name = Some(info.name.clone());
63                    info.abi.functions().find(|function| function.selector() == selector)
64                } else {
65                    // This CALL is made to an external contract; try to decode it from the given
66                    // decoder.
67                    decoder
68                        .functions_for_selector(to, &selector)
69                        .and_then(|functions| functions.first())
70                };
71
72                if let Some(function) = function {
73                    self.transaction.function = Some(function.signature());
74                    self.transaction.function_abi = Some(function.full_signature());
75                    self.transaction.display_function = Some(function.name.clone());
76
77                    let values = function.abi_decode_input(data).inspect_err(|_| {
78                        error!(
79                            contract=?self.transaction.contract_name,
80                            signature=?function,
81                            data=hex::encode(data),
82                            "Failed to decode function arguments",
83                        );
84                    })?;
85                    self.transaction.arguments =
86                        Some(values.iter().map(format_token_raw).collect());
87                }
88            }
89        }
90
91        Ok(())
92    }
93
94    /// Populate the transaction as CREATE tx
95    ///
96    /// If this is a CREATE2 transaction this attempt to decode the arguments from the CREATE2
97    /// deployer's function
98    pub fn set_create(
99        &mut self,
100        is_create2: bool,
101        address: Address,
102        contracts: &BTreeMap<Address, &ContractData>,
103    ) -> Result<()> {
104        if is_create2 {
105            self.transaction.call_kind = CallKind::Create2;
106        } else {
107            self.transaction.call_kind = CallKind::Create;
108        }
109
110        let info = contracts.get(&address);
111        self.transaction.contract_name = info.map(|info| info.name.clone());
112        self.transaction.contract_address = Some(address);
113
114        let Some(data) = self.transaction.transaction.input() else { return Ok(()) };
115        let Some(info) = info else { return Ok(()) };
116        let Some(bytecode) = info.bytecode() else { return Ok(()) };
117
118        // `create2` transactions are prefixed by a 32 byte salt.
119        let creation_code = if is_create2 {
120            if data.len() < 32 {
121                return Ok(());
122            }
123            &data[32..]
124        } else {
125            data
126        };
127
128        // The constructor args start after bytecode.
129        let contains_constructor_args = creation_code.len() > bytecode.len();
130        if !contains_constructor_args {
131            return Ok(());
132        }
133        let constructor_args = &creation_code[bytecode.len()..];
134
135        let Some(constructor) = info.abi.constructor() else { return Ok(()) };
136        let values = constructor.abi_decode_input(constructor_args).inspect_err(|_| {
137                error!(
138                    contract=?self.transaction.contract_name,
139                    signature=%format!("constructor({})", constructor.inputs.iter().map(|p| &p.ty).format(",")),
140                    is_create2,
141                    constructor_args=%hex::encode(constructor_args),
142                    "Failed to decode constructor arguments",
143                );
144                debug!(full_data=%hex::encode(data), bytecode=%hex::encode(creation_code));
145            })?;
146        self.transaction.arguments = Some(values.iter().map(format_token_raw).collect());
147
148        Ok(())
149    }
150
151    /// Populates additional data from the transaction execution result.
152    pub fn with_execution_result(
153        mut self,
154        result: &ScriptResult<N>,
155        gas_estimate_multiplier: u64,
156        linked_build_data: &LinkedBuildData,
157    ) -> Self {
158        let mut created_contracts =
159            result.get_created_contracts(&linked_build_data.known_contracts);
160
161        // Add the additional contracts created in this transaction, so we can verify them later.
162        created_contracts.retain(|contract| {
163            // Filter out the contract that was created by the transaction itself.
164            self.transaction.contract_address != Some(contract.address)
165        });
166
167        self.transaction.additional_contracts = created_contracts;
168
169        if !self.transaction.is_fixed_gas_limit
170            && let Some(unsigned) = self.transaction.transaction.as_unsigned_mut()
171        {
172            // We inflate the gas used by the user specified percentage
173            unsigned.set_gas_limit(result.gas_used * gas_estimate_multiplier / 100);
174        }
175
176        self
177    }
178
179    pub fn build(self) -> TransactionWithMetadata<N> {
180        self.transaction
181    }
182}
183
184impl<N: Network> From<TransactionWithMetadata<N>> for ScriptTransactionBuilder<N> {
185    fn from(transaction: TransactionWithMetadata<N>) -> Self {
186        Self { transaction }
187    }
188}
189
190#[cfg(all(test, feature = "monad"))]
191mod tests {
192    use super::*;
193    use alloy_network::Ethereum;
194    use alloy_primitives::{Bytes, address, keccak256};
195    use alloy_rpc_types::TransactionRequest;
196    use foundry_evm::{hardforks::MonadHardfork, traces::CallTraceDecoderBuilder};
197    use foundry_evm_networks::NetworkConfigs;
198
199    const STAKING_ADDRESS: Address = address!("0000000000000000000000000000000000001000");
200    const RESERVE_BALANCE_ADDRESS: Address = address!("0000000000000000000000000000000000001001");
201
202    fn monad_decoder(hardfork: MonadHardfork) -> CallTraceDecoder {
203        CallTraceDecoderBuilder::new()
204            .with_networks(NetworkConfigs::with_monad())
205            .with_chain_id(Some(143))
206            .with_monad_hardfork(Some(hardfork))
207            .build()
208    }
209
210    fn call_metadata(
211        address: Address,
212        signature: &str,
213        hardfork: MonadHardfork,
214    ) -> TransactionWithMetadata<Ethereum> {
215        let input = Bytes::copy_from_slice(&keccak256(signature)[..SELECTOR_LEN]);
216        let selector = Selector::from_slice(&input);
217        let decoder = monad_decoder(hardfork);
218
219        assert!(!decoder.functions.contains_key(&selector));
220        assert!(decoder.functions_for_selector(address, &selector).is_some());
221
222        let transaction = TransactionRequest::default()
223            .with_from(Address::repeat_byte(0x11))
224            .with_to(address)
225            .with_nonce(0)
226            .with_input(input);
227        let mut builder = ScriptTransactionBuilder::new(
228            TransactionMaybeSigned::new(transaction),
229            "http://localhost:8545".to_string(),
230        );
231        builder.set_call(&BTreeMap::new(), &decoder, Address::ZERO).unwrap();
232        builder.build()
233    }
234
235    #[test]
236    fn address_scoped_monad_calls_populate_metadata() {
237        let staking = call_metadata(STAKING_ADDRESS, "getEpoch()", MonadHardfork::MonadEight);
238        assert_eq!(staking.function.as_deref(), Some("getEpoch()"));
239        assert_eq!(
240            staking.function_abi.as_deref(),
241            Some("function getEpoch() returns (uint64 epoch, bool inEpochDelayPeriod)")
242        );
243        assert_eq!(staking.display_function.as_deref(), Some("getEpoch"));
244        assert_eq!(staking.arguments, Some(Vec::new()));
245
246        let reserve =
247            call_metadata(RESERVE_BALANCE_ADDRESS, "dippedIntoReserve()", MonadHardfork::MonadNine);
248        assert_eq!(reserve.function.as_deref(), Some("dippedIntoReserve()"));
249        assert_eq!(
250            reserve.function_abi.as_deref(),
251            Some("function dippedIntoReserve() returns (bool dipped)")
252        );
253        assert_eq!(reserve.display_function.as_deref(), Some("dippedIntoReserve"));
254        assert_eq!(reserve.arguments, Some(Vec::new()));
255    }
256}