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 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 self.transaction.contract_name = Some(info.name.clone());
63 info.abi.functions().find(|function| function.selector() == selector)
64 } else {
65 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 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 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 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 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 created_contracts.retain(|contract| {
163 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 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}