foundry_common/preprocessor/
data.rs

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