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