use super::*;
use crate::decompiler::analysis::call_graph::CallTarget;
use crate::instruction::OpCode;
#[test]
fn decompilation_includes_call_graph_syscalls() {
let script = [
OpCode::Syscall.byte(),
0xB7,
0xC3,
0x88,
0x03, OpCode::Ret.byte(), ];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
assert_eq!(decompilation.call_graph.edges.len(), 1);
let edge = &decompilation.call_graph.edges[0];
assert_eq!(edge.opcode, "SYSCALL");
match &edge.target {
CallTarget::Syscall {
hash,
name,
returns_value,
} => {
assert_eq!(*hash, 0x0388C3B7);
assert_eq!(
name.as_deref(),
Some("System.Runtime.GetTime"),
"expected syscall name to resolve"
);
assert!(*returns_value);
}
other => panic!("unexpected call target: {other:?}"),
}
}
#[test]
fn decompilation_includes_call_graph_internal_calls() {
let script = [
OpCode::Call.byte(),
0x04,
OpCode::Ret.byte(),
OpCode::Nop.byte(),
OpCode::Ret.byte(),
];
let nef_bytes = build_nef(&script);
let manifest = ContractManifest::from_json_str(
r#"
{
"name": "ExampleContract",
"supportedstandards": [],
"features": {},
"abi": {
"methods": [
{
"name": "main",
"parameters": [],
"returntype": "Void",
"offset": 0,
"safe": false
},
{
"name": "helper",
"parameters": [],
"returntype": "Void",
"offset": 4,
"safe": false
}
],
"events": []
},
"permissions": [],
"trusts": "*"
}
"#,
)
.expect("manifest parsed");
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::Pseudocode)
.expect("decompile succeeds");
assert_eq!(decompilation.call_graph.methods.len(), 2);
assert_eq!(decompilation.call_graph.methods[0].offset, 0);
assert_eq!(decompilation.call_graph.methods[0].name, "main");
assert_eq!(decompilation.call_graph.methods[1].offset, 4);
assert_eq!(decompilation.call_graph.methods[1].name, "helper");
assert_eq!(decompilation.call_graph.edges.len(), 1);
let edge = &decompilation.call_graph.edges[0];
assert_eq!(edge.opcode, "CALL");
assert_eq!(edge.call_offset, 0);
assert_eq!(edge.caller.offset, 0);
assert_eq!(edge.caller.name, "main");
match &edge.target {
CallTarget::Internal { method } => {
assert_eq!(method.offset, 4);
assert_eq!(method.name, "helper");
}
other => panic!("unexpected call target: {other:?}"),
}
}
#[test]
fn call_graph_resolves_relative_call_from_opcode_offset() {
let script = [OpCode::Call.byte(), 0x02, OpCode::Ret.byte()];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
assert_eq!(decompilation.call_graph.edges.len(), 1);
let edge = &decompilation.call_graph.edges[0];
assert_eq!(edge.opcode, "CALL");
assert_eq!(edge.call_offset, 0);
match &edge.target {
CallTarget::Internal { method } => {
assert_eq!(method.offset, 2);
assert_eq!(method.name, "sub_0x0002");
}
other => panic!("unexpected call target: {other:?}"),
}
}
#[test]
fn call_graph_out_of_range_call_target_is_unresolved() {
let script = [OpCode::Call.byte(), 0x7F, OpCode::Ret.byte()];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
assert_eq!(decompilation.call_graph.edges.len(), 1);
match &decompilation.call_graph.edges[0].target {
CallTarget::UnresolvedInternal { target } => assert_eq!(*target, 127),
other => panic!("out-of-range CALL target should be unresolved, got {other:?}"),
}
assert!(
decompilation
.call_graph
.methods
.iter()
.all(|method| method.offset != 127),
"out-of-range CALL must not fabricate a method: {:?}",
decompilation.call_graph.methods
);
}
#[test]
fn call_graph_out_of_range_calla_target_is_indirect() {
let script = [
OpCode::PushA.byte(),
0x7F,
0x00,
0x00,
0x00,
OpCode::CallA.byte(),
OpCode::Ret.byte(),
];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
let edge = decompilation
.call_graph
.edges
.iter()
.find(|edge| edge.opcode == "CALLA")
.expect("CALLA edge present");
assert!(
matches!(edge.target, CallTarget::Indirect { .. }),
"out-of-range CALLA target should be Indirect, got {:?}",
edge.target
);
assert!(
decompilation
.call_graph
.methods
.iter()
.all(|method| method.offset != 127),
"out-of-range CALLA must not fabricate a method: {:?}",
decompilation.call_graph.methods
);
}
#[test]
fn decompilation_includes_call_graph_method_tokens() {
let script = [OpCode::CallT.byte(), 0x00, 0x00, OpCode::Ret.byte()];
let hash: [u8; 20] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13,
];
let nef_bytes = build_nef_with_single_token(&script, hash, "transfer", 2, true, 0x0F);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
assert_eq!(decompilation.call_graph.edges.len(), 1);
let edge = &decompilation.call_graph.edges[0];
assert_eq!(edge.opcode, "CALLT");
match &edge.target {
CallTarget::MethodToken {
index,
hash_le,
hash_be,
method,
parameters_count,
has_return_value,
call_flags,
call_flags_description,
} => {
assert_eq!(*index, 0);
assert_eq!(method, "transfer");
assert_eq!(*parameters_count, 2);
assert!(*has_return_value);
assert_eq!(*call_flags, 0x0F);
assert_eq!(
call_flags_description,
"ReadStates|WriteStates|AllowCall|AllowNotify"
);
let expected_le = hex::encode_upper(hash);
let mut reversed = hash;
reversed.reverse();
let expected_be = hex::encode_upper(reversed);
assert_eq!(hash_le, &expected_le);
assert_eq!(hash_be, &expected_be);
}
other => panic!("unexpected call target: {other:?}"),
}
}
#[test]
fn decompilation_includes_indirect_calls() {
let script = [
OpCode::CallA.byte(),
OpCode::CallT.byte(),
0x01,
0x00,
OpCode::Ret.byte(),
];
let hash = [0x42u8; 20];
let nef_bytes = build_nef_with_single_token(&script, hash, "stub", 0, false, 0x0F);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
assert_eq!(decompilation.call_graph.edges.len(), 2);
assert_eq!(decompilation.call_graph.edges[0].opcode, "CALLA");
match &decompilation.call_graph.edges[0].target {
CallTarget::Indirect { opcode, operand } => {
assert_eq!(opcode, "CALLA");
assert_eq!(*operand, None);
}
other => panic!("unexpected call target: {other:?}"),
}
assert_eq!(decompilation.call_graph.edges[1].opcode, "CALLT");
match &decompilation.call_graph.edges[1].target {
CallTarget::Indirect { opcode, operand } => {
assert_eq!(opcode, "CALLT");
assert_eq!(*operand, Some(1));
}
other => panic!("unexpected call target: {other:?}"),
}
}
#[test]
fn decompilation_resolves_pusha_calla_to_internal_call_edge() {
let script = [
OpCode::PushA.byte(),
0x0A,
0x00,
0x00,
0x00, OpCode::CallA.byte(), OpCode::Ret.byte(), OpCode::Nop.byte(),
OpCode::Nop.byte(),
OpCode::Nop.byte(), OpCode::Initslot.byte(),
0x00,
0x00, OpCode::Ret.byte(), ];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
let edge = decompilation
.call_graph
.edges
.iter()
.find(|edge| edge.opcode == "CALLA")
.expect("CALLA edge present");
match &edge.target {
CallTarget::Internal { method } => {
assert_eq!(method.offset, 0x000A);
assert_eq!(method.name, "sub_0x000A");
}
other => panic!("expected resolved internal CALLA target, got: {other:?}"),
}
}
#[test]
fn decompilation_resolves_local_pointer_flow_into_calla_edge() {
let script = [
OpCode::PushA.byte(),
0x09,
0x00,
0x00,
0x00, OpCode::Stloc0.byte(), OpCode::Ldloc0.byte(), OpCode::CallA.byte(), OpCode::Ret.byte(), OpCode::Initslot.byte(),
0x00,
0x00, OpCode::Ret.byte(), ];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
let edge = decompilation
.call_graph
.edges
.iter()
.find(|edge| edge.opcode == "CALLA")
.expect("CALLA edge present");
match &edge.target {
CallTarget::Internal { method } => {
assert_eq!(method.offset, 0x0009);
assert_eq!(method.name, "sub_0x0009");
}
other => panic!("expected resolved local-flow CALLA target, got: {other:?}"),
}
}
#[test]
fn decompilation_resolves_local_pointer_flow_with_nop_before_calla() {
let script = [
OpCode::PushA.byte(),
0x0A,
0x00,
0x00,
0x00, OpCode::Stloc0.byte(), OpCode::Ldloc0.byte(), OpCode::Nop.byte(), OpCode::CallA.byte(), OpCode::Ret.byte(), OpCode::Initslot.byte(),
0x00,
0x00, OpCode::Ret.byte(), ];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
let edge = decompilation
.call_graph
.edges
.iter()
.find(|edge| edge.opcode == "CALLA")
.expect("CALLA edge present");
match &edge.target {
CallTarget::Internal { method } => {
assert_eq!(method.offset, 0x000A);
assert_eq!(method.name, "sub_0x000A");
}
other => panic!("expected resolved local-flow CALLA target, got: {other:?}"),
}
}
#[test]
fn decompilation_resolves_multi_hop_local_pointer_flow_into_calla_edge() {
let script = [
OpCode::PushA.byte(),
0x0C,
0x00,
0x00,
0x00, OpCode::Stloc0.byte(), OpCode::Ldloc0.byte(), OpCode::Stloc1.byte(), OpCode::Ldloc1.byte(), OpCode::CallA.byte(), OpCode::Ret.byte(), OpCode::Nop.byte(), OpCode::Initslot.byte(),
0x00,
0x00, OpCode::Ret.byte(), ];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
let edge = decompilation
.call_graph
.edges
.iter()
.find(|edge| edge.opcode == "CALLA")
.expect("CALLA edge present");
match &edge.target {
CallTarget::Internal { method } => {
assert_eq!(method.offset, 0x000C);
assert_eq!(method.name, "sub_0x000C");
}
other => panic!("expected resolved multi-hop local CALLA target, got: {other:?}"),
}
}
#[test]
fn decompilation_does_not_resolve_local_pointer_across_method_boundary() {
let script = [
OpCode::Initslot.byte(),
0x01,
0x00, OpCode::PushA.byte(),
0x0E,
0x00,
0x00,
0x00, OpCode::Stloc0.byte(), OpCode::Ret.byte(), OpCode::Initslot.byte(),
0x01,
0x00, OpCode::Ldloc0.byte(), OpCode::CallA.byte(), OpCode::Ret.byte(), OpCode::Nop.byte(), OpCode::Initslot.byte(),
0x00,
0x00, OpCode::Ret.byte(), ];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::Pseudocode)
.expect("decompile succeeds");
let edge = decompilation
.call_graph
.edges
.iter()
.find(|edge| edge.opcode == "CALLA")
.expect("CALLA edge present");
match &edge.target {
CallTarget::Indirect { opcode, operand } => {
assert_eq!(opcode, "CALLA");
assert_eq!(*operand, None);
}
other => panic!("expected cross-method local CALLA to remain indirect, got: {other:?}"),
}
}