Skip to main content

neo_devpack_solidity/cli/cli_parts/cli_compile/
compile.rs

1pub fn compile_contracts(
2    source: &str,
3    verbose: bool,
4    optimizer_level: u8,
5) -> Result<Vec<CompilationArtifacts>, CompileError> {
6    compile_contracts_with_options(
7        source,
8        verbose,
9        CompileOptions {
10            optimizer_level,
11            use_callt: false,
12            deny_wildcard_permissions: false,
13            deny_wildcard_contracts: false,
14            deny_wildcard_methods: false,
15            manifest_permissions: None,
16        },
17    )
18}
19
20
21fn apply_manifest_permissions_override(
22    manifest: &mut serde_json::Value,
23    metadata: &ContractMetadata,
24    override_permissions: &ManifestPermissionsOverride,
25) -> Result<(), CompileError> {
26    let mut inferred = parse_manifest_permissions_from_manifest(manifest).map_err(|err| {
27        CompileError::Message(format!("Failed to parse inferred manifest permissions: {err}"))
28    })?;
29
30    match override_permissions.mode {
31        ManifestPermissionsMode::Merge => {
32            merge_manifest_permissions(&mut inferred, &override_permissions.permissions);
33        }
34        ManifestPermissionsMode::ReplaceWildcards => {
35            let had_wildcards = inferred
36                .iter()
37                .any(|(contract, methods)| contract == "*" || methods.is_wildcard());
38
39            let is_empty = override_permissions.permissions.is_empty();
40
41            if !had_wildcards && !is_empty {
42                return Err(CompileError::Manifest(format!(
43                    "contract '{}' specifies a --manifest-permissions override with `replace-wildcards` mode, but the inferred manifest has no wildcards to replace. Use `merge` mode to append explicit entries, or `replace-wildcards` only for dynamically-calling contracts.",
44                    metadata.name
45                )));
46            }
47
48            if had_wildcards && is_empty {
49                return Err(CompileError::Manifest(format!(
50                    "contract '{}' specifies a --manifest-permissions override with `replace-wildcards` mode, but the override is empty. An explicit permission allowlist must be provided to replace the dynamic wildcards.",
51                    metadata.name
52                )));
53            }
54
55            inferred.retain(|contract, methods| contract != "*" && !methods.is_wildcard());
56            merge_manifest_permissions(&mut inferred, &override_permissions.permissions);
57        }
58    }
59
60    manifest["permissions"] = manifest_permissions_to_json(inferred);
61    Ok(())
62}
63
64pub fn compile_contracts_with_options(
65    source: &str,
66    verbose: bool,
67    options: CompileOptions,
68) -> Result<Vec<CompilationArtifacts>, CompileError> {
69    let metadatas = analyse_all_sources(source).map_err(|err| match err {
70        // Preserve structured parser diagnostics so standard-JSON emits one
71        // error per diagnostic with a precise sourceLocation, rather than
72        // collapsing them into a single opaque Generic error.
73        neo_devpack_solidity::solidity::SolidityError::Frontend(
74            neo_devpack_solidity::frontend::FrontendError::ParseDiagnostics(diags),
75        ) => CompileError::ParseErrors(diags),
76        other => CompileError::Message(other.to_string()),
77    })?;
78
79    metadatas
80        .into_iter()
81        .map(|metadata| compile_metadata(metadata, verbose, options.clone()))
82        .collect()
83}
84
85fn compile_metadata(
86    mut metadata: ContractMetadata,
87    verbose: bool,
88    options: CompileOptions,
89) -> Result<CompilationArtifacts, CompileError> {
90    fn manifest_allows_permission(manifest: &Value, contract: &str, method: &str) -> bool {
91        let permissions = manifest
92            .get("permissions")
93            .and_then(|v| v.as_array())
94            .map(|v| v.as_slice())
95            .unwrap_or(&[]);
96
97        permissions.iter().any(|entry| {
98            let Some(entry_contract) = entry.get("contract").and_then(|v| v.as_str()) else {
99                return false;
100            };
101            let methods_value = entry.get("methods").unwrap_or(&Value::Null);
102
103            let contract_matches =
104                entry_contract == "*" || entry_contract.eq_ignore_ascii_case(contract);
105            if !contract_matches {
106                return false;
107            }
108
109            match methods_value {
110                Value::String(s) => s == "*",
111                Value::Array(arr) => arr
112                    .iter()
113                    .filter_map(|v| v.as_str())
114                    .any(|name| name == method),
115                _ => false,
116            }
117        })
118    }
119
120    let optimizer_level = options.optimizer_level.min(3);
121    let diagnostics = validate_contract(&metadata);
122    let mut errors = Vec::new();
123    let mut warnings = Vec::new();
124    for diagnostic in diagnostics {
125        match diagnostic.severity {
126            DiagnosticSeverity::Warning => warnings.push(diagnostic),
127            DiagnosticSeverity::Error => errors.push(diagnostic),
128        }
129    }
130
131    if !errors.is_empty() {
132        return Err(CompileError::Diagnostics(errors));
133    }
134
135    if let Err(diags) = build_semantic_model(&metadata) {
136        return Err(CompileError::Semantic(diags));
137    }
138
139    ensure_deploy_stub(&mut metadata)?;
140    let has_parameterised_constructor = metadata.methods.iter().any(|m| {
141        matches!(m.kind, FunctionKind::Constructor) && !m.parameters.is_empty()
142    });
143
144    let (ir_module, ir_warnings) = ir::Module::from_contract_with_warnings(&metadata).map_err(CompileError::Ir)?;
145    warnings.extend(ir_warnings);
146    let ir_module = optimize_ir(ir_module, optimizer_level);
147
148    if verbose {
149        println!(
150            "Semantic model built: {} functions, {} state variables",
151            ir_module.functions.len(),
152            ir_module.state_variables.len()
153        );
154
155        for function in &ir_module.functions {
156            let instruction_count: usize = function
157                .basic_blocks
158                .iter()
159                .map(|block| block.instructions.len())
160                .sum();
161            println!(
162                "   • IR function '{}' (kind: {:?}) => {} instruction(s)",
163                function.name, function.kind, instruction_count
164            );
165        }
166    }
167
168    // Pass optimizer_level to bytecode generation for optimization passes.
169    let bytecode_output = generate_contract_bytecode(
170        &mut metadata,
171        &ir_module,
172        verbose,
173        optimizer_level,
174        options.use_callt,
175    )
176    .map_err(CompileError::Message)?;
177    let mut manifest = build_manifest(&metadata, &ir_module)?;
178
179    if let Some(source_override) = load_manifest_permissions_override_from_natspec(&metadata)
180        .map_err(CompileError::Message)?
181    {
182        apply_manifest_permissions_override(&mut manifest, &metadata, &source_override)?;
183    }
184
185    if let Some(override_permissions) = &options.manifest_permissions {
186        apply_manifest_permissions_override(&mut manifest, &metadata, override_permissions)?;
187    }
188
189    if has_parameterised_constructor {
190        let stdlib_hash_le = bytecode::native_contract_hash(ir::NativeContract::StdLib);
191        let stdlib_hash_be = stdlib_hash_le.iter().rev().copied().collect::<Vec<_>>();
192        let stdlib_contract = format!("0x{}", hex::encode(stdlib_hash_be));
193
194        let has_json_deserialize =
195            manifest_allows_permission(&manifest, &stdlib_contract, "jsonDeserialize");
196        let has_deserialize = manifest_allows_permission(&manifest, &stdlib_contract, "deserialize");
197
198        if !(has_json_deserialize && has_deserialize) {
199            warnings.push(neo_devpack_solidity::solidity::Diagnostic::warning(format!(
200                    "contract '{}' has a parameterised constructor; deploy it by passing constructor args through `_deploy(data, update)`. Neo-Express: pass a JSON array string via `-d '[7]'`; SDKs that support StackItems may pass an Array directly. The injected deploy prologue uses `StdLib.jsonDeserialize` with a `StdLib.deserialize` fallback, so the manifest must allow these methods.",
201                    metadata.name
202                )));
203        }
204    }
205
206    let (
207        has_wildcard_contract,
208        has_wildcard_methods,
209        has_full_wildcard_permissions,
210        wildcard_contract_only_nep_callbacks,
211    ) = manifest["permissions"]
212        .as_array()
213        .map(|permissions| {
214            let mut wildcard_contract = false;
215            let mut wildcard_methods = false;
216            let mut full_wildcard = false;
217            let mut wildcard_contract_only_nep_callbacks = true;
218            const NEP_CALLBACK_METHODS: [&str; 3] = ["onNEP11Payment", "onNEP17Payment", "onOracleResponse"];
219            for entry in permissions {
220                let contract_is_wildcard = entry["contract"] == "*";
221                let methods_is_wildcard = entry["methods"] == "*";
222                wildcard_contract |= contract_is_wildcard;
223                wildcard_methods |= methods_is_wildcard;
224                full_wildcard |= contract_is_wildcard && methods_is_wildcard;
225
226                if contract_is_wildcard {
227                    if methods_is_wildcard {
228                        wildcard_contract_only_nep_callbacks = false;
229                    } else if let Some(methods) = entry["methods"].as_array() {
230                        for method in methods {
231                            match method.as_str() {
232                                Some(name) if NEP_CALLBACK_METHODS.contains(&name) => {}
233                                _ => wildcard_contract_only_nep_callbacks = false,
234                            }
235                        }
236                    } else {
237                        wildcard_contract_only_nep_callbacks = false;
238                    }
239                }
240            }
241            if !wildcard_contract {
242                wildcard_contract_only_nep_callbacks = false;
243            }
244            (
245                wildcard_contract,
246                wildcard_methods,
247                full_wildcard,
248                wildcard_contract_only_nep_callbacks,
249            )
250        })
251        .unwrap_or((false, false, false, false));
252
253    if has_full_wildcard_permissions {
254        let message = format!(
255            "contract '{}' requires full wildcard manifest permissions (contract='*', methods='*') because at least one contract call is fully dynamic (unknown target + method) or could not be statically analysed. This is usually not acceptable for production deployments; prefer static calls or restrict the call surface, or provide an explicit allowlist using @custom:neo.manifest.permissions. Use --deny-wildcard-permissions to make this a hard error.",
256            metadata.name
257        );
258        if options.deny_wildcard_permissions
259            || options.deny_wildcard_contracts
260            || options.deny_wildcard_methods
261        {
262            return Err(CompileError::Manifest(message));
263        }
264        warnings.push(neo_devpack_solidity::solidity::Diagnostic::warning(message));
265    } else {
266        if has_wildcard_contract {
267            let message = format!(
268                "contract '{}' requires wildcard contract manifest permissions (contract='*') due to dynamic contract calls. This is riskier than fixed contract hashes; use --deny-wildcard-contracts to make this a hard error, or provide an explicit allowlist using @custom:neo.manifest.permissions.",
269                metadata.name
270            );
271            if options.deny_wildcard_contracts && !wildcard_contract_only_nep_callbacks {
272                return Err(CompileError::Manifest(message));
273            }
274            if !wildcard_contract_only_nep_callbacks {
275                warnings.push(neo_devpack_solidity::solidity::Diagnostic::warning(message));
276            }
277        }
278
279        if has_wildcard_methods {
280            let message = format!(
281                "contract '{}' requires wildcard method manifest permissions (methods='*') due to dynamic method calls. This is riskier than fixed method names; use --deny-wildcard-methods to make this a hard error, or provide an explicit allowlist using @custom:neo.manifest.permissions.",
282                metadata.name
283            );
284            if options.deny_wildcard_methods {
285                return Err(CompileError::Manifest(message));
286            }
287            warnings.push(neo_devpack_solidity::solidity::Diagnostic::warning(message));
288        }
289    }
290
291    Ok(CompilationArtifacts {
292        metadata,
293        bytecode: bytecode_output.script,
294        tokens: bytecode_output.tokens,
295        manifest,
296        warnings,
297    })
298}