neo_devpack_solidity/cli/cli_parts/cli_compile/
compile.rs1pub 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 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 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}