neo-decompiler 0.11.0

Neo N3 NEF decompiler: parse, disassemble, lift bytecode to high-level pseudocode and C# skeletons, with a CLI, JSON reports, and optional WebAssembly bindings.
Documentation
use super::*;
use crate::instruction::OpCode;

#[test]
fn csharp_contract_call_lifts_to_native_method_form() {
    // The C# renderer shares the high-level emitter, so the
    // `GasToken.transfer(args)` expansion from the high-level view must
    // also surface in the C# output. Without this, the C# body would
    // show the opaque `syscall("System.Contract.Call", hash, "transfer",
    // 15, args)` form and the reader would have to mentally decode the
    // method call themselves.
    let gas_token_hash: [u8; 20] = [
        0xCF, 0x76, 0xE2, 0x8B, 0xD0, 0x06, 0x2C, 0x4A, 0x47, 0x8E, 0xE3, 0x55, 0x61, 0x01, 0x13,
        0x19, 0xF3, 0xCF, 0xA4, 0xD2,
    ];
    let mut script = vec![
        OpCode::Newarray0.byte(),
        OpCode::Pushint8.byte(),
        0x0F,
        OpCode::Pushdata1.byte(),
        0x08,
    ];
    script.extend_from_slice(b"transfer");
    script.extend_from_slice(&[OpCode::Pushdata1.byte(), 0x14]);
    script.extend_from_slice(&gas_token_hash);
    script.extend_from_slice(&[
        OpCode::Syscall.byte(),
        0x62,
        0x7D,
        0x5B,
        0x52, // System.Contract.Call
        OpCode::Ret.byte(),
    ]);

    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("GasToken.transfer("),
        "C# output must surface the native contract call as `GasToken.transfer(args)`: {csharp}"
    );
    assert!(
        !csharp.contains("syscall(\"System.Contract.Call\""),
        "C# output must not surface the raw contract-call syscall: {csharp}"
    );
}

#[test]
fn csharp_synthetic_script_entry_exposes_initslot_args_and_preserves_return() {
    // Bytecode: INITSLOT 0,1; LDARG0; ISNULL; NOT; RET — declares one
    // argument and returns its non-null-ness. Without a manifest, the
    // C# emitter must (a) surface the arg as a parameter so the body's
    // `arg0` reference resolves, and (b) preserve the lifted return
    // value (the previous hardcoded `void` signature silently dropped
    // it).
    let nef_bytes = build_nef(&[
        OpCode::Initslot.byte(),
        0x00,
        0x01,
        OpCode::Ldarg0.byte(),
        OpCode::Isnull.byte(),
        OpCode::Not.byte(),
        OpCode::Ret.byte(),
    ]);
    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("public static object ScriptEntry(object arg0)"),
        "synthetic ScriptEntry should declare INITSLOT-counted args: {csharp}"
    );
    // Verbose-mode (lib API default) keeps the temp; clean-mode would
    // inline. Either form preserves the return value, which is the
    // bug we care about — the previous `void` signature dropped it.
    assert!(
        csharp.contains("return !t0;") || csharp.contains("return !(arg0 is null);"),
        "lifted return value should be preserved (not dropped via void signature): {csharp}"
    );
    // Also verify the body actually references arg0 (not just the
    // signature) so the param isn't unused boilerplate.
    assert!(
        csharp.contains("(arg0 is null)") || csharp.contains("is_null(arg0)"),
        "body should reference arg0 from the new parameter: {csharp}"
    );
}

#[test]
fn csharp_omits_trailing_return_in_void_methods() {
    // Smallest script: a single RET. Without a manifest the synthetic
    // ScriptEntry now defaults to `object` return (so any pushed value
    // is preserved instead of dropped) — for a bare RET that produces
    // `return default;` in the body. The historical behavior (`void`
    // signature with `return;` stripped) was buggy for non-void scripts
    // (it silently discarded the lifted return value).
    let nef_bytes = build_nef(&[OpCode::Ret.byte()]);
    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("public static object ScriptEntry()"),
        "synthetic ScriptEntry should default to object return when no manifest is provided: {csharp}"
    );
    // Bare RET on empty stack lifts to `return;`. With a non-void
    // signature the C# render rewrites that to `return default;` to
    // satisfy the type system.
    assert!(
        csharp.contains("return default;"),
        "synthetic ScriptEntry with bare RET should yield `return default;`: {csharp}"
    );
}

#[test]
fn csharp_keeps_explicit_return_value_in_non_void_methods() {
    // Script: PUSH1 RET — high-level lifts as `return 1;`, which the C#
    // emitter must preserve since the method is non-void.
    let nef_bytes = build_nef(&[OpCode::Push1.byte(), OpCode::Ret.byte()]);
    let manifest = ContractManifest::from_json_str(
        r#"
            {
                "name": "ReturnsInt",
                "abi": {
                    "methods": [
                        {
                            "name": "main",
                            "parameters": [],
                            "returntype": "Integer",
                            "offset": 0
                        }
                    ],
                    "events": []
                },
                "permissions": [],
                "trusts": "*"
            }
            "#,
    )
    .expect("manifest parsed");

    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("return 1;"),
        "non-void method should keep its computed return: {csharp}"
    );
}

#[test]
fn csharp_non_void_method_with_empty_body_throws_not_implemented() {
    // `prologue` spans [0,3) = INITSLOT only, which lifts to no statements.
    // A non-void (Integer) return with no body is C# error CS0161, so the
    // renderer must emit a throwing stub rather than a bare comment.
    let nef_bytes = build_nef(&[
        OpCode::Initslot.byte(),
        0x00,
        0x00,
        OpCode::Push1.byte(),
        OpCode::Push2.byte(),
        OpCode::Add.byte(),
        OpCode::Ret.byte(),
        OpCode::Push16.byte(),
        OpCode::Ret.byte(),
    ]);
    let manifest = ContractManifest::from_json_str(
        r#"{"name":"Demo","abi":{"methods":[
            {"name":"prologue","returntype":"Integer","offset":0,"parameters":[],"safe":false},
            {"name":"body","returntype":"Integer","offset":3,"parameters":[],"safe":false}
        ],"events":[]}}"#,
    )
    .expect("manifest parsed");
    // Clean mode (trace comments off) is the user-facing compilable C# output —
    // the mode the CLI `decompile --format csharp` emits. There INITSLOT lifts
    // to no statements, so `prologue`'s body is empty.
    let decompilation = Decompiler::new()
        .with_trace_comments(false)
        .decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::All)
        .expect("decompile succeeds");
    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("BigInteger prologue()"),
        "prologue should render with its declared return type: {csharp}"
    );
    assert!(
        csharp.contains("throw new NotImplementedException();"),
        "non-void method with an empty lifted body must throw, not emit a bare comment: {csharp}"
    );
}