EVM TAC β†’ Huff + Solidity Decompiler β€” Training Scripts

Fine-tuning AEON-7/Qwen3.6-27B-AEON-Ultimate-Uncensored to translate EVM Three-Address Code (TAC) into Huff assembly and Solidity source code.

Trained model β†’ demeleww/evm-tac-decompiler (private)
Pre-built dataset β†’ demeleww/evm-decompiler-dataset (public)

v2: Selector-Aligned β€” No Guessing

The model is trained to never guess code. Every training example is verified:

  1. Selector alignment: Each Solidity function is paired with TAC only when its 4-byte function selector (keccak256(canonical_sig)[:4]) appears in the bytecode dispatch table (PUSH4 <sel> EQ PUSH2 <dest> JUMPI)
  2. Refusal training: ~10% of examples teach the model to say "I cannot decompile this" when a function's selector is NOT in the TAC
  3. Grounded system prompt: Instructs the model to only output code directly supported by TAC input

Architecture

Detail Value
Base model AEON-7/Qwen3.6-27B-AEON-Ultimate-Uncensored
Architecture qwen3_5 β€” Hybrid: 48 GatedDeltaNet + 16 full attention layers (64 total)
Method QLoRA (8-bit LLM.int8 or 4-bit NF4)
LoRA rank r=32, Ξ±=64
LoRA targets q_proj, k_proj, v_proj, o_proj + in_proj_qkv, in_proj_z, out_proj + gate_proj, up_proj, down_proj
Dataset demeleww/evm-decompiler-dataset β€” selector-aligned, 4 task types
Sequence length 4096 tokens

Files

File Description
train.py Core training script β€” model loading, dataset handling, SFT config
run_full.py Colab wrapper β€” BestLossUploader callback, orchestrates training
build_dataset.py v2 dataset builder β€” selector-aligned, refusal training, grounded prompt
requirements.txt Python dependencies

πŸš€ Google Colab Training β€” Full Notebook

Requirements: Google Colab with β‰₯95GB GPU (A100 95GB). Runtime β†’ Change runtime type β†’ A100.


Cell 1 β€” Check GPU

import torch
if torch.cuda.is_available():
    gpu = torch.cuda.get_device_properties(0)
    print(f"βœ“ GPU: {gpu.name}")
    print(f"βœ“ VRAM: {gpu.total_memory / 1e9:.1f} GB")
    assert gpu.total_memory / 1e9 > 90, f"Need β‰₯95GB GPU, got {gpu.total_memory / 1e9:.1f}GB"
    print("βœ“ Ready for training!")
else:
    print("βœ— No GPU! Go to Runtime β†’ Change runtime type β†’ A100")

Cell 2 β€” Install dependencies

⚠️ Must install transformers from source β€” PyPI version doesn't support qwen3_5 architecture.

!pip install -q git+https://github.com/huggingface/transformers.git
!pip install -q trl peft datasets accelerate bitsandbytes pyevmasm sentencepiece huggingface_hub liger-kernel pycryptodome

Cell 3 β€” Login to HuggingFace

from huggingface_hub import login
login()  # paste your token when prompted

Cell 4 β€” (ONE TIME) Build and upload selector-aligned dataset

Skip if dataset already exists at demeleww/evm-decompiler-dataset.
Run once β€” processes contracts with selector alignment + refusal examples (~15-30 min).

from huggingface_hub import hf_hub_download
hf_hub_download("demeleww/evm-bytecode-to-solidity-qwen3.6-27b", "build_dataset.py", local_dir=".", force_download=True)
%run build_dataset.py

What it does:

  • Scans bytecode dispatch tables for PUSH4 <selector> EQ PUSH2 <dest> JUMPI patterns
  • Computes keccak256 selectors for each Solidity function in the source code
  • Only creates training pairs where the function selector matches a dispatch entry
  • Generates refusal examples (~10%) for unmatched functions
  • Uploads to demeleww/evm-decompiler-dataset

