forge_script/
transaction.rs1use 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 transaction.is_fixed_gas_limit = transaction.tx().gas().is_some();
25
26 Self { transaction }
27 }
28
29 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 self.transaction.contract_name = Some(info.name.clone());
71 info.abi.functions().find(|function| function.selector() == selector)
72 } else {
73 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 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 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 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 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 created_contracts.retain(|contract| {
171 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 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}