forge_script/
transaction.rs
1use super::ScriptResult;
2use alloy_dyn_abi::JsonAbiExt;
3use alloy_primitives::{hex, Address, TxKind, B256};
4use eyre::Result;
5use forge_script_sequence::TransactionWithMetadata;
6use foundry_common::{fmt::format_token_raw, ContractData, TransactionMaybeSigned, SELECTOR_LEN};
7use foundry_evm::traces::CallTraceDecoder;
8use itertools::Itertools;
9use revm_inspectors::tracing::types::CallKind;
10use std::collections::BTreeMap;
11
12#[derive(Debug)]
13pub struct ScriptTransactionBuilder {
14 transaction: TransactionWithMetadata,
15}
16
17impl ScriptTransactionBuilder {
18 pub fn new(transaction: TransactionMaybeSigned, rpc: String) -> Self {
19 let mut transaction = TransactionWithMetadata::from_tx_request(transaction);
20 transaction.rpc = rpc;
21 transaction.is_fixed_gas_limit = transaction.tx().gas().is_some();
23
24 Self { transaction }
25 }
26
27 pub fn set_call(
29 &mut self,
30 local_contracts: &BTreeMap<Address, &ContractData>,
31 decoder: &CallTraceDecoder,
32 create2_deployer: Address,
33 ) -> Result<()> {
34 if let Some(TxKind::Call(to)) = self.transaction.transaction.to() {
35 if to == create2_deployer {
36 if let Some(input) = self.transaction.transaction.input() {
37 let (salt, init_code) = input.split_at(32);
38
39 self.set_create(
40 true,
41 create2_deployer.create2_from_code(B256::from_slice(salt), init_code),
42 local_contracts,
43 )?;
44 }
45 } else {
46 self.transaction.opcode = CallKind::Call;
47 self.transaction.contract_address = Some(to);
48
49 let Some(data) = self.transaction.transaction.input() else { return Ok(()) };
50
51 if data.len() < SELECTOR_LEN {
52 return Ok(());
53 }
54
55 let (selector, data) = data.split_at(SELECTOR_LEN);
56
57 let function = if let Some(info) = local_contracts.get(&to) {
58 self.transaction.contract_name = Some(info.name.clone());
60 info.abi.functions().find(|function| function.selector() == selector)
61 } else {
62 decoder.functions.get(selector).and_then(|v| v.first())
65 };
66
67 if let Some(function) = function {
68 self.transaction.function = Some(function.signature());
69
70 let values = function.abi_decode_input(data, false).inspect_err(|_| {
71 error!(
72 contract=?self.transaction.contract_name,
73 signature=?function,
74 data=hex::encode(data),
75 "Failed to decode function arguments",
76 );
77 })?;
78 self.transaction.arguments =
79 Some(values.iter().map(format_token_raw).collect());
80 }
81 }
82 }
83
84 Ok(())
85 }
86
87 pub fn set_create(
92 &mut self,
93 is_create2: bool,
94 address: Address,
95 contracts: &BTreeMap<Address, &ContractData>,
96 ) -> Result<()> {
97 if is_create2 {
98 self.transaction.opcode = CallKind::Create2;
99 } else {
100 self.transaction.opcode = CallKind::Create;
101 }
102
103 let info = contracts.get(&address);
104 self.transaction.contract_name = info.map(|info| info.name.clone());
105 self.transaction.contract_address = Some(address);
106
107 let Some(data) = self.transaction.transaction.input() else { return Ok(()) };
108 let Some(info) = info else { return Ok(()) };
109 let Some(bytecode) = info.bytecode() else { return Ok(()) };
110
111 let creation_code = if is_create2 {
113 if data.len() < 32 {
114 return Ok(())
115 }
116 &data[32..]
117 } else {
118 data
119 };
120
121 let contains_constructor_args = creation_code.len() > bytecode.len();
123 if !contains_constructor_args {
124 return Ok(());
125 }
126 let constructor_args = &creation_code[bytecode.len()..];
127
128 let Some(constructor) = info.abi.constructor() else { return Ok(()) };
129 let values = constructor.abi_decode_input(constructor_args, false).inspect_err(|_| {
130 error!(
131 contract=?self.transaction.contract_name,
132 signature=%format!("constructor({})", constructor.inputs.iter().map(|p| &p.ty).format(",")),
133 is_create2,
134 constructor_args=%hex::encode(constructor_args),
135 "Failed to decode constructor arguments",
136 );
137 debug!(full_data=%hex::encode(data), bytecode=%hex::encode(creation_code));
138 })?;
139 self.transaction.arguments = Some(values.iter().map(format_token_raw).collect());
140
141 Ok(())
142 }
143
144 pub fn with_execution_result(
146 mut self,
147 result: &ScriptResult,
148 gas_estimate_multiplier: u64,
149 ) -> Self {
150 let mut created_contracts = result.get_created_contracts();
151
152 created_contracts.retain(|contract| {
154 self.transaction.contract_address != Some(contract.address)
156 });
157
158 self.transaction.additional_contracts = created_contracts;
159
160 if !self.transaction.is_fixed_gas_limit {
161 if let Some(unsigned) = self.transaction.transaction.as_unsigned_mut() {
162 unsigned.gas = Some(result.gas_used * gas_estimate_multiplier / 100);
164 }
165 }
166
167 self
168 }
169
170 pub fn build(self) -> TransactionWithMetadata {
171 self.transaction
172 }
173}
174
175impl From<TransactionWithMetadata> for ScriptTransactionBuilder {
176 fn from(transaction: TransactionWithMetadata) -> Self {
177 Self { transaction }
178 }
179}