Skip to main content

forge_script/
library_deployments.rs

1use crate::{
2    build::{LinkedBuildData, ScriptPredeployLibraries},
3    execute::LinkedState,
4    simulate::PreSimulationState,
5};
6use alloy_network::TransactionBuilder;
7use alloy_primitives::{Address, keccak256};
8use eyre::Result;
9use foundry_common::{LIBRARY_DEPLOYER, matches_contract_creation, shell};
10use foundry_compilers::ArtifactId;
11use foundry_evm::{constants::CHEATCODE_ADDRESS, core::evm::FoundryEvmNetwork};
12use std::collections::BTreeSet;
13
14struct EligibleTransactions {
15    required: BTreeSet<ArtifactId>,
16    artifacts: Vec<ArtifactId>,
17    rpc: String,
18    baseline_count: usize,
19}
20
21const RERUN_UNSAFE_CHEATCODE_SIGNATURES: &[&str] = &[
22    "setEnv(string,string)",
23    "sign(bytes32)",
24    "sign(address,bytes32)",
25    "signCompact(bytes32)",
26    "signCompact(address,bytes32)",
27    "rpc(string,string)",
28    "rpc(string,string,string)",
29    "rpcJson(string,string)",
30    "rpcJson(string,string,string)",
31    "sleep(uint256)",
32    "prompt(string)",
33    "promptSecret(string)",
34    "promptSecretUint(string)",
35    "promptAddress(string)",
36    "promptUint(string)",
37    "dumpState(string)",
38    "createFork(string)",
39    "createFork(string,uint256)",
40    "createFork(string,bytes32)",
41    "createSelectFork(string)",
42    "createSelectFork(string,uint256)",
43    "createSelectFork(string,bytes32)",
44    "rollFork(uint256)",
45    "rollFork(bytes32)",
46    "rollFork(uint256,uint256)",
47    "rollFork(uint256,bytes32)",
48    "selectFork(uint256)",
49    "transact(bytes32)",
50    "transact(uint256,bytes32)",
51];
52
53pub(crate) fn rerun_unsafe_cheatcode_selectors() -> Vec<[u8; 4]> {
54    RERUN_UNSAFE_CHEATCODE_SIGNATURES
55        .iter()
56        .map(|signature| {
57            let hash = keccak256(signature.as_bytes());
58            [hash[0], hash[1], hash[2], hash[3]]
59        })
60        .collect()
61}
62
63impl<FEN: FoundryEvmNetwork> PreSimulationState<FEN> {
64    /// Reruns narrowly eligible scripts with libraries unused by direct broadcasts deployed only
65    /// in the local EVM.
66    pub async fn optimize_library_deployments(self) -> Result<Self> {
67        let Ok(Some(EligibleTransactions { required, artifacts, rpc, baseline_count })) =
68            self.eligible()
69        else {
70            return Ok(self);
71        };
72        let onchain = match &self.build_data.predeploy_libraries {
73            ScriptPredeployLibraries::Default { onchain, .. }
74            | ScriptPredeployLibraries::Create2 { onchain, .. } => onchain,
75        };
76        if required.len() == onchain.len() {
77            return Ok(self);
78        }
79
80        let Ok(libraries) = self.script_config.config.libraries_with_remappings() else {
81            return Ok(self);
82        };
83        let linker = self.build_data.build_data.get_linker();
84        let linked = match &self.build_data.predeploy_libraries {
85            ScriptPredeployLibraries::Default { .. } => linker
86                .link_with_partition(
87                    libraries,
88                    self.script_config.evm_opts.sender,
89                    self.script_config.sender_nonce,
90                    LIBRARY_DEPLOYER,
91                    &required,
92                    &self.build_data.build_data.target,
93                )
94                .map(|(output, local)| {
95                    let onchain = onchain_libraries(&output, &local);
96                    (output.output, ScriptPredeployLibraries::Default { onchain, local })
97                }),
98            ScriptPredeployLibraries::Create2 { salt, .. } => linker
99                .link_with_create2_partition(
100                    libraries,
101                    self.script_config.evm_opts.create2_deployer,
102                    *salt,
103                    LIBRARY_DEPLOYER,
104                    &required,
105                    &self.build_data.build_data.target,
106                )
107                .map(|(output, local)| {
108                    let onchain = onchain_libraries(&output, &local);
109                    (
110                        output.output,
111                        ScriptPredeployLibraries::Create2 { onchain, salt: *salt, local },
112                    )
113                }),
114        };
115        let Ok((output, predeploy_libraries)) = linked else { return Ok(self) };
116        let Ok(build_data) = LinkedBuildData::new(
117            output.libraries,
118            predeploy_libraries,
119            self.build_data.build_data.clone(),
120        ) else {
121            return Ok(self);
122        };
123        let candidate = LinkedState {
124            args: self.args.clone(),
125            script_config: self.script_config.clone(),
126            script_wallets: self.script_wallets.clone(),
127            browser_wallet: self.browser_wallet.clone(),
128            build_data,
129        }
130        .prepare_execution()
131        .await;
132        let Ok(candidate) = candidate else { return Ok(self) };
133        let candidate = candidate.execute_restricted().await;
134        let Ok(candidate) = candidate else { return Ok(self) };
135        if !candidate.execution_result.success {
136            return Ok(self);
137        }
138        let candidate = candidate.prepare_simulation_silent().await;
139        let Ok(candidate) = candidate else { return Ok(self) };
140        if self.execution_result.returned != candidate.execution_result.returned
141            || self.execution_result.logs != candidate.execution_result.logs
142            || !self.equivalent_candidate(&candidate, &artifacts, &rpc, baseline_count)
143        {
144            return Ok(self);
145        }
146        Ok(candidate)
147    }
148
149    fn eligible(&self) -> Result<Option<EligibleTransactions>> {
150        if !self.execution_result.success
151            || self.args.skip_simulation
152            || self.args.debug
153            || self.args.dump.is_some()
154            || self.args.batch
155            || self.args.slow
156            || self.script_config.config.ffi
157            || self.script_config.config.live_logs
158            || shell::is_json()
159            || shell::verbosity() > 3
160            || self.script_config.evm_opts.env.gas_price.is_some()
161            || self.execution_artifacts.rpc_data.missing_rpc
162            || self.execution_artifacts.rpc_data.is_multi_chain()
163            || self.script_config.evm_opts.sender == LIBRARY_DEPLOYER
164            || self.script_config.config.fs_permissions.permissions.iter().any(|permission| {
165                matches!(
166                    permission.access,
167                    foundry_config::fs_permissions::FsAccessPermission::Write
168                        | foundry_config::fs_permissions::FsAccessPermission::ReadWrite
169                )
170            })
171            || self.used_rerun_unsafe_cheatcode()
172        {
173            return Ok(None);
174        }
175        let Some(rpc) = self.script_config.evm_opts.fork_url.clone() else { return Ok(None) };
176        if self.script_config.resolved_fork()?.is_none()
177            || self.execution_artifacts.rpc_data.total_rpcs.len() != 1
178            || !self.execution_artifacts.rpc_data.total_rpcs.contains(&rpc)
179        {
180            return Ok(None);
181        }
182        let onchain = match &self.build_data.predeploy_libraries {
183            ScriptPredeployLibraries::Default { onchain, .. }
184            | ScriptPredeployLibraries::Create2 { onchain, .. } => onchain,
185        };
186        if onchain.is_empty() {
187            return Ok(None);
188        }
189        let Some(transactions) = &self.execution_result.transactions else { return Ok(None) };
190        if transactions.len() <= onchain.len()
191            || transactions.iter().any(|tx| {
192                !tx.transaction.is_unsigned()
193                    || tx.transaction.authorization_list().is_some_and(|list| !list.is_empty())
194            })
195        {
196            return Ok(None);
197        }
198        for (index, (tx, library)) in transactions.iter().zip(onchain).enumerate() {
199            let input = tx.transaction.input().map(|input| input.as_ref()).unwrap_or_default();
200            let deployment_matches = match &self.build_data.predeploy_libraries {
201                ScriptPredeployLibraries::Default { .. } => {
202                    tx.transaction.to().is_none() && input == library.bytecode.as_ref()
203                }
204                ScriptPredeployLibraries::Create2 { salt, .. } => {
205                    tx.transaction.to() == Some(self.script_config.evm_opts.create2_deployer)
206                        && input.get(..32) == Some(salt.as_slice())
207                        && input.get(32..) == Some(library.bytecode.as_ref())
208                }
209            };
210            let exact = deployment_matches
211                && tx.rpc.as_ref() == Some(&rpc)
212                && tx.transaction.from() == Some(self.script_config.evm_opts.sender)
213                && tx.transaction.nonce() == Some(self.script_config.sender_nonce + index as u64);
214            if !exact {
215                return Ok(None);
216            }
217        }
218
219        let library_ids = onchain.iter().map(|library| library.id.clone()).collect::<BTreeSet<_>>();
220        let linker = self.build_data.build_data.get_linker();
221        let mut required = BTreeSet::new();
222        let mut artifacts = Vec::new();
223        for tx in transactions.iter().skip(onchain.len()) {
224            if tx.rpc.as_ref() != Some(&rpc)
225                || tx.transaction.from() != Some(self.script_config.evm_opts.sender)
226                || tx.transaction.to().is_some()
227            {
228                return Ok(None);
229            }
230            let Some(input) = tx.transaction.input() else { return Ok(None) };
231            let matches = self
232                .build_data
233                .known_contracts
234                .iter()
235                .filter(|(_, contract)| matches_contract_creation(contract, input))
236                .map(|(id, _)| id)
237                .collect::<Vec<_>>();
238            let [artifact] = matches.as_slice() else { return Ok(None) };
239            artifacts.push((*artifact).clone());
240            required.extend(
241                linker.dependencies(artifact)?.into_iter().filter(|id| library_ids.contains(id)),
242            );
243        }
244        Ok(Some(EligibleTransactions { required, artifacts, rpc, baseline_count: onchain.len() }))
245    }
246
247    fn equivalent_candidate(
248        &self,
249        candidate: &Self,
250        artifacts: &[ArtifactId],
251        rpc: &str,
252        baseline_count: usize,
253    ) -> bool {
254        let Some(baseline) = self.execution_result.transactions.as_ref() else { return false };
255        let Some(candidate_txs) = candidate.execution_result.transactions.as_ref() else {
256            return false;
257        };
258        let candidate_count = candidate.build_data.predeploy_libraries.libraries_count();
259        let baseline = baseline.iter().skip(baseline_count).collect::<Vec<_>>();
260        let candidate_txs = candidate_txs.iter().skip(candidate_count).collect::<Vec<_>>();
261        if baseline.len() != artifacts.len() || candidate_txs.len() != artifacts.len() {
262            return false;
263        }
264        let mut remaps = Vec::new();
265        let baseline_libraries = match &self.build_data.predeploy_libraries {
266            ScriptPredeployLibraries::Default { onchain, .. }
267            | ScriptPredeployLibraries::Create2 { onchain, .. } => onchain,
268        };
269        let candidate_libraries = match &candidate.build_data.predeploy_libraries {
270            ScriptPredeployLibraries::Default { onchain, .. }
271            | ScriptPredeployLibraries::Create2 { onchain, .. } => onchain,
272        };
273        for library in baseline_libraries {
274            if let Some(other) = candidate_libraries.iter().find(|other| other.id == library.id) {
275                remaps.push((library.address, other.address));
276            }
277        }
278        for ((baseline, candidate_tx), artifact) in
279            baseline.iter().zip(&candidate_txs).zip(artifacts)
280        {
281            let (Some(mut old), Some(new)) = (
282                baseline.transaction.clone().as_unsigned_mut().cloned(),
283                candidate_tx.transaction.clone().as_unsigned_mut().cloned(),
284            ) else {
285                return false;
286            };
287            if baseline.rpc.as_deref() != Some(rpc)
288                || candidate_tx.rpc.as_deref() != Some(rpc)
289                || old.from() != new.from()
290                || old.to().is_some()
291                || new.to().is_some()
292            {
293                return false;
294            }
295            let (Some(old_nonce), Some(new_nonce)) = (old.nonce(), new.nonce()) else {
296                return false;
297            };
298            if old_nonce.checked_sub(baseline_count as u64)
299                != new_nonce.checked_sub(candidate_count as u64)
300            {
301                return false;
302            }
303            let mut input = old.input().cloned().unwrap_or_default().to_vec();
304            for (from, to) in &remaps {
305                replace_addresses(&mut input, *from, *to);
306            }
307            old.set_nonce(new_nonce);
308            old.set_input(input);
309            let (Ok(old_value), Ok(new_value)) =
310                (serde_json::to_value(&old), serde_json::to_value(&new))
311            else {
312                return false;
313            };
314            if old_value != new_value
315                || !candidate.build_data.known_contracts.get(artifact).is_some_and(|contract| {
316                    new.input().is_some_and(|input| matches_contract_creation(contract, input))
317                })
318            {
319                return false;
320            }
321            remaps.push((
322                old.from().unwrap().create(old_nonce),
323                new.from().unwrap().create(new_nonce),
324            ));
325        }
326        true
327    }
328
329    fn used_rerun_unsafe_cheatcode(&self) -> bool {
330        let selectors = rerun_unsafe_cheatcode_selectors();
331        self.execution_result.traces.iter().any(|(_, traces)| {
332            traces.nodes().iter().any(|node| {
333                node.trace.address == CHEATCODE_ADDRESS
334                    && node.trace.data.get(..4).is_some_and(|selector| {
335                        selectors.iter().any(|blocked| blocked.as_slice() == selector)
336                    })
337            })
338        })
339    }
340}
341
342fn onchain_libraries(
343    output: &foundry_linking::DetailedLinkOutput,
344    local: &[foundry_linking::LinkedLibrary],
345) -> Vec<foundry_linking::LinkedLibrary> {
346    output
347        .linked_libraries
348        .iter()
349        .filter(|library| !local.iter().any(|local| local.id == library.id))
350        .cloned()
351        .collect()
352}
353
354fn replace_addresses(input: &mut [u8], from: Address, to: Address) {
355    let mut offset = 0;
356    while let Some(index) =
357        input[offset..].windows(Address::len_bytes()).position(|window| window == from.as_slice())
358    {
359        let start = offset + index;
360        input[start..start + Address::len_bytes()].copy_from_slice(to.as_slice());
361        offset = start + Address::len_bytes();
362    }
363}