neo-decompiler 0.10.1

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
// Bytecode offset arithmetic requires isize/usize casts for signed jump deltas.
// NEF scripts are bounded (~1 MB), so these conversions are structurally safe.
#![allow(
    clippy::cast_possible_truncation,
    clippy::cast_possible_wrap,
    clippy::cast_sign_loss
)]

use std::collections::{BTreeMap, HashSet};

use crate::instruction::{Instruction, OpCode, Operand};
use crate::manifest::ContractManifest;
use crate::nef::{describe_call_flags, NefFile};
use crate::{syscalls, util};

use super::super::{MethodRef, MethodTable};
use super::arguments::resolve_ldarg_calla_targets;
use super::model::{CallEdge, CallGraph, CallTarget};
use super::pointers::calla_target_from_pusha;

/// Build a call graph for the provided instruction stream.
#[must_use]
pub fn build_call_graph(
    nef: &NefFile,
    instructions: &[Instruction],
    manifest: Option<&ContractManifest>,
) -> CallGraph {
    let table = MethodTable::new(instructions, manifest);
    let mut methods: BTreeMap<usize, MethodRef> = table
        .spans()
        .iter()
        .map(|span| (span.method.offset, span.method.clone()))
        .collect();

    // Valid instruction start offsets. A CALL/CALL_L target must land on one of
    // these to resolve to an internal method; a target past the script end (or
    // mid-instruction) is reported as unresolved rather than fabricating a
    // synthetic `sub_0xNNNN`.
    let instruction_offsets: HashSet<usize> = instructions.iter().map(|i| i.offset).collect();

    let mut edges = Vec::new();
    for (index, instr) in instructions.iter().enumerate() {
        match instr.opcode {
            OpCode::Syscall => push_syscall_edge(&mut edges, &table, instr),
            OpCode::Call | OpCode::Call_L => push_direct_call_edge(
                &mut edges,
                &mut methods,
                &table,
                &instruction_offsets,
                instr,
            ),
            OpCode::CallT => push_method_token_edge(&mut edges, nef, &table, instr),
            OpCode::CallA => push_calla_edge(
                &mut edges,
                &mut methods,
                &table,
                &instruction_offsets,
                instructions,
                index,
                instr,
            ),
            _ => {}
        }
    }

    // Second pass: resolve CALLA targets that load function pointers from
    // argument slots (LDARG) by tracing back through callers.
    resolve_ldarg_calla_targets(instructions, &mut edges, &table, &mut methods);

    CallGraph {
        methods: methods.into_values().collect(),
        edges,
    }
}

fn push_syscall_edge(edges: &mut Vec<CallEdge>, table: &MethodTable, instr: &Instruction) {
    let Some(Operand::Syscall(hash)) = instr.operand else {
        return;
    };
    let info = syscalls::lookup(hash);
    edges.push(CallEdge {
        caller: table.method_for_offset(instr.offset),
        call_offset: instr.offset,
        opcode: instr.opcode.to_string(),
        target: CallTarget::Syscall {
            hash,
            name: info.map(|i| i.name.to_string()),
            returns_value: info.map(|i| i.returns_value).unwrap_or(true),
        },
    });
}

fn push_direct_call_edge(
    edges: &mut Vec<CallEdge>,
    methods: &mut BTreeMap<usize, MethodRef>,
    table: &MethodTable,
    instruction_offsets: &HashSet<usize>,
    instr: &Instruction,
) {
    let caller = table.method_for_offset(instr.offset);
    match relative_target_isize(instr) {
        Some(target) if target >= 0 && instruction_offsets.contains(&(target as usize)) => {
            let target = target as usize;
            let callee = table.resolve_internal_target(target);
            methods.insert(callee.offset, callee.clone());
            edges.push(CallEdge {
                caller,
                call_offset: instr.offset,
                opcode: instr.opcode.to_string(),
                target: CallTarget::Internal { method: callee },
            });
        }
        Some(target) => edges.push(CallEdge {
            caller,
            call_offset: instr.offset,
            opcode: instr.opcode.to_string(),
            target: CallTarget::UnresolvedInternal { target },
        }),
        None => edges.push(CallEdge {
            caller,
            call_offset: instr.offset,
            opcode: instr.opcode.to_string(),
            target: CallTarget::UnresolvedInternal { target: -1 },
        }),
    }
}

fn push_method_token_edge(
    edges: &mut Vec<CallEdge>,
    nef: &NefFile,
    table: &MethodTable,
    instr: &Instruction,
) {
    let Some(Operand::U16(index)) = instr.operand else {
        return;
    };
    let token = nef.method_tokens.get(index as usize);
    if let Some(token) = token {
        edges.push(CallEdge {
            caller: table.method_for_offset(instr.offset),
            call_offset: instr.offset,
            opcode: instr.opcode.to_string(),
            target: CallTarget::MethodToken {
                index,
                hash_le: util::format_hash(&token.hash),
                hash_be: util::format_hash_be(&token.hash),
                method: token.method.clone(),
                parameters_count: token.parameters_count,
                has_return_value: token.has_return_value,
                call_flags: token.call_flags,
                call_flags_description: describe_call_flags(token.call_flags),
            },
        });
    } else {
        edges.push(CallEdge {
            caller: table.method_for_offset(instr.offset),
            call_offset: instr.offset,
            opcode: instr.opcode.to_string(),
            target: CallTarget::Indirect {
                opcode: instr.opcode.to_string(),
                operand: Some(index),
            },
        });
    }
}

fn push_calla_edge(
    edges: &mut Vec<CallEdge>,
    methods: &mut BTreeMap<usize, MethodRef>,
    table: &MethodTable,
    instruction_offsets: &HashSet<usize>,
    instructions: &[Instruction],
    index: usize,
    instr: &Instruction,
) {
    // CALLA takes no operand; it pops a Pointer from the stack. Resolve direct
    // PUSHA + CALLA sequences to internal call edges.
    let caller = table.method_for_offset(instr.offset);
    if let Some(target) = calla_target_from_pusha(instructions, index)
        .filter(|target| instruction_offsets.contains(target))
    {
        let callee = table.resolve_internal_target(target);
        methods.insert(callee.offset, callee.clone());
        edges.push(CallEdge {
            caller,
            call_offset: instr.offset,
            opcode: instr.opcode.to_string(),
            target: CallTarget::Internal { method: callee },
        });
    } else {
        edges.push(CallEdge {
            caller,
            call_offset: instr.offset,
            opcode: instr.opcode.to_string(),
            target: CallTarget::Indirect {
                opcode: instr.opcode.to_string(),
                operand: None,
            },
        });
    }
}

fn relative_target_isize(instr: &Instruction) -> Option<isize> {
    let delta = match &instr.operand {
        Some(Operand::Jump(v)) => *v as isize,
        Some(Operand::Jump32(v)) => *v as isize,
        _ => return None,
    };
    Some(instr.offset as isize + delta)
}