본문으로 건너뛰기
블록체인Mar 28, 2026

Deep EVM #9: Huff 언어 입문 — 매크로, 레이블, 원시 옵코드

OS
Open Soft Team

Engineering Team

Huff가 존재하는 이유

Solidity는 훌륭한 추상화입니다 — 그렇지 않을 때까지. 100바이트의 런타임 바이트코드 안에 들어가는 컨트랙트가 필요하거나, 압축된 점프 테이블로 O(1) 함수 디스패치가 필요하거나, 하루에 수백만 번 실행되는 핫 패스에서 200 가스를 절약해야 할 때, 메탈에 더 가까운 무언가가 필요합니다. 그것이 바로 Huff입니다.

Huff는 얇은 매크로 시스템이 덧붙여진 저수준 EVM 어셈블리 언어입니다. 변수도, 타입도, 뒤에서 최적화하는 컴파일러도 없습니다. 당신이 쓰는 것이 그대로 온체인에 올라갑니다 — 옵코드 대 옵코드.

Huff 설치

표준 컴파일러는 Rust로 작성된 huffc입니다:

curl -L get.huff.sh | bash
huffup
huffc --version

이것은 huffc~/.huff/bin에 설치합니다. PATH에 추가하고 확인하세요:

$ huffc --version
huffc 0.3.2

Foundry 프로젝트에서 foundry-huff를 사용하면 .huff 파일을 .sol 파일과 동일한 방식으로 배포할 수 있습니다.

Hello World: 최소 컨트랙트

모든 호출에 32바이트 워드 0x01을 반환하는 컨트랙트를 작성해 봅시다:

#define macro MAIN() = takes(0) returns(0) {
    0x01            // [0x01]
    0x00            // [0x00, 0x01]
    mstore          // []          — memory[0x00..0x20] = 0x01
    0x20            // [0x20]
    0x00            // [0x00, 0x20]
    return          // halt — return memory[0x00..0x20]
}

컴파일:

huffc src/HelloWorld.huff -r

-r 플래그는 런타임 바이트코드를 출력합니다. 600160005260206000f3과 같은 것을 볼 수 있습니다 — 10바이트. 1을 반환하는 Solidity 컨트랙트는 약 200바이트 이상의 런타임 바이트코드로 컴파일됩니다. solc가 전체 함수 디스패처, 메타데이터 해시, 프리 메모리 포인터 설정, ABI 인코더를 생성하기 때문입니다.

매크로 vs 함수

Huff에는 두 가지 코드 재사용 기본 요소가 있습니다: 매크로함수.

매크로 (#define macro)

매크로는 모든 호출 사이트에서 인라인됩니다. JUMP 오버헤드도, 추가 가스도 없습니다 — 컴파일러가 문자 그대로 옵코드를 호출자에 복사-붙여넣기합니다. 가스 크리티컬 코드에서는 기본이자 선호되는 선택입니다.

#define macro REQUIRE_NOT_ZERO() = takes(1) returns(0) {
    // takes: [value]
    continue        // [continue_dest, value]
    jumpi           // []  — value != 0이면 점프
    0x00 0x00 revert
    continue:
}

함수 (#define fn)

함수는 실제 JUMP/JUMPDEST 쌍을 생성합니다. 호출당 약 22 가스의 추가 비용(JUMP 8 + JUMPDEST 1 + 스택 조작)으로 바이트코드 크기를 절약합니다. 바이트코드 크기가 가스보다 중요할 때만 사용하세요.

#define fn safe_add() = takes(2) returns(1) {
    // takes: [a, b]
    dup2 dup2       // [a, b, a, b]
    add             // [sum, a, b]
    dup1            // [sum, sum, a, b]
    swap2           // [a, sum, sum, b]
    gt              // [overflow?, sum, b]
    overflow jumpi
    swap1 pop       // [sum]
    back jump
    overflow:
        0x00 0x00 revert
    back:
}

레이블과 점프 대상

Huff에서 레이블은 명명된 JUMPDEST 위치입니다. 컴파일러는 컴파일 시간에 이를 구체적인 바이트코드 오프셋으로 해결합니다.

#define macro LOOP_EXAMPLE() = takes(1) returns(1) {
    // takes: [n]
    0x00                // [acc, n]
    loop:
        dup2            // [n, acc, n]
        iszero          // [n==0?, acc, n]
        done jumpi      // [acc, n]
        swap1           // [n, acc]
        0x01 swap1 sub  // [n-1, acc]
        swap1           // [acc, n-1]
        0x01 add        // [acc+1, n-1]
        loop jump
    done:
        swap1 pop       // [acc]
}

