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::{
9    ContractData, ContractsByArtifact, SELECTOR_LEN, TransactionMaybeSigned, fmt::format_token_raw,
10};
11use foundry_evm::traces::{CallKind, CallTraceDecoder};
12use itertools::Itertools;
13use std::collections::BTreeMap;
14
15#[derive(Debug)]
16pub struct ScriptTransactionBuilder<N: Network> {
17    transaction: TransactionWithMetadata<N>,
18}
19
20impl<N: Network> ScriptTransactionBuilder<N> {
21    pub fn new(transaction: TransactionMaybeSigned<N>, rpc: String) -> Self {
22        let mut transaction = TransactionWithMetadata::from_tx_request(transaction);
23        transaction.rpc = rpc;
24        // If tx.gas is already set that means it was specified in script
25        transaction.is_fixed_gas_limit = transaction.tx().gas().is_some();
26
27        Self { transaction }
28    }
29
30    /// Populate the transaction as CALL tx
31    pub fn set_call(
32        &mut self,
33        local_contracts: &BTreeMap<Address, &ContractData>,
34        known_contracts: &ContractsByArtifact,
35        decoder: &CallTraceDecoder,
36        create2_deployer: Address,
37    ) -> Result<()> {
38        if let Some(to) = self.transaction.transaction.to() {
39            self.transaction.call_kind = CallKind::Call;
40            self.transaction.contract_address = Some(to);
41
42            if to == create2_deployer {
43                if let Some(input) = self.transaction.transaction.input()
44                    && input.len() >= 32
45                {
46                    let (salt, init_code) = input.split_at(32);
47
48                    self.set_create(
49                        true,
50                        create2_deployer.create2_from_code(B256::from_slice(salt), init_code),
51                        known_contracts,
52                    )?;
53                } else {
54                    let input_len =
55                        self.transaction.transaction.input().map_or(0, |input| input.len());
56                    sh_warn!(
57                        "Skipping CREATE2 decoding for call to deployer {create2_deployer}: input length {input_len} is shorter than the 32-byte salt prefix"
58                    )?;
59                }
60            } else {
61                let Some(data) = self.transaction.transaction.input() else { return Ok(()) };
62
63                if data.len() < SELECTOR_LEN {
64                    return Ok(());
65                }
66
67                let (selector, data) = data.split_at(SELECTOR_LEN);
68                let selector = Selector::from_slice(selector);
69
70                let function = if let Some(info) = local_contracts.get(&to) {
71                    // This CALL is made to a local contract.
72                    self.transaction.contract_name = Some(info.name.clone());
73                    info.abi.functions().find(|function| function.selector() == selector)
74                } else {
75                    // This CALL is made to an external contract; try to decode it from the given
76                    // decoder.
77                    decoder
78                        .functions_for_selector(to, &selector)
79                        .and_then(|functions| functions.first())
80                };
81
82                if let Some(function) = function {
83                    self.transaction.function = Some(function.signature());
84                    self.transaction.function_abi = Some(function.full_signature());
85                    self.transaction.display_function = Some(function.name.clone());
86
87                    let values = function.abi_decode_input(data).inspect_err(|_| {
88                        error!(
89                            contract=?self.transaction.contract_name,
90                            signature=?function,
91                            data=hex::encode(data),
92                            "Failed to decode function arguments",
93                        );
94                    })?;
95                    self.transaction.arguments =
96                        Some(values.iter().map(format_token_raw).collect());
97                }
98            }
99        }
100
101        Ok(())
102    }
103
104    /// Populate the transaction as CREATE tx
105    ///
106    /// If this is a CREATE2 transaction this attempt to decode the arguments from the CREATE2
107    /// deployer's function
108    pub fn set_create(
109        &mut self,
110        is_create2: bool,
111        address: Address,
112        contracts: &ContractsByArtifact,
113    ) -> Result<()> {
114        if is_create2 {
115            self.transaction.call_kind = CallKind::Create2;
116        } else {
117            self.transaction.call_kind = CallKind::Create;
118        }
119
120        self.transaction.contract_address = Some(address);
121
122        let Some(data) = self.transaction.transaction.input() else { return Ok(()) };
123
124        // `create2` transactions are prefixed by a 32 byte salt.
125        let creation_code = if is_create2 {
126            if data.len() < 32 {
127                return Ok(());
128            }
129            &data[32..]
130        } else {
131            data
132        };
133        let artifact = contracts.find_by_creation_code_prefix(creation_code);
134        self.transaction.contract_name = artifact
135            .or_else(|| contracts.find_by_creation_code(creation_code))
136            .map(|(id, _)| id.name.clone());
137        // Script verification skips Vyper, so a Vyper deployment never has to match on resume.
138        self.transaction.exact_artifact_match =
139            artifact.is_some_and(|(id, _)| !id.source.extension().is_some_and(|ext| ext == "vy"));
140
141        let Some((_, info)) = artifact else { return Ok(()) };
142        let Some(bytecode) = info.bytecode() else { return Ok(()) };
143
144        // The constructor args start after bytecode.
145        let contains_constructor_args = creation_code.len() > bytecode.len();
146        if !contains_constructor_args {
147            return Ok(());
148        }
149        let constructor_args = &creation_code[bytecode.len()..];
150
151        let Some(constructor) = info.abi.constructor() else { return Ok(()) };
152        let values = constructor.abi_decode_input(constructor_args).inspect_err(|_| {
153                error!(
154                    contract=?self.transaction.contract_name,
155                    signature=%format!("constructor({})", constructor.inputs.iter().map(|p| &p.ty).format(",")),
156                    is_create2,
157                    constructor_args=%hex::encode(constructor_args),
158                    "Failed to decode constructor arguments",
159                );
160                debug!(full_data=%hex::encode(data), bytecode=%hex::encode(creation_code));
161            })?;
162        self.transaction.arguments = Some(values.iter().map(format_token_raw).collect());
163
164        Ok(())
165    }
166
167    /// Populates additional data from the transaction execution result.
168    pub fn with_execution_result(
169        mut self,
170        result: &ScriptResult<N>,
171        gas_estimate_multiplier: u64,
172        linked_build_data: &LinkedBuildData,
173    ) -> Self {
174        let mut created_contracts =
175            result.get_created_contracts(&linked_build_data.known_contracts);
176
177        // Add the additional contracts created in this transaction, so we can verify them later.
178        created_contracts.retain(|contract| {
179            // Filter out the contract that was created by the transaction itself.
180            self.transaction.contract_address != Some(contract.address)
181        });
182
183        self.transaction.additional_contracts = created_contracts;
184
185        if !self.transaction.is_fixed_gas_limit
186            && let Some(unsigned) = self.transaction.transaction.as_unsigned_mut()
187        {
188            // We inflate the gas used by the user specified percentage
189            unsigned.set_gas_limit(result.gas_used * gas_estimate_multiplier / 100);
190        }
191
192        self
193    }
194
195    pub fn build(self) -> TransactionWithMetadata<N> {
196        self.transaction
197    }
198}
199
200impl<N: Network> From<TransactionWithMetadata<N>> for ScriptTransactionBuilder<N> {
201    fn from(transaction: TransactionWithMetadata<N>) -> Self {
202        Self { transaction }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use alloy_network::Ethereum;
210    use alloy_primitives::{Bytes, address};
211    use alloy_rpc_types::TransactionRequest;
212
213    fn call_to_create2_deployer(input: Option<Bytes>) -> TransactionWithMetadata<Ethereum> {
214        let create2_deployer = address!("0000000000000000000000000000000000001234");
215        let mut transaction = TransactionRequest::default()
216            .with_from(Address::repeat_byte(0x11))
217            .with_to(create2_deployer)
218            .with_nonce(0);
219        if let Some(input) = input {
220            transaction = transaction.with_input(input);
221        }
222        let mut builder = ScriptTransactionBuilder::<Ethereum>::new(
223            TransactionMaybeSigned::new(transaction),
224            "http://localhost:8545".to_string(),
225        );
226        let decoder = CallTraceDecoder::new();
227        builder
228            .set_call(&BTreeMap::new(), &ContractsByArtifact::default(), decoder, create2_deployer)
229            .unwrap();
230        builder.build()
231    }
232
233    #[test]
234    fn short_create2_input_is_classified_as_call() {
235        let create2_deployer = address!("0000000000000000000000000000000000001234");
236        for input in [None, Some(Bytes::new()), Some(Bytes::from(vec![0xab; 31]))] {
237            let transaction = call_to_create2_deployer(input);
238            assert_eq!(transaction.call_kind, CallKind::Call);
239            assert_eq!(transaction.contract_address, Some(create2_deployer));
240        }
241    }
242
243    #[test]
244    fn valid_create2_input_is_classified_as_create2() {
245        let create2_deployer = address!("0000000000000000000000000000000000001234");
246        for input in [Bytes::from(vec![0xab; 32]), Bytes::from(vec![0xab; 33])] {
247            let expected = create2_deployer
248                .create2_from_code(B256::repeat_byte(0xab), input.get(32..).unwrap());
249            let transaction = call_to_create2_deployer(Some(input));
250            assert_eq!(transaction.call_kind, CallKind::Create2);
251            assert_eq!(transaction.contract_address, Some(expected));
252        }
253    }
254}
255
256#[cfg(all(test, feature = "monad"))]
257mod monad_tests {
258    use super::*;
259    use alloy_network::Ethereum;
260    use alloy_primitives::{Bytes, address, keccak256};
261    use alloy_rpc_types::TransactionRequest;
262    use foundry_evm::{hardforks::MonadHardfork, traces::CallTraceDecoderBuilder};
263    use foundry_evm_networks::NetworkConfigs;
264
265    const STAKING_ADDRESS: Address = address!("0000000000000000000000000000000000001000");
266    const RESERVE_BALANCE_ADDRESS: Address = address!("0000000000000000000000000000000000001001");
267
268    fn monad_decoder(hardfork: MonadHardfork) -> CallTraceDecoder {
269        CallTraceDecoderBuilder::new()
270            .with_networks(NetworkConfigs::with_monad())
271            .with_chain_id(Some(143))
272            .with_hardfork(Some(hardfork.into()))
273            .build()
274    }
275
276    fn call_metadata(
277        address: Address,
278        signature: &str,
279        hardfork: MonadHardfork,
280    ) -> TransactionWithMetadata<Ethereum> {
281        let input = Bytes::copy_from_slice(&keccak256(signature)[..SELECTOR_LEN]);
282        let selector = Selector::from_slice(&input);
283        let decoder = monad_decoder(hardfork);
284
285        assert!(!decoder.functions.contains_key(&selector));
286        assert!(decoder.functions_for_selector(address, &selector).is_some());
287
288        let transaction = TransactionRequest::default()
289            .with_from(Address::repeat_byte(0x11))
290            .with_to(address)
291            .with_nonce(0)
292            .with_input(input);
293        let mut builder = ScriptTransactionBuilder::new(
294            TransactionMaybeSigned::new(transaction),
295            "http://localhost:8545".to_string(),
296        );
297        builder
298            .set_call(&BTreeMap::new(), &ContractsByArtifact::default(), &decoder, Address::ZERO)
299            .unwrap();
300        builder.build()
301    }
302
303    #[test]
304    fn address_scoped_monad_calls_populate_metadata() {
305        let staking = call_metadata(STAKING_ADDRESS, "getEpoch()", MonadHardfork::MonadEight);
306        assert_eq!(staking.function.as_deref(), Some("getEpoch()"));
307        assert_eq!(
308            staking.function_abi.as_deref(),
309            Some("function getEpoch() returns (uint64 epoch, bool inEpochDelayPeriod)")
310        );
311        assert_eq!(staking.display_function.as_deref(), Some("getEpoch"));
312        assert_eq!(staking.arguments, Some(Vec::new()));
313
314        let reserve =
315            call_metadata(RESERVE_BALANCE_ADDRESS, "dippedIntoReserve()", MonadHardfork::MonadNine);
316        assert_eq!(reserve.function.as_deref(), Some("dippedIntoReserve()"));
317        assert_eq!(
318            reserve.function_abi.as_deref(),
319            Some("function dippedIntoReserve() returns (bool dipped)")
320        );
321        assert_eq!(reserve.display_function.as_deref(), Some("dippedIntoReserve"));
322        assert_eq!(reserve.arguments, Some(Vec::new()));
323    }
324}