Skip to main content

Bytecode Disassembly

After executing bytecode or debugging with source maps, you may want to analyze bytecode structure statically — what opcodes it contains, which function selectors are present, and how the code is organized. The ProgramInstructionsUtils class provides tools for parsing and disassembling EVM bytecode without executing it.

This is useful for security analysis (checking for specific opcodes), contract verification (confirming expected function signatures), or simply understanding what a deployed contract does at the bytecode level.

Prerequisites

dotnet add package Nethereum.EVM

Disassembling Bytecode

The simplest way to understand bytecode is to disassemble it into human-readable opcodes:

using Nethereum.EVM;

var bytecode = "0x6080604052600436106100..."; // Contract bytecode

// Full disassembly with byte offsets
var disassembly = ProgramInstructionsUtils.DisassembleToString(bytecode);
Console.WriteLine(disassembly);

The output shows each instruction with its byte offset, opcode byte, mnemonic, and operand:

0000   60   PUSH1  0x80
0002 60 PUSH1 0x40
0004 52 MSTORE
0005 60 PUSH1 0x04
0007 36 CALLDATASIZE
0008 10 LT

For a more compact view without byte offsets:

var simplified = ProgramInstructionsUtils.DisassembleSimplifiedToString(bytecode);
Console.WriteLine(simplified);
// Output: PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x04 CALLDATASIZE LT ...

Parsing into Instructions

For programmatic analysis, parse bytecode into structured ProgramInstruction objects:

var instructions = ProgramInstructionsUtils.GetProgramInstructions(bytecode);

Console.WriteLine($"Total instructions: {instructions.Count}");

foreach (var instruction in instructions)
{
Console.WriteLine($"Offset {instruction.Step}: {instruction.Instruction}");
}

Each ProgramInstruction contains the opcode, its position in the bytecode, and any operand bytes (for PUSH1 through PUSH32).

Detecting Function Signatures

Solidity compilers typically place function selector checks at the start of the bytecode — comparing msg.sig (the first 4 bytes of calldata) against known function selectors. You can check whether a contract contains a specific function:

var instructions = ProgramInstructionsUtils.GetProgramInstructions(contractBytecode);

// Check for transfer(address,uint256)
bool hasTransfer = ProgramInstructionsUtils.ContainsFunctionSignature(
instructions,
"0xa9059cbb"
);

// Check for multiple signatures at once
var signatures = new[] { "0xa9059cbb", "0x70a08231", "0x095ea7b3" };
// transfer, balanceOf, approve
bool hasAll = ProgramInstructionsUtils.ContainsFunctionSignatures(instructions, signatures);

Console.WriteLine($"Has transfer: {hasTransfer}");
Console.WriteLine($"Has all ERC-20 functions: {hasAll}");

This checks whether the selector bytes appear as PUSH4 operands in the bytecode — the standard pattern for Solidity function dispatch.

Real-World Example: Analyzing a Deployed Contract

Fetch a contract's bytecode from the blockchain and analyze it:

using Nethereum.EVM;
using Nethereum.Web3;

var web3 = new Web3("https://mainnet.infura.io/v3/YOUR_KEY");

// Fetch USDC contract bytecode
var usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
var bytecode = await web3.Eth.GetCode.SendRequestAsync(usdcAddress);

var instructions = ProgramInstructionsUtils.GetProgramInstructions(bytecode);
Console.WriteLine($"Total instructions: {instructions.Count}");

// Check for standard ERC-20 functions
var erc20Signatures = new[]
{
"0x70a08231", // balanceOf(address)
"0xa9059cbb", // transfer(address,uint256)
"0x23b872dd", // transferFrom(address,address,uint256)
"0x095ea7b3", // approve(address,uint256)
"0xdd62ed3e", // allowance(address,address)
"0x18160ddd", // totalSupply()
};

foreach (var sig in erc20Signatures)
{
bool found = ProgramInstructionsUtils.ContainsFunctionSignature(instructions, sig);
Console.WriteLine($" {sig}: {(found ? "found" : "missing")}");
}

// Full disassembly
var disassembly = ProgramInstructionsUtils.DisassembleToString(bytecode);
Console.WriteLine(disassembly);

This pattern is useful for verifying that a contract implements expected interfaces before interacting with it.

Supported Opcodes

The disassembler supports all EVM opcodes through the Prague hard fork:

CategoryOpcodes
ArithmeticADD, MUL, SUB, DIV, SDIV, MOD, SMOD, ADDMOD, MULMOD, EXP, SIGNEXTEND
ComparisonLT, GT, SLT, SGT, EQ, ISZERO
BitwiseAND, OR, XOR, NOT, BYTE, SHL, SHR, SAR
CryptoKECCAK256
EnvironmentADDRESS, BALANCE, ORIGIN, CALLER, CALLVALUE, CALLDATALOAD, CALLDATASIZE, GASPRICE, CHAINID, SELFBALANCE, BASEFEE
BlockBLOCKHASH, COINBASE, TIMESTAMP, NUMBER, DIFFICULTY, GASLIMIT, BLOBHASH, BLOBBASEFEE
StackPOP, PUSH0-PUSH32, DUP1-DUP16, SWAP1-SWAP16
MemoryMLOAD, MSTORE, MSTORE8, MSIZE, MCOPY
StorageSLOAD, SSTORE, TLOAD, TSTORE
FlowJUMP, JUMPI, PC, GAS, JUMPDEST
LoggingLOG0-LOG4
SystemCREATE, CALL, CALLCODE, RETURN, DELEGATECALL, CREATE2, STATICCALL, REVERT, INVALID, SELFDESTRUCT

Next Steps