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