#![allow(clippy::uninlined_format_args)]
use neo_devpack_solidity::cli::compile_contracts;
use neo_devpack_solidity::interop::interop_id_bytes;
use neo_devpack_solidity::runtime::types::StackItem;
use neo_devpack_solidity::runtime::{ExecutionResult, NeoRuntime, RuntimeConfig};
use num_bigint::BigUint;
fn test_runtime() -> NeoRuntime {
NeoRuntime::new(RuntimeConfig {
gas_limit: 1_000_000_000,
..RuntimeConfig::default()
})
.expect("runtime")
}
fn decode_uint_le(bytes: &[u8]) -> BigUint {
if bytes.is_empty() {
BigUint::from(0u8)
} else {
BigUint::from_bytes_le(bytes)
}
}
fn returned_uint(result: &ExecutionResult, what: &str) -> BigUint {
assert!(
result.success,
"{what} must succeed; exception={:?}",
result.exception.as_ref().map(|e| &e.message)
);
decode_uint_le(&result.return_data)
}
fn count_occurrences(haystack: &[u8], needle: &[u8]) -> usize {
if needle.is_empty() || haystack.len() < needle.len() {
return 0;
}
let mut count = 0;
let mut i = 0;
while i + needle.len() <= haystack.len() {
if &haystack[i..i + needle.len()] == needle {
count += 1;
i += needle.len();
} else {
i += 1;
}
}
count
}
fn find_positions(haystack: &[u8], needle: &[u8]) -> Vec<usize> {
let mut out = Vec::new();
if needle.is_empty() || haystack.len() < needle.len() {
return out;
}
for i in 0..=haystack.len() - needle.len() {
if &haystack[i..i + needle.len()] == needle {
out.push(i);
}
}
out
}
#[test]
fn mapping_key_serialize_is_preceded_by_canonicalizing_convert() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract MapKeys {
mapping(bytes => uint256) m;
function put(bytes memory a, bytes memory b, uint256 v) public {
m[bytes.concat(a, b)] = v;
}
function get(bytes memory k) public view returns (uint256) {
return m[k];
}
}"#;
let arts = compile_contracts(src, false, 0).expect("MapKeys must compile");
assert!(!arts.is_empty());
let bytecode = &arts[0].bytecode;
let mut needle = vec![0x0Cu8, 0x09];
needle.extend_from_slice(b"serialize");
let sites = find_positions(bytecode, &needle);
assert!(
sites.len() >= 2,
"expected serialize call sites for put+get, found {}",
sites.len()
);
for pos in &sites {
let window_start = pos.saturating_sub(16);
let window = &bytecode[window_start..*pos];
assert!(
count_occurrences(window, &[0xDB, 0x28]) >= 1,
"serialize call at byte offset {pos} is not preceded by CONVERT \
ByteString (0xDB 0x28); window={:?}",
window
);
}
let art = &arts[0];
let mut rt = test_runtime();
let a = StackItem::byte_array(vec![0xAA, 0xBB]);
let b = StackItem::byte_array(vec![0xCC]);
let r = rt
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"put",
&[a, b, StackItem::Integer(42)],
)
.expect("put host call");
assert!(
r.success,
"put failed: {:?}",
r.exception.map(|e| e.message)
);
let r = rt
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"get",
&[StackItem::byte_array(vec![0xAA, 0xBB, 0xCC])],
)
.expect("get host call");
assert_eq!(returned_uint(&r, "get"), BigUint::from(42u8));
}
#[test]
fn storage_put_enforces_neo_n3_value_size_limit() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
bytes data;
function set(bytes memory v) external { data = v; }
}"#;
let arts = compile_contracts(src, false, 2).expect("compile");
let art = &arts[0];
let mut rt = test_runtime();
let big = rt
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"set",
&[StackItem::byte_array(vec![0x5A; 70_000])],
)
.expect("host call");
assert!(
!big.success,
"storing a 70000-byte value must FAULT (exceeds Neo N3 MaxStorageValueSize)"
);
let mut rt2 = test_runtime();
let ok = rt2
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"set",
&[StackItem::byte_array(vec![0x5A; 1000])],
)
.expect("host call");
assert!(
ok.success,
"storing a 1000-byte value must succeed: {:?}",
ok.exception.map(|e| e.message)
);
}
#[test]
fn integer_mapping_key_serialize_uses_integer_convert() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract IntKeys {
mapping(uint256 => uint256) m;
function put(uint256 k, uint256 v) public { m[k] = v; }
function get(uint256 k) public view returns (uint256) { return m[k]; }
}"#;
let arts = compile_contracts(src, false, 0).expect("IntKeys must compile");
let bytecode = &arts[0].bytecode;
let mut needle = vec![0x0Cu8, 0x09];
needle.extend_from_slice(b"serialize");
let sites = find_positions(bytecode, &needle);
assert!(
sites.len() >= 2,
"expected serialize sites, found {}",
sites.len()
);
for pos in &sites {
let window = &bytecode[pos.saturating_sub(16)..*pos];
assert!(
count_occurrences(window, &[0xDB, 0x21]) >= 1,
"serialize call at offset {pos} missing CONVERT Integer (0xDB 0x21)"
);
}
}
#[test]
fn delete_fixed_size_storage_array_zeroes_all_elements() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract FixedDelete {
uint256[3] arr;
function setAll() public {
arr[0] = 11;
arr[1] = 22;
arr[2] = 33;
}
function wipe() public {
delete arr;
}
function get(uint256 i) public view returns (uint256) {
return arr[i];
}
}"#;
let arts = compile_contracts(src, false, 2).expect("FixedDelete must compile");
let art = &arts[0];
let mut rt = test_runtime();
let r = rt
.call_method(&art.bytecode, &art.tokens, &art.manifest, "setAll", &[])
.expect("setAll");
assert!(
r.success,
"setAll failed: {:?}",
r.exception.map(|e| e.message)
);
for (i, expected) in [(0i64, 11u64), (1, 22), (2, 33)] {
let r = rt
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"get",
&[StackItem::Integer(i)],
)
.expect("get pre-wipe");
assert_eq!(
returned_uint(&r, "get pre-wipe"),
BigUint::from(expected),
"arr[{i}] before delete"
);
}
let r = rt
.call_method(&art.bytecode, &art.tokens, &art.manifest, "wipe", &[])
.expect("wipe");
assert!(
r.success,
"wipe failed: {:?}",
r.exception.map(|e| e.message)
);
for i in 0i64..3 {
let r = rt
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"get",
&[StackItem::Integer(i)],
)
.expect("get post-wipe");
assert_eq!(
returned_uint(&r, "get post-wipe"),
BigUint::from(0u8),
"arr[{i}] must be zero after delete"
);
}
}
#[test]
fn delete_dynamic_storage_array_resets_length_and_stays_usable() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract DynDelete {
uint256[] arr;
function pushVal(uint256 v) public { arr.push(v); }
function wipe() public { delete arr; }
function len() public view returns (uint256) { return arr.length; }
function get(uint256 i) public view returns (uint256) { return arr[i]; }
function readAt(uint256 i) public returns (uint256) {
try this.get(i) returns (uint256 v) {
return v;
} catch Panic(uint256) {
return 0xdead;
}
}
}"#;
let arts = compile_contracts(src, false, 2).expect("DynDelete must compile");
let art = &arts[0];
let mut rt = test_runtime();
let call = |rt: &mut NeoRuntime, name: &str, args: &[StackItem]| {
rt.call_method(&art.bytecode, &art.tokens, &art.manifest, name, args)
.unwrap_or_else(|e| panic!("{name} host call failed: {e:?}"))
};
assert!(call(&mut rt, "pushVal", &[StackItem::Integer(7)]).success);
assert!(call(&mut rt, "pushVal", &[StackItem::Integer(9)]).success);
let r = call(&mut rt, "len", &[]);
assert_eq!(returned_uint(&r, "len pre-wipe"), BigUint::from(2u8));
let r = call(&mut rt, "get", &[StackItem::Integer(0)]);
assert_eq!(returned_uint(&r, "get(0) pre-wipe"), BigUint::from(7u8));
assert!(call(&mut rt, "wipe", &[]).success);
let r = call(&mut rt, "len", &[]);
assert_eq!(returned_uint(&r, "len post-wipe"), BigUint::from(0u8));
let r = call(&mut rt, "readAt", &[StackItem::Integer(0)]);
assert_eq!(
returned_uint(&r, "readAt(0) post-wipe"),
BigUint::from(0xdeadu32),
"post-delete read must panic, not return stale data"
);
assert!(call(&mut rt, "pushVal", &[StackItem::Integer(5)]).success);
let r = call(&mut rt, "len", &[]);
assert_eq!(returned_uint(&r, "len after re-push"), BigUint::from(1u8));
let r = call(&mut rt, "get", &[StackItem::Integer(0)]);
assert_eq!(
returned_uint(&r, "get(0) after re-push"),
BigUint::from(5u8)
);
}
#[test]
fn struct_field_dynamic_array_subscript_is_bounds_guarded() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract StructArr {
struct S { uint256 a; uint256[] arr; }
S s;
function setup() public {
s.a = 1;
s.arr.push(11);
s.arr.push(22);
}
function len() public view returns (uint256) { return s.arr.length; }
function get(uint256 i) public view returns (uint256) { return s.arr[i]; }
function readAt(uint256 i) public returns (uint256) {
try this.get(i) returns (uint256 v) {
return v;
} catch Panic(uint256) {
return 0xdead;
}
}
function wipe() public { delete s; }
}"#;
let arts = compile_contracts(src, false, 2).expect("StructArr must compile");
let art = &arts[0];
let mut rt = test_runtime();
let call = |rt: &mut NeoRuntime, name: &str, args: &[StackItem]| {
rt.call_method(&art.bytecode, &art.tokens, &art.manifest, name, args)
.unwrap_or_else(|e| panic!("{name} host call failed: {e:?}"))
};
assert!(call(&mut rt, "setup", &[]).success);
let r = call(&mut rt, "len", &[]);
assert_eq!(returned_uint(&r, "len"), BigUint::from(2u8));
let r = call(&mut rt, "readAt", &[StackItem::Integer(0)]);
assert_eq!(returned_uint(&r, "readAt(0)"), BigUint::from(11u8));
let r = call(&mut rt, "readAt", &[StackItem::Integer(1)]);
assert_eq!(returned_uint(&r, "readAt(1)"), BigUint::from(22u8));
let r = call(&mut rt, "readAt", &[StackItem::Integer(5)]);
assert_eq!(
returned_uint(&r, "readAt(5)"),
BigUint::from(0xdeadu32),
"OOB struct-field array read must Panic(0x32)"
);
assert!(call(&mut rt, "wipe", &[]).success);
let r = call(&mut rt, "len", &[]);
assert_eq!(returned_uint(&r, "len post-delete"), BigUint::from(0u8));
for i in 0i64..2 {
let r = call(&mut rt, "readAt", &[StackItem::Integer(i)]);
assert_eq!(
returned_uint(&r, "readAt post-delete"),
BigUint::from(0xdeadu32),
"deleted struct-array element {i} must not be readable"
);
}
}
#[test]
fn struct_field_fixed_size_array_uses_declared_bound() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract StructFixed {
struct F { uint256 x; uint256[3] vals; }
F f;
function setVals() public {
f.vals[0] = 5;
f.vals[1] = 6;
f.vals[2] = 7;
}
function get(uint256 i) public view returns (uint256) { return f.vals[i]; }
function readAt(uint256 i) public returns (uint256) {
try this.get(i) returns (uint256 v) {
return v;
} catch Panic(uint256) {
return 0xdead;
}
}
}"#;
let arts = compile_contracts(src, false, 2).expect("StructFixed must compile");
let art = &arts[0];
let mut rt = test_runtime();
let call = |rt: &mut NeoRuntime, name: &str, args: &[StackItem]| {
rt.call_method(&art.bytecode, &art.tokens, &art.manifest, name, args)
.unwrap_or_else(|e| panic!("{name} host call failed: {e:?}"))
};
assert!(call(&mut rt, "setVals", &[]).success);
for (i, expected) in [(0i64, 5u64), (1, 6), (2, 7)] {
let r = call(&mut rt, "readAt", &[StackItem::Integer(i)]);
assert_eq!(
returned_uint(&r, "readAt in-range"),
BigUint::from(expected),
"f.vals[{i}] in-range read"
);
}
let r = call(&mut rt, "readAt", &[StackItem::Integer(3)]);
assert_eq!(
returned_uint(&r, "readAt(3)"),
BigUint::from(0xdeadu32),
"read past the fixed bound must Panic(0x32)"
);
}
#[test]
fn delete_struct_with_mapping_member_behaves_like_solidity() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SlotDelete {
struct Slot { uint256 amount; mapping(address => uint256) balances; }
mapping(uint256 => Slot) slots;
function setAmount(uint256 k, uint256 v) public { slots[k].amount = v; }
function setBal(uint256 k, address a, uint256 v) public { slots[k].balances[a] = v; }
function del(uint256 k) public { delete slots[k]; }
function getAmount(uint256 k) public view returns (uint256) { return slots[k].amount; }
function getBal(uint256 k, address a) public view returns (uint256) { return slots[k].balances[a]; }
}"#;
let arts = compile_contracts(src, false, 2).expect("SlotDelete must compile");
let art = &arts[0];
let mut rt = test_runtime();
let addr = StackItem::byte_array(vec![0x11u8; 20]);
let call = |rt: &mut NeoRuntime, name: &str, args: &[StackItem]| {
rt.call_method(&art.bytecode, &art.tokens, &art.manifest, name, args)
.unwrap_or_else(|e| panic!("{name} host call failed: {e:?}"))
};
assert!(
call(
&mut rt,
"setAmount",
&[StackItem::Integer(1), StackItem::Integer(42)]
)
.success
);
assert!(
call(
&mut rt,
"setBal",
&[StackItem::Integer(1), addr.clone(), StackItem::Integer(99)]
)
.success
);
let r = call(&mut rt, "del", &[StackItem::Integer(1)]);
assert!(
r.success,
"delete slots[k] must not fault: {:?}",
r.exception.map(|e| e.message)
);
let r = call(&mut rt, "getAmount", &[StackItem::Integer(1)]);
assert_eq!(returned_uint(&r, "getAmount"), BigUint::from(0u8));
let r = call(&mut rt, "getBal", &[StackItem::Integer(1), addr]);
assert_eq!(returned_uint(&r, "getBal"), BigUint::from(99u8));
}
#[test]
fn delete_struct_with_mapping_member_emits_no_put_for_mapping_field() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract DelOnly {
struct Slot { uint256 amount; mapping(address => uint256) balances; }
mapping(uint256 => Slot) slots;
function del(uint256 k) public { delete slots[k]; }
}"#;
let arts = compile_contracts(src, false, 0).expect("DelOnly must compile");
let bytecode = &arts[0].bytecode;
let mut put_needle = vec![0x41u8]; put_needle.extend_from_slice(&interop_id_bytes("System.Storage.Put"));
let puts = count_occurrences(bytecode, &put_needle);
assert_eq!(
puts, 1,
"delete on a struct with one value member + one mapping member must \
emit exactly one Storage.Put (the value member); found {puts}"
);
}
#[test]
fn sibling_merge_rejects_conflicting_state_variable_types() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Mock {
mapping(address => uint256) _bal;
function mint(address a, uint256 v) public { _bal[a] += v; }
function balanceOf(address a) public view returns (uint256) { return _bal[a]; }
}
contract Client {
uint256 _bal;
Mock public m;
function go() public { m = new Mock(); }
function setHostBal(uint256 v) public { _bal = v; }
}"#;
let result = compile_contracts(src, false, 2);
let err = match result {
Err(e) => format!("{e:?}"),
Ok(_) => panic!(
"conflicting same-named state variables across merged contracts \
must be a hard compile error (storage slots are name-keyed)"
),
};
assert!(
err.contains("_bal"),
"error should name the conflicting variable; got: {err}"
);
}
#[test]
fn sibling_merge_keeps_allowing_identical_state_variable_types() {
let src = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Mock {
mapping(address => uint256) _bal;
function mint(address a, uint256 v) public { _bal[a] += v; }
function balanceOf(address a) public view returns (uint256) { return _bal[a]; }
}
contract Client {
mapping(address => uint256) _bal;
Mock public m;
function go() public { m = new Mock(); }
}"#;
let arts = compile_contracts(src, false, 2)
.expect("identical-type same-name sibling state vars must keep compiling");
assert!(!arts.is_empty());
}