Skip to main content

foundry_common/preprocessor/
data.rs

1use super::span_to_range;
2use foundry_compilers::artifacts::{Source, Sources};
3use path_slash::PathExt;
4use solar::sema::{
5    Gcx,
6    hir::{Contract, ContractId},
7    interface::source_map::FileName,
8};
9use std::{
10    collections::{BTreeMap, HashSet},
11    path::{Path, PathBuf},
12};
13
14/// Keeps data about project contracts definitions referenced from tests and scripts.
15/// Contract id -> Contract data definition mapping.
16pub type PreprocessorData = BTreeMap<ContractId, ContractData>;
17
18/// Keeps data about a contract constructor.
19#[derive(Debug)]
20pub struct ContractConstructorData {
21    /// ABI encoded args.
22    pub abi_encode_args: String,
23    /// Constructor struct fields.
24    pub struct_fields: String,
25}
26
27/// Keeps data about a single contract definition.
28#[derive(Debug)]
29pub(crate) struct ContractData {
30    /// HIR Id of the contract.
31    contract_id: ContractId,
32    /// Path of the source file.
33    path: PathBuf,
34    /// Name of the contract
35    name: String,
36    /// Constructor parameters, if any.
37    pub constructor_data: Option<ContractConstructorData>,
38    /// Artifact string to pass into cheatcodes.
39    pub artifact: String,
40}
41
42impl ContractData {
43    fn new(
44        gcx: Gcx<'_>,
45        contract_id: ContractId,
46        contract: &Contract<'_>,
47        path: &Path,
48        source: &solar::sema::hir::Source<'_>,
49    ) -> Self {
50        let artifact = format!("{}:{}", path.to_slash_lossy(), contract.name);
51
52        // Process data for contracts with constructor and parameters.
53        let constructor_data = contract
54            .ctor
55            .map(|ctor_id| gcx.hir.function(ctor_id))
56            .filter(|ctor| !ctor.parameters.is_empty())
57            .map(|ctor| {
58                let mut abi_encode_args = vec![];
59                let mut struct_fields = vec![];
60                let mut arg_index = 0;
61                for param_id in ctor.parameters {
62                    let src = source.file.src.as_str();
63                    let loc =
64                        span_to_range(gcx.sess.source_map(), gcx.hir.variable(*param_id).span);
65                    let mut new_src = src[loc].replace(" memory ", " ").replace(" calldata ", " ");
66                    if let Some(ident) = gcx.hir.variable(*param_id).name {
67                        abi_encode_args.push(format!("args.{}", ident.name));
68                    } else {
69                        // Generate an unique name if constructor arg doesn't have one.
70                        arg_index += 1;
71                        abi_encode_args.push(format!("args.foundry_pp_ctor_arg{arg_index}"));
72                        new_src.push_str(&format!(" foundry_pp_ctor_arg{arg_index}"));
73                    }
74                    struct_fields.push(new_src);
75                }
76
77                ContractConstructorData {
78                    abi_encode_args: abi_encode_args.join(", "),
79                    struct_fields: struct_fields.join("; "),
80                }
81            });
82
83        Self {
84            contract_id,
85            path: path.to_path_buf(),
86            name: contract.name.to_string(),
87            constructor_data,
88            artifact,
89        }
90    }
91
92    /// If contract has a non-empty constructor, generates a helper source file for it containing a
93    /// helper to encode constructor arguments.
94    ///
95    /// This is needed because current preprocessing wraps the arguments, leaving them unchanged.
96    /// This allows us to handle nested new expressions correctly. However, this requires us to have
97    /// a way to wrap both named and unnamed arguments. i.e you can't do abi.encode({arg: val}).
98    ///
99    /// This function produces a helper struct + a helper function to encode the arguments. The
100    /// struct is defined in scope of an abstract contract inheriting the contract containing the
101    /// constructor. This is done as a hack to allow us to inherit the same scope of definitions.
102    ///
103    /// The resulted helper looks like this:
104    /// ```solidity
105    /// import "lib/openzeppelin-contracts/contracts/token/ERC20.sol";
106    ///
107    /// abstract contract DeployHelper335 is ERC20 {
108    ///     struct FoundryPpConstructorArgs {
109    ///         string name;
110    ///         string symbol;
111    ///     }
112    /// }
113    ///
114    /// function encodeArgs335(DeployHelper335.FoundryPpConstructorArgs memory args) pure returns (bytes memory) {
115    ///     return abi.encode(args.name, args.symbol);
116    /// }
117    /// ```
118    ///
119    /// Example usage:
120    /// ```solidity
121    /// new ERC20(name, symbol)
122    /// ```
123    /// becomes
124    /// ```solidity
125    /// vm.deployCode("artifact path", encodeArgs335(DeployHelper335.FoundryPpConstructorArgs(name, symbol)))
126    /// ```
127    /// With named arguments:
128    /// ```solidity
129    /// new ERC20({name: name, symbol: symbol})
130    /// ```
131    /// becomes
132    /// ```solidity
133    /// vm.deployCode("artifact path", encodeArgs335(DeployHelper335.FoundryPpConstructorArgs({name: name, symbol: symbol})))
134    /// ```
135    pub fn build_helper(&self) -> Option<String> {
136        let Self { contract_id, path, name, constructor_data, artifact: _ } = self;
137
138        let Some(constructor_details) = constructor_data else { return None };
139        let contract_id = contract_id.index();
140        let struct_fields = &constructor_details.struct_fields;
141        let abi_encode_args = &constructor_details.abi_encode_args;
142
143        let helper = format!(
144            r#"
145// SPDX-License-Identifier: MIT
146pragma solidity >=0.4.0;
147
148import "{path}";
149
150abstract contract DeployHelper{contract_id} is {name} {{
151    struct FoundryPpConstructorArgs {{
152        {struct_fields};
153    }}
154}}
155
156function encodeArgs{contract_id}(DeployHelper{contract_id}.FoundryPpConstructorArgs memory args) pure returns (bytes memory) {{
157    return abi.encode({abi_encode_args});
158}}
159        "#,
160            path = path.to_slash_lossy(),
161        );
162
163        Some(helper)
164    }
165}
166
167/// Collects preprocessor data from referenced contracts.
168pub(crate) fn collect_preprocessor_data(
169    gcx: Gcx<'_>,
170    referenced_contracts: &HashSet<ContractId>,
171    root_dir: &Path,
172) -> PreprocessorData {
173    let mut data = PreprocessorData::default();
174    for contract_id in referenced_contracts {
175        let contract = gcx.hir.contract(*contract_id);
176        let source = gcx.hir.source(contract.source);
177
178        let FileName::Real(path) = &source.file.name else {
179            continue;
180        };
181
182        // Match the compiler input paths in generated imports and artifact references.
183        let path = path.strip_prefix(root_dir).unwrap_or(path);
184        let contract_data = ContractData::new(gcx, *contract_id, contract, path, source);
185        data.insert(*contract_id, contract_data);
186    }
187    data
188}
189
190/// Creates helper libraries for contracts with a non-empty constructor.
191///
192/// See [`ContractData::build_helper`] for more details.
193pub(crate) fn create_deploy_helpers(data: &BTreeMap<ContractId, ContractData>) -> Sources {
194    let mut deploy_helpers = Sources::new();
195    for (contract_id, contract) in data {
196        if let Some(code) = contract.build_helper() {
197            let path = format!("foundry-pp/DeployHelper{}.sol", contract_id.index());
198            deploy_helpers.insert(path.into(), Source::new(code));
199        }
200    }
201    deploy_helpers
202}