Deep EVM #12: 고급 Huff — 적응형 실행과 온체인 계산
Engineering Team
Hello World를 넘어서
이전 기사들은 Huff 기초 — 매크로, 스택 관리, 점프 테이블 — 를 다루었습니다. 이제 실제 MEV 봇 컨트랙트에서 추출한 프로덕션 패턴으로 이동합니다. 이 패턴들은 Solidity가 효율적으로 표현할 수 없는 문제를 해결합니다: 콜데이터 기반 적응형 실행, 스토리지 읽기 없는 다중 운영자 인증, 바이트코드 크기를 최소화하는 메모리 레이아웃.
패턴 1: 적응형 실행 — amount_in == 0 폴백
MEV 차익거래 컨트랙트에서 봇은 최적의 입력 금액을 오프체인에서 사전 계산하여 콜데이터를 통해 전달합니다. 그러나 때때로 봇은 사용 가능한 정확한 잔액을 알지 못합니다 — 예를 들어, 같은 번들의 이전 스왑이 컨트랙트에 토큰을 입금하고 정확한 출력 금액이 실행 시점의 풀 상태에 따라 달라지는 경우.
해결책: 콜데이터에서 amount_in == 0이면, 컨트랙트가 자체 온체인 잔액을 읽어 대신 사용합니다.
#define macro GET_AMOUNT_IN() = takes(0) returns(1) {
// 콜데이터[1..33]에서 금액 읽기
0x01 calldataload // [amount_in]
dup1 // [amount_in, amount_in]
use_calldata_amount jumpi // [amount_in]
// amount_in == 0 → 온체인 잔액 읽기
pop // []
// balanceOf(address(this)) 호출 구성
// selector: 0x70a08231
0x70a08231 // [selector]
0xe0 shl // [selector << 224]
0x00 mstore // [] — memory[0x00] = selector
address // [this]
0x04 mstore // [] — memory[0x04] = address(this)
// 토큰에 staticcall
0x20 // [retSize]
0x00 // [retOffset]
0x24 // [argSize]
0x00 // [argOffset]
[TOKEN] // [token, argOffset, argSize, retOffset, retSize]
gas // [gas, token, ...]
staticcall // [success]
pop // []
0x00 mload // [balance] — 실제 토큰 잔액
// amount_in으로 사용하기 위해 이어짐
use_calldata_amount:
// 스택: [amount_in] (콜데이터 또는 balanceOf에서)
}
이 패턴은 폴백이 트리거될 때 약 200 가스를 추가하지만(staticcall), amount_in이 콜데이터에 제공될 때는 제로 가스입니다(DUP + JUMPI만). 봇은 90%의 경우 콜데이터 경로를 사용하고 중간 금액이 예측 불가능한 다단계 번들을 실행할 때만 온체인 읽기로 폴백합니다.
패턴 2: 우선 수수료 엔트로피를 통한 다중 운영자 인증
MEV 봇은 종종 컨트랙트를 호출할 수 있는 여러 운영자(핫 월렛)가 필요합니다. 매핑에 승인된 주소를 저장하면 SLOAD당 2,100 가스(cold) 또는 100 가스(warm)가 듭니다. 블록당 한 번 호출되는 컨트랙트의 경우 모든 호출이 cold입니다.
대안: 트랜잭션의 tx.gasprice(더 정확히는 우선 수수료)에 운영자의 인증을 인코딩합니다. 봇이 비밀 논스를 포함하는 값으로 maxPriorityFeePerGas를 설정합니다:
#define constant AUTH_MASK = 0xFFFF // 우선 수수료의 하위 16비트
#define constant AUTH_SECRET = 0xBEEF
#define macro CHECK_AUTH() = takes(0) returns(0) {
// 우선 수수료 추출: gasprice - basefee
gasprice // [gasprice]
basefee // [basefee, gasprice]
swap1 sub // [priority_fee]
// 하위 16비트가 비밀과 일치하는지 확인
[AUTH_MASK] // [mask, priority_fee]
and // [fee & mask]
[AUTH_SECRET] // [secret, fee & mask]
eq // [authorized?]
authorized jumpi
0x00 0x00 revert
authorized:
}
이것은 스토리지 기반 접근법의 2,100+ 대비 14 가스만 듭니다(GASPRICE + BASEFEE + SUB + AND + EQ + JUMPI). 트레이드오프: 비밀이 멤풀에서 보입니다. 그러나 MEV 봇은 Flashbots나 프라이빗 멤풀을 사용하므로 트랜잭션은 포함 전에 공개적으로 보이지 않습니다.
우선 수수료 비트를 분할하여 다른 운영자에 대해 다른 비밀을 인코딩할 수 있습니다 — 예: 비트 0-15는 인증 토큰, 비트 16-31은 운영자 ID.
패턴 3: USDT 안전 승인 (제로 리셋)
USDT의 approve 함수는 현재 허용량이 0이 아니고 새로운 0이 아닌 값을 설정하려 하면 되돌립니다. 이것은 수많은 DeFi 통합을 깨뜨린 악명 높은 quirk입니다. Huff에서는 항상 먼저 0으로 리셋하여 처리합니다:
#define macro SAFE_APPROVE() = takes(2) returns(0) {
// takes: [spender, amount]
// 먼저: approve(spender, 0)
0x095ea7b3 0xe0 shl // [approve_selector]
0x00 mstore // memory[0x00] = selector
dup2 // [spender, spender, amount]
0x04 mstore // memory[0x04] = spender [spender, amount]
0x00 0x24 mstore // memory[0x24] = 0 [spender, amount]
0x00 // [retSize]
0x00 // [retOffset]
0x44 // [argSize]
0x00 // [argOffset]
0x00 // [value (ETH 없음)]
[USDT] // [token]
gas call // [success]
pop // [] — 결과 무시, 일부 토큰은 아무것도 반환하지 않음
// 두 번째: approve(spender, amount)
// selector는 여전히 memory[0x00]에
// spender는 여전히 memory[0x04]에
swap1 // [amount, spender]
0x24 mstore // memory[0x24] = amount [spender]
pop // []
0x00 0x00 0x44 0x00 0x00
[USDT] gas call
pop
}
이 패턴은 USDT뿐만 아니라 모든 ERC20 토큰에 대해 작동합니다 — 안전한 범용 승인입니다. 추가 호출은 약 2,600 가스(warm CALL)이지만 조용한 실패를 방지합니다.
패턴 4: WETH 입금 및 출금
WETH (Wrapped Ether) 변환은 MEV 봇에서 빈번한 작업입니다. ABI는 간단합니다:
#define constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
#define macro WETH_DEPOSIT() = takes(1) returns(0) {
// takes: [amount]
// WETH.deposit{value: amount}()
0x00 0x00 0x00 0x00 // [0, 0, 0, 0, amount] — retSize, retOff, argSize, argOff
swap4 // [amount, 0, 0, 0, 0]
[WETH] // [weth, amount, 0, 0, 0, 0]
gas call // [success]
pop
}
#define macro WETH_WITHDRAW() = takes(1) returns(0) {
// takes: [amount]
// WETH.withdraw(amount)
0x2e1a7d4d 0xe0 shl // [selector, amount]
0x00 mstore // memory[0] = selector [amount]
0x04 mstore // memory[4] = amount []
0x00 0x00 0x24 0x00 0x00
[WETH] gas call
pop
}
입금 함수는 특히 우아합니다 — WETH의 deposit은 인수가 없으므로 ETH 값과 함께 제로 argSize를 보냅니다. 입금을 위한 총 바이트코드: 약 20바이트.
메모리 레이아웃 트릭
EVM 메모리는 바이트 주소 지정이 가능하고 확장이 자유롭습니다(확장에 이차적으로 가스 소비). MEV 컨트랙트는 중복 MSTORE 작업을 피하기 위해 고정 메모리 레이아웃을 사용합니다:
// 고정 메모리 레이아웃 — 호출 간에 변경되지 않음
// 0x00 - 0x03: 현재 함수 셀렉터 (4바이트)
// 0x04 - 0x23: 인수 1 (32바이트)
// 0x24 - 0x43: 인수 2 (32바이트)
// 0x44 - 0x63: 인수 3 (32바이트)
// 0x64 - 0x83: 반환 데이터 스크래치 (32바이트)
// 0x80 - 0x9f: 스택 스필 영역 (32바이트)
// 0xa0 - 0xbf: 두 번째 스필 슬롯 (32바이트)
핵심 통찰: 같은 셀렉터로 여러 외부 호출을 하는 경우(예: 여러 transfer 호출), 셀렉터를 한 번만 씁니다. 후속 호출에서는 변경된 인수만 업데이트합니다.
#define macro MULTI_TRANSFER() = takes(0) returns(0) {
// 설정: transfer 셀렉터를 한 번 작성
0xa9059cbb 0xe0 shl
0x00 mstore // memory[0] = transfer(address,uint256)
// 전송 1: to=Alice, amount=100
[ALICE] 0x04 mstore
0x64 0x24 mstore // 100
0x00 0x00 0x44 0x00 0x00 [TOKEN] gas call pop
// 전송 2: to=Bob, amount=200
// 셀렉터는 여전히 memory[0]에 — 다시 쓸 필요 없음!
[BOB] 0x04 mstore
0xc8 0x24 mstore // 200
0x00 0x00 0x44 0x00 0x00 [TOKEN] gas call pop
// 전송 3: to=Bob, amount=300
// 주소는 여전히 memory[4]에 — 금액만 업데이트!
0x012c 0x24 mstore // 300
0x00 0x00 0x44 0x00 0x00 [TOKEN] gas call pop
}
피한 MSTORE마다 3 가스 + 값에 대한 PUSH를 절약합니다. 5-6번의 스왑 호출이 있는 멀티홉 차익거래에서 50-100 가스를 절약합니다.
프리 메모리 포인터의 신화
Solidity는 다음 사용 가능한 메모리 오프셋을 추적하는 “프리 메모리 포인터“를 0x40에 유지합니다. 이것은 동적 메모리 할당(배열, 문자열, abi.encode)을 위한 추상화입니다. Huff에서는 이것이 필요 없습니다.
MEV 컨트랙트는 외부 호출의 고정된 알려진 집합을 가집니다. 컴파일 시간에 메모리 영역을 정적으로 할당할 수 있습니다. 프리 메모리 포인터가 없다는 것은:
- 모든 메모리 쓰기 전에 포인터를 읽는 3 가스 MLOAD가 없음.
- 모든 할당 후 포인터를 업데이트하는 3 가스 MSTORE가 없음.
- 재진입으로 인한 메모리 충돌 위험이 없음(레이아웃이 결정적).
프리 메모리 포인터를 삭제하세요. 메모리 맵을 직접 소유하세요.
패턴 결합: 프로덕션 스왑 매크로
여러 패턴을 결합한 프로덕션급 Uniswap V2 스왑 매크로입니다:
#define macro SWAP_V2() = takes(3) returns(0) {
// takes: [amountOut, zeroForOne, pair]
// swap(uint256,uint256,address,bytes) 호출 구성
0x022c0d9f 0xe0 shl
0x00 mstore // selector
// amount0Out = zeroForOne ? 0 : amountOut
// amount1Out = zeroForOne ? amountOut : 0
swap1 // [zeroForOne, amountOut, pair]
skip_zero jumpi // [amountOut, pair]
// zeroForOne == 0: token1 → token0, amount0Out = amountOut
dup1 0x04 mstore // amount0Out = amountOut
0x00 0x24 mstore // amount1Out = 0
done_amounts jump
skip_zero: // [amountOut, pair]
0x00 0x04 mstore // amount0Out = 0
dup1 0x24 mstore // amount1Out = amountOut
done_amounts:
pop // [pair]
address 0x44 mstore // to = address(this)
0x80 0x64 mstore // bytes offset
0x00 0x84 mstore // bytes length = 0
// pair.swap 호출
0x00 0x00 0xa4 0x00 0x00
swap5 // [pair, ...]
gas call
// 성공 확인
iszero revert_swap jumpi
stop
revert_swap:
returndatasize 0x00 0x00 returndatacopy
returndatasize 0x00 revert
}
이것은 120바이트의 바이트코드입니다. SafeERC20, 인터페이스 타입, ABI 인코딩이 포함된 동등한 Solidity는 400+ 바이트를 생성합니다.
요약
고급 Huff는 학술적 연습이 아닌 프로덕션 패턴에 관한 것입니다. amount_in == 0 폴백은 봇이 예측 불가능한 중간 상태에 적응할 수 있게 합니다. 우선 수수료 인증은 cold SLOAD 비용을 제거합니다. USDT 안전 승인은 조용한 되돌림을 방지합니다. 고정 메모리 레이아웃은 중복 쓰기를 제거합니다. 각 패턴은 50-200 가스를 절약합니다 — 그리고 MEV에서 이 절약은 수천 번의 일일 실행에 걸쳐 의미 있는 이익으로 복합됩니다. 다음 기사에서는 컨트랙트 작성에서 익스플로잇으로 초점을 전환합니다: MEV에 대한 소개 — 추출 가능한 가치, 서처, 블록 빌더.