neo_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
20fn compile_contracts_with_options(
21    source: &str,
22    verbose: bool,
23    options: CompileOptions,
24) -> Result<Vec<CompilationArtifacts>, CompileError> {
25    let metadatas =
26        analyse_all_sources(source).map_err(|err| CompileError::Message(err.to_string()))?;
27
28    metadatas
29        .into_iter()
30        .map(|metadata| compile_metadata(metadata, verbose, options.clone()))
31        .collect()
32}
33
34fn compile_metadata(
35    mut metadata: ContractMetadata,
36    verbose: bool,
37    options: CompileOptions,
38) -> Result<CompilationArtifacts, CompileError> {
39    fn manifest_allows_permission(manifest: &Value, contract: &str, method: &str) -> bool {
40        let permissions = manifest
41            .get("permissions")
42            .and_then(|v| v.as_array())
43            .map(|v| v.as_slice())
44            .unwrap_or(&[]);
45
46        permissions.iter().any(|entry| {
47            let Some(entry_contract) = entry.get("contract").and_then(|v| v.as_str()) else {
48                return false;
49            };
50            let methods_value = entry.get("methods").unwrap_or(&Value::Null);
51
52            let contract_matches =
53                entry_contract == "*" || entry_contract.eq_ignore_ascii_case(contract);
54            if !contract_matches {
55                return false;
56            }
57
58            match methods_value {
59                Value::String(s) => s == "*",
60                Value::Array(arr) => arr
61                    .iter()
62                    .filter_map(|v| v.as_str())
63                    .any(|name| name == method),
64                _ => false,
65            }
66        })
67    }
68
69    let optimizer_level = options.optimizer_level.min(3);
70    let diagnostics = validate_contract(&metadata);
71    let mut errors = Vec::new();
72    let mut warnings = Vec::new();
73    for diagnostic in diagnostics {
74        match diagnostic.severity {
75            DiagnosticSeverity::Warning => warnings.push(diagnostic),
76            DiagnosticSeverity::Error => errors.push(diagnostic),
77        }
78    }
79
80    if !errors.is_empty() {
81        return Err(CompileError::Diagnostics(errors));
82    }
83
84    if let Err(diags) = build_semantic_model(&metadata) {
85        return Err(CompileError::Semantic(diags));
86    }
87
88    ensure_deploy_stub(&mut metadata)?;
89    let has_parameterised_constructor = metadata.methods.iter().any(|m| {
90        matches!(m.kind, FunctionKind::Constructor) && !m.parameters.is_empty()
91    });
92
93    let ir_module = ir::Module::from_contract(&metadata).map_err(CompileError::Ir)?;
94    let ir_module = optimize_ir(ir_module, optimizer_level);
95
96    if verbose {
97        println!(
98            "Semantic model built: {} functions, {} state variables",
99            ir_module.functions.len(),
100            ir_module.state_variables.len()
101        );
102
103        for function in &ir_module.functions {
104            let instruction_count: usize = function
105                .basic_blocks
106                .iter()
107                .map(|block| block.instructions.len())
108                .sum();
109            println!(
110                "   • IR function '{}' (kind: {:?}) => {} instruction(s)",
111                function.name, function.kind, instruction_count
112            );
113        }
114    }
115
116    // Pass optimizer_level to bytecode generation for optimization passes.
117    let bytecode_output = generate_contract_bytecode(
118        &mut metadata,
119        &ir_module,
120        verbose,
121        optimizer_level,
122        options.use_callt,
123    )
124    .map_err(CompileError::Message)?;
125    let mut manifest = build_manifest(&metadata, &ir_module);
126
127    if let Some(override_permissions) = &options.manifest_permissions {
128        let mut inferred = parse_manifest_permissions_from_manifest(&manifest).map_err(|err| {
129            CompileError::Message(format!("Failed to parse inferred manifest permissions: {err}"))
130        })?;
131
132        match override_permissions.mode {
133            ManifestPermissionsMode::Merge => {
134                merge_manifest_permissions(&mut inferred, &override_permissions.permissions);
135            }
136            ManifestPermissionsMode::ReplaceWildcards => {
137                inferred.retain(|contract, methods| contract != "*" && !methods.is_wildcard());
138                merge_manifest_permissions(&mut inferred, &override_permissions.permissions);
139            }
140        }
141
142        manifest["permissions"] = manifest_permissions_to_json(inferred);
143    }
144
145    if has_parameterised_constructor {
146        let stdlib_hash_le = bytecode::native_contract_hash(ir::NativeContract::StdLib);
147        let stdlib_hash_be = stdlib_hash_le.iter().rev().copied().collect::<Vec<_>>();
148        let stdlib_contract = format!("0x{}", hex::encode(stdlib_hash_be));
149
150        let has_json_deserialize =
151            manifest_allows_permission(&manifest, &stdlib_contract, "jsonDeserialize");
152        let has_deserialize = manifest_allows_permission(&manifest, &stdlib_contract, "deserialize");
153
154        if !(has_json_deserialize && has_deserialize) {
155            warnings.push(neo_solidity::solidity::Diagnostic::warning(format!(
156                    "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.",
157                    metadata.name
158                )));
159        }
160    }
161
162    let (
163        has_wildcard_contract,
164        has_wildcard_methods,
165        has_full_wildcard_permissions,
166        wildcard_contract_only_nep_callbacks,
167    ) = manifest["permissions"]
168        .as_array()
169        .map(|permissions| {
170            let mut wildcard_contract = false;
171            let mut wildcard_methods = false;
172            let mut full_wildcard = false;
173            let mut wildcard_contract_only_nep_callbacks = true;
174            const NEP_CALLBACK_METHODS: [&str; 3] = ["onNEP11Payment", "onNEP17Payment", "onOracleResponse"];
175            for entry in permissions {
176                let contract_is_wildcard = entry["contract"] == "*";
177                let methods_is_wildcard = entry["methods"] == "*";
178                wildcard_contract |= contract_is_wildcard;
179                wildcard_methods |= methods_is_wildcard;
180                full_wildcard |= contract_is_wildcard && methods_is_wildcard;
181
182                if contract_is_wildcard {
183                    if methods_is_wildcard {
184                        wildcard_contract_only_nep_callbacks = false;
185                    } else if let Some(methods) = entry["methods"].as_array() {
186                        for method in methods {
187                            match method.as_str() {
188                                Some(name) if NEP_CALLBACK_METHODS.contains(&name) => {}
189                                _ => wildcard_contract_only_nep_callbacks = false,
190                            }
191                        }
192                    } else {
193                        wildcard_contract_only_nep_callbacks = false;
194                    }
195                }
196            }
197            if !wildcard_contract {
198                wildcard_contract_only_nep_callbacks = false;
199            }
200            (
201                wildcard_contract,
202                wildcard_methods,
203                full_wildcard,
204                wildcard_contract_only_nep_callbacks,
205            )
206        })
207        .unwrap_or((false, false, false, false));
208
209    if has_full_wildcard_permissions {
210        let message = format!(
211            "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. Use --deny-wildcard-permissions to make this a hard error.",
212            metadata.name
213        );
214        if options.deny_wildcard_permissions
215            || options.deny_wildcard_contracts
216            || options.deny_wildcard_methods
217        {
218            return Err(CompileError::Manifest(message));
219        }
220        warnings.push(neo_solidity::solidity::Diagnostic::warning(message));
221    } else {
222        if has_wildcard_contract {
223            let message = format!(
224                "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.",
225                metadata.name
226            );
227            if options.deny_wildcard_contracts && !wildcard_contract_only_nep_callbacks {
228                return Err(CompileError::Manifest(message));
229            }
230            if !wildcard_contract_only_nep_callbacks {
231                warnings.push(neo_solidity::solidity::Diagnostic::warning(message));
232            }
233        }
234
235        if has_wildcard_methods {
236            let message = format!(
237                "contract '{}' requires wildcard method manifest permissions (methods='*') due to dynamic method names in contract calls. This is riskier than calling fixed methods; use --deny-wildcard-methods to make this a hard error.",
238                metadata.name
239            );
240            if options.deny_wildcard_methods {
241                return Err(CompileError::Manifest(message));
242            }
243            warnings.push(neo_solidity::solidity::Diagnostic::warning(message));
244        }
245    }
246
247    Ok(CompilationArtifacts {
248        metadata,
249        bytecode: bytecode_output.script,
250        tokens: bytecode_output.tokens,
251        manifest,
252        warnings,
253    })
254}