#![allow(dead_code)]
use super::common::*;
use neo_devpack_solidity::cli::compile_contracts;
use neo_devpack_solidity::neo::{build_nef_with_tokens, parse_nef};
use neo_devpack_solidity::runtime::types::StackItem;
use neo_devpack_solidity::runtime::{NeoRuntime, RuntimeConfig};
use proptest::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SimpleType {
Uint256,
Bool,
Address,
Bytes32,
BytesMemory, }
impl SimpleType {
fn solidity_param(self) -> &'static str {
match self {
SimpleType::Uint256 => "uint256",
SimpleType::Bool => "bool",
SimpleType::Address => "address",
SimpleType::Bytes32 => "bytes32",
SimpleType::BytesMemory => "bytes memory",
}
}
fn solidity_return(self) -> &'static str {
self.solidity_param()
}
fn default_return_literal(self) -> &'static str {
match self {
SimpleType::Uint256 => "42",
SimpleType::Bool => "true",
SimpleType::Address => "address(0)",
SimpleType::Bytes32 => "bytes32(0)",
SimpleType::BytesMemory => "new bytes(0)",
}
}
fn default_stack_item(self) -> StackItem {
match self {
SimpleType::Uint256 => StackItem::Integer(0i64),
SimpleType::Bool => StackItem::Boolean(false),
SimpleType::Address => StackItem::byte_array(vec![0u8; 20]),
SimpleType::Bytes32 => StackItem::byte_array(vec![0u8; 32]),
SimpleType::BytesMemory => StackItem::byte_array(Vec::new()),
}
}
}
fn param_type_strategy() -> impl Strategy<Value = SimpleType> {
prop_oneof![
Just(SimpleType::Uint256),
Just(SimpleType::Bool),
Just(SimpleType::Address),
Just(SimpleType::Bytes32),
Just(SimpleType::BytesMemory),
]
}
fn return_type_strategy() -> impl Strategy<Value = SimpleType> {
param_type_strategy()
}
#[derive(Debug, Clone)]
struct MethodSpec {
name: String,
params: Vec<SimpleType>,
ret: SimpleType,
}
impl MethodSpec {
fn render(&self) -> String {
let mut params = String::new();
for (i, ty) in self.params.iter().enumerate() {
if i > 0 {
params.push_str(", ");
}
params.push_str(ty.solidity_param());
params.push_str(&format!(" p{i}"));
}
format!(
" function {name}({params}) external pure returns ({ret}) {{\n\
\x20 return {lit};\n\
\x20 }}\n",
name = self.name,
params = params,
ret = self.ret.solidity_return(),
lit = self.ret.default_return_literal(),
)
}
fn default_args(&self) -> Vec<StackItem> {
self.params.iter().map(|t| t.default_stack_item()).collect()
}
}
fn method_spec_strategy(
name_strategy: impl Strategy<Value = String> + Clone,
) -> impl Strategy<Value = MethodSpec> {
(
name_strategy,
prop::collection::vec(param_type_strategy(), 0..=2),
return_type_strategy(),
)
.prop_map(|(name, params, ret)| MethodSpec { name, params, ret })
}
fn build_contract_source(n_state_vars: usize, methods: &[MethodSpec]) -> String {
let mut src = String::new();
src.push_str("// SPDX-License-Identifier: MIT\n");
src.push_str("pragma solidity ^0.8.19;\n");
src.push_str("contract C {\n");
for i in 0..n_state_vars {
src.push_str(&format!(" uint256 public s{i};\n"));
}
for m in methods {
src.push_str(&m.render());
}
src.push_str("}\n");
src
}
fn is_known_exception_shape(msg: &str) -> bool {
if msg.contains("Panic") {
return true;
}
if msg.contains("require failed") || msg.contains("Error(string)") || msg.contains("revert") {
return true;
}
if msg.contains("abi.decode")
|| msg.contains("Decode")
|| msg.contains("decode")
|| msg.contains("ABI")
{
return true;
}
if msg.contains("THROW") || msg.contains("Execution failed") {
return true;
}
false
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(20))]
#[test]
fn compile_runtime_roundtrip(
n_state_vars in 0usize..=4,
n_methods in 1usize..=4,
method_name in identifier_strategy(),
) {
let mut method_specs: Vec<MethodSpec> = Vec::with_capacity(n_methods);
let collides_with_state_var = method_name
.strip_prefix('s')
.and_then(|tail| tail.parse::<usize>().ok())
.map(|i| i < n_state_vars)
.unwrap_or(false);
let collides_with_synth_method = matches!(method_name.as_str(), "m1" | "m2" | "m3");
let first_method_name = if collides_with_state_var || collides_with_synth_method {
format!("f_{method_name}")
} else {
method_name.clone()
};
method_specs.push(MethodSpec {
name: first_method_name,
params: Vec::new(), ret: SimpleType::Uint256, });
for i in 1..n_methods {
let candidate = format!("m{i}");
let final_name = if candidate == method_name {
format!("m_extra_{i}")
} else {
candidate
};
method_specs.push(MethodSpec {
name: final_name,
params: Vec::new(),
ret: SimpleType::Uint256,
});
}
for (i, spec) in method_specs.iter_mut().enumerate() {
let arity = i % 3; let ret_idx = (i + n_state_vars) % 5;
let ret_type = match ret_idx {
0 => SimpleType::Uint256,
1 => SimpleType::Bool,
2 => SimpleType::Address,
3 => SimpleType::Bytes32,
_ => SimpleType::BytesMemory,
};
let params: Vec<SimpleType> = (0..arity)
.map(|j| {
let p_idx = (i * 7 + j * 3) % 5;
match p_idx {
0 => SimpleType::Uint256,
1 => SimpleType::Bool,
2 => SimpleType::Address,
3 => SimpleType::Bytes32,
_ => SimpleType::BytesMemory,
}
})
.collect();
spec.params = params;
spec.ret = ret_type;
}
let source = build_contract_source(n_state_vars, &method_specs);
let arts = compile_contracts(&source, false, 2)
.unwrap_or_else(|e| panic!(
"compile failed for generated contract:\n{source}\nerror: {e:?}"
));
prop_assert!(!arts.is_empty(), "compile produced no artifacts");
let art = &arts[0];
let nef = build_nef_with_tokens(
&art.bytecode,
"neo-devpack-solidity-fuzz",
"",
&art.tokens,
).expect("NEF must build for compiled artifact");
prop_assert!(
nef.starts_with(b"NEF3"),
"NEF must start with NEF3 magic; got {:02x?}",
&nef[..nef.len().min(4)]
);
let parsed = parse_nef(&nef)
.unwrap_or_else(|e| panic!("parse_nef failed for generated artifact: {e}"));
prop_assert_eq!(
&parsed.script, &art.bytecode,
"parsed NEF script payload differs from compiled bytecode"
);
prop_assert_eq!(
parsed.tokens.len(),
art.tokens.len(),
"parsed NEF token count differs from compiled token count"
);
let manifest_str = serde_json::to_string(&art.manifest)
.expect("manifest must serialize to JSON");
let reparsed: serde_json::Value = serde_json::from_str(&manifest_str)
.expect("manifest JSON must reparse");
prop_assert_eq!(&reparsed, &art.manifest, "manifest JSON round-trip was lossy");
let methods = art.manifest
.get("abi").expect("manifest.abi present")
.get("methods").expect("manifest.abi.methods present")
.as_array().expect("manifest.abi.methods must be an array");
for spec in &method_specs {
let found = methods.iter().any(|m| {
m.get("name").and_then(serde_json::Value::as_str) == Some(spec.name.as_str())
});
prop_assert!(
found,
"declared method '{}' missing from manifest.abi.methods",
spec.name
);
}
let mut rt = NeoRuntime::new(RuntimeConfig::default())
.expect("NeoRuntime construction must not fail");
for spec in &method_specs {
let args = spec.default_args();
let result = rt.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
&spec.name,
&args,
);
let exec = result.unwrap_or_else(|e| panic!(
"host-level error from call_method('{}'); inputs n_state_vars={}, n_methods={}, method_name={}; err={:?}\nsource:\n{}",
spec.name, n_state_vars, n_methods, method_name, e, source
));
if !exec.success {
let exc = exec.exception.as_ref().unwrap_or_else(|| panic!(
"method '{}' returned success=false but no exception was populated; inputs n_state_vars={}, n_methods={}, method_name={}\nsource:\n{}",
spec.name, n_state_vars, n_methods, method_name, source
));
prop_assert!(
is_known_exception_shape(&exc.message),
"unexpected exception shape from method '{}': {:?}; inputs n_state_vars={}, n_methods={}, method_name={}; source:\n{}",
spec.name, exc.message, n_state_vars, n_methods, method_name, source
);
}
}
}
}