각 레이블은 단일 JUMPDEST 바이트(0x5b)로 컴파일됩니다. 참조(loop jump, done jumpi)는 PUSH2 <offset> JUMP(또는 JUMPI)로 컴파일됩니다. 이것은 원시 EVM 어셈블리에서 직접 작성하는 것과 정확히 같습니다 — Huff가 오프셋 관리만 처리합니다.

takes()와 returns()

매크로와 함수의 takes(n)returns(m) 어노테이션은 문서화 및 컴파일러 힌트입니다. 독자 — 그리고 Huff 컴파일러의 스택 검사기 — 에게 블록이 소비하고 생성할 스택 항목 수를 알려줍니다.

#define macro ADD_TWO() = takes(2) returns(1) {
    add  // 2개 항목 소비, 1개 생성
}

실제 스택 동작이 어노테이션과 일치하지 않으면 huffc가 경고를 발생시킵니다. 이 어노테이션을 간이 타입 시스템으로 취급하세요 — 실수로 스택에 쓰레기를 남기거나 언더플로가 발생하는 것을 방지합니다.

비교: Huff vs Solidity 바이트코드

스토리지 슬롯을 반환하는 간단한 getValue() view 함수를 생각해 봅시다:

Solidity:

function getValue() external view returns (uint256) {
    return value;
}

Solc는 디스패처 + ABI 인코딩에 약 40바이트를 생성합니다:

CALLDATASIZE → CALLDATALOAD → SHR 224 → DUP1 → PUSH4 selector
→ EQ → PUSH2 dest → JUMPI → ... → SLOAD → PUSH1 0x20
→ MSTORE → PUSH1 0x20 → PUSH1 0x00 → RETURN

Huff 등가물:

#define function getValue() view returns (uint256)

#define macro GET_VALUE() = takes(0) returns(0) {
    [VALUE_SLOT]    // [slot]
    sload           // [value]
    0x00 mstore     // []  — 메모리에 저장
    0x20 0x00 return
}

Huff 버전은 본문에 12바이트의 바이트코드입니다. ABI 인코딩 오버헤드도, 프리 메모리 포인터도, 메타데이터 해시도 없습니다. 호출자를 제어할 때(예: 자체 컨트랙트를 호출하는 MEV 봇), Solidity 컴파일러가 필요하다고 가정하는 모든 것을 제거할 수 있습니다.

상수와 스토리지 슬롯

Huff 상수는 PUSH 명령으로 인라인되는 컴파일 타임 값입니다:

#define constant VALUE_SLOT = 0x00
#define constant OWNER_SLOT = 0x01
#define constant MAX_UINT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

사용법: [VALUE_SLOT]0x00을 푸시하고, [MAX_UINT]는 전체 32바이트 값을 푸시합니다. 상수는 가스 비용 없이 가독성을 높여줍니다 — 순전히 구문적입니다.

인클루드와 프로젝트 구조

실제 Huff 프로젝트는 여러 파일에 코드를 분산합니다:

// src/Main.huff
#include "./utils/SafeMath.huff"
#include "./interfaces/IERC20.huff"
#include "./Dispatcher.huff"

#define macro MAIN() = takes(0) returns(0) {
    DISPATCHER()
}

인클루드 시스템은 단순한 텍스트 포함입니다 — 모듈 범위나 네임스페이스는 없습니다. 충돌을 피하기 위해 매크로 이름을 신중하게 지정하세요.

Huff를 언제 사용해야 하는가

Huff는 범용 언어가 아닙니다. 다음 경우에 사용하세요:

  1. 가스가 주요 제약 조건일 때 — 100 가스가 수익성을 결정하는 MEV 컨트랙트.
  2. 바이트코드 크기가 중요할 때 — 다른 컨트랙트에 의해 배포되는 컨트랙트(CREATE2 팩토리)에서 더 작은 initcode = 더 적은 배포 가스.
  3. 커스텀 디스패치가 필요할 때 — 점프 테이블, 비트 패킹된 셀렉터, 또는 비표준 ABI 인코딩.
  4. EVM을 배우고 있을 때 — 원시 옵코드를 작성하는 것보다 EVM을 더 잘 가르치는 것은 없습니다.

그 외의 모든 것에 대해서는 Solidity를 작성하고 solc --asm으로 컴파일러 출력을 읽으세요. 더 생산적이고 오류가 적을 것입니다.

요약

Huff는 정신을 유지하기 위한 최소한의 추상화와 함께 EVM 바이트코드에 직접 연결을 제공합니다. 매크로는 제로 오버헤드 재사용을 위해 코드를 인라인합니다. 레이블은 점프 오프셋 관리를 처리합니다. takes/returns 어노테이션은 스택 오류를 조기에 포착합니다. 다음 기사에서는 스택 관리 — dup, swap의 기술과 스택의 멘탈 모델을 현실과 동기화하는 방법 — 를 심층적으로 다룹니다.