Cell 5 β€” Download training scripts & start training

from huggingface_hub import hf_hub_download

hf_hub_download("demeleww/evm-bytecode-to-solidity-qwen3.6-27b", "train.py", local_dir=".", force_download=True)
hf_hub_download("demeleww/evm-bytecode-to-solidity-qwen3.6-27b", "run_full.py", local_dir=".", force_download=True)
print("βœ“ Scripts downloaded")
%run run_full.py

Cell 6 β€” Test inference (after training)

import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(load_in_8bit=True)

tokenizer = AutoTokenizer.from_pretrained("demeleww/evm-tac-decompiler")
base = AutoModelForCausalLM.from_pretrained(
    "AEON-7/Qwen3.6-27B-AEON-Ultimate-Uncensored",
    quantization_config=bnb_config,
    device_map="auto",
    attn_implementation="eager",
)
model = PeftModel.from_pretrained(base, "demeleww/evm-tac-decompiler")
model.eval()
print("βœ“ Model loaded")

# Sample TAC with visible selectors
tac = """Block_0:
  v1 = 0x80
  v2 = 0x40
  MSTORE(stack[-2], stack[-1])
  v3 = CALLVALUE()
  DUP1
  v4 = ISZERO(stack[-1])
  v5 = 0x10
  IF(stack[-1]) GOTO(stack[-2])

Block_1:  // @pc=16
  v6 = 0x0
  DUP1
  REVERT

Block_2:  // @pc=20
  POP
  v7 = 0x4
  v8 = CALLDATASIZE()
  v9 = LT(stack[-2], stack[-1])
  v10 = 0x4c
  IF(stack[-1]) GOTO(stack[-2])"""

prompt = f"### Task: Convert the following EVM Three-Address Code (TAC) to Solidity source code.\n\n### TAC:\n{tac}\n\n### Solidity:\n"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
    output = model.generate(**inputs, max_new_tokens=512, do_sample=False)
print("=== TAC β†’ Solidity ===")
print(tokenizer.decode(output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))

Cell 7 β€” Test with real bytecode (end-to-end)

from pyevmasm import disassemble_all

def bytecode_to_tac(bytecode_hex, max_chars=5000):
    clean = bytecode_hex.strip()
    if clean.startswith("0x"): clean = clean[2:]
    if len(clean) % 2 != 0: clean = clean[:-1]
    instructions = list(disassemble_all(bytes.fromhex(clean)))
    binary_ops = {"ADD","SUB","MUL","DIV","SDIV","MOD","SMOD","EXP","AND","OR","XOR","SHL","SHR","SAR","LT","GT","SLT","SGT","EQ","BYTE","SIGNEXTEND"}
    unary_ops = {"NOT","ISZERO"}
    nullary_ops = {"CALLER","CALLVALUE","CALLDATASIZE","ADDRESS","ORIGIN","GASPRICE","TIMESTAMP","NUMBER","CHAINID","SELFBALANCE","CODESIZE","RETURNDATASIZE","GAS","COINBASE"}
    lines, bid, vc = ["Block_0:"], 0, 0
    for inst in instructions:
        n = inst.name
        if n == "JUMPDEST": bid += 1; lines.append(f"\nBlock_{bid}:  // @pc={inst.pc}")
        elif n.startswith("PUSH"): vc += 1; lines.append(f"  v{vc} = {inst.operand or 0:#x}")
        elif n in binary_ops: vc += 1; lines.append(f"  v{vc} = {n}(stack[-2], stack[-1])")
        elif n in unary_ops: vc += 1; lines.append(f"  v{vc} = {n}(stack[-1])")
        elif n in nullary_ops: vc += 1; lines.append(f"  v{vc} = {n}()")
        elif n == "CALLDATALOAD": vc += 1; lines.append(f"  v{vc} = CALLDATALOAD(stack[-1])")
        elif n in ("SLOAD","MLOAD"): vc += 1; lines.append(f"  v{vc} = {n}(stack[-1])")
        elif n in ("SSTORE","MSTORE"): lines.append(f"  {n}(stack[-2], stack[-1])")
        elif n == "JUMP": lines.append(f"  GOTO(stack[-1])")
        elif n == "JUMPI": lines.append(f"  IF(stack[-1]) GOTO(stack[-2])")
        elif n in ("RETURN","REVERT","STOP","SELFDESTRUCT","INVALID"): lines.append(f"  {n}")
        elif n in ("CALL","STATICCALL","DELEGATECALL"): vc += 1; lines.append(f"  v{vc} = {n}(...)")
        elif n == "KECCAK256": vc += 1; lines.append(f"  v{vc} = KECCAK256(stack[-2], stack[-1])")
        else: lines.append(f"  {n}")
        if len("\n".join(lines)) > max_chars: break
    return "\n".join(lines)

bytecode = "0x608060405234801561001057600080fd5b506004361061004c5760003560e01c806306fdde0314610051578063095ea7b31461006f57806318160ddd1461009f57806323b872dd146100bd575b600080fd5b"

tac = bytecode_to_tac(bytecode)
print("=== Generated TAC ===")
print(tac[:2000])

print("\n=== Decompiled Solidity ===")
prompt = f"### Task: Convert the following EVM Three-Address Code (TAC) to Solidity source code.\n\n### TAC:\n{tac}\n\n### Solidity:\n"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
    output = model.generate(**inputs, max_new_tokens=512, do_sample=False)
print(tokenizer.decode(output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))

How selector alignment prevents guessing

OLD (v1) β€” ungrounded:
  Contract has 20 functions, TAC covers first 200 opcodes (dispatcher only)
  β†’ All 20 functions paired with same TAC
  β†’ Model learns: "when I see ANY TAC, generate transfer(), approve(), etc."
  β†’ Result: HALLUCINATED code

NEW (v2) β€” grounded:
  Contract has 20 functions, bytecode dispatch table has selectors for all 20
  β†’ Only functions whose selector appears in PUSH4...EQ...JUMPI get paired
  β†’ Functions NOT in dispatch β†’ become refusal training examples
  β†’ Model learns: "only output code for selectors I can SEE in the TAC"
  β†’ Result: GROUNDED code or honest refusal

Training config

Parameter Value Why
batch_size 2 Conservative for 95GB with 8-bit weights (~30GB)
grad_accum 4 Effective batch = 8
lr 2e-4 Standard for QLoRA
scheduler cosine Smooth decay
warmup 100 steps
lora_r 32 Higher rank = more expressive
lora_alpha 64 2Γ— rank
max_length 4096 Fits most contract TAC
optimizer paged_adamw_8bit Spills to CPU if GPU full
gradient_checkpointing True Required for 27B model
liger_kernel True Up to 60% less activation memory
packing False Preserves prompt/completion boundaries

Quantization options

Mode VRAM for weights Quality Flag
8-bit (LLM.int8) ~30 GB Higher USE_8BIT = True (default)
4-bit (NF4) ~14 GB Good USE_8BIT = False

If you get OOM

  1. Switch to 4-bit: set USE_8BIT = False in train.py
  2. If still OOM with 4-bit: reduce BATCH_SIZE = 1, GRAD_ACCUM = 8
  3. Keep use_liger_kernel=True and optim="paged_adamw_8bit" always

Important notes

  • transformers must be from source: pip install git+https://github.com/huggingface/transformers.git
  • pycryptodome required: for proper Keccak-256 hashing (NOT NIST SHA3)
  • Install deps in separate cell from training: avoids stale .pyc import errors
  • force_download=True: always get latest script versions
  • BestLossUploader: auto-uploads every 50 steps when loss improves
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Model tree for demeleww/evm-bytecode-to-solidity-qwen3.6-27b

Base model

Qwen/Qwen3.6-27B
Adapter
(2)
this model

Datasets used to train demeleww/evm-bytecode-to-solidity-qwen3.6-27b