Deep EVM #17: Huff 컨트랙트 테스트 — Foundry 포크 테스트와 가스 어설션
Engineering Team
Huff 컨트랙트 테스트가 다른 이유
Huff는 스택, 메모리, 스토리지를 직접 제어할 수 있는 저수준 EVM 어셈블리 언어입니다. 이 강력함에는 대가가 따릅니다: 타입 에러를 잡아주는 컴파일러가 없고, SafeMath도 없으며, 자동 범위 검사도 없습니다. 작성하는 모든 옵코드가 그대로 배포됩니다. 이것이 테스트를 단순히 중요한 것이 아니라 절대적으로 필수적인 것으로 만듭니다.
Solidity에서는 컴파일러가 함수 디스패칭, 스토리지 레이아웃, ABI 인코딩을 위한 보일러플레이트를 생성하지만, Huff에서는 이 모든 것을 수동으로 구현해야 합니다. 잘못 배치된 하나의 SWAP이나 잘못된 점프 목적지가 자금을 유출하거나 컨트랙트를 영구적으로 벽돌로 만들 수 있습니다.
이 글에서는 Foundry를 사용하여 Huff 컨트랙트에 대한 완전한 테스트 전략을 구축합니다. 유닛 테스트, 메인넷 상태에 대한 포크 테스트, 가스 어설션, Solidity 참조 구현에 대한 차등 테스트를 다룹니다.
프로젝트 설정
먼저 Huff 지원이 포함된 Foundry 프로젝트를 설정합니다. Foundry와 함께 Huff 컴파일러(huffc)가 설치되어 있어야 합니다:
curl -L get.huff.sh | bash
huffup
forge init huff-testing && cd huff-testing
forge install huff-language/foundry-huff
foundry.toml에서 Huff 컴파일러를 사용하도록 구성합니다:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
ffi = true
[profile.default.fuzz]
runs = 10000
max_test_rejects = 100000
[profile.default.invariant]
runs = 256
depth = 128
ffi = true 플래그는 foundry-huff가 테스트 중에 Huff 컴파일러를 호출하기 위해 FFI 호출을 사용하므로 필수적입니다.
테스트 대상 Huff 컨트랙트 작성
테스트할 간단한 ERC20 유사 토큰을 Huff로 작성합니다:
// src/SimpleToken.huff
#define function balanceOf(address) view returns (uint256)
#define function transfer(address, uint256) nonpayable returns (bool)
#define constant BALANCES_SLOT = FREE_STORAGE_POINTER()
#define macro BALANCE_OF() = takes (0) returns (0) {
0x04 calldataload // [account]
BALANCES_SLOT // [slot, account]
STORE_ELEMENT_FROM_KEYS(0x00) // [balance_slot]
sload // [balance]
0x00 mstore // []
0x20 0x00 return
}
#define macro TRANSFER() = takes (0) returns (0) {
0x24 calldataload // [amount]
0x04 calldataload // [to, amount]
caller // [from, to, amount]
dup1 // [from, from, to, amount]
BALANCES_SLOT
STORE_ELEMENT_FROM_KEYS(0x00)
sload // [from_bal, from, to, amount]
dup1 // [from_bal, from_bal, from, to, amount]
dup5 // [amount, from_bal, from_bal, from, to, amount]
gt // [amount>from_bal, from_bal, from, to, amount]
fail jumpi
dup4 // [amount, from_bal, from, to, amount]
swap1 sub // [new_from_bal, from, to, amount]
dup2 BALANCES_SLOT
STORE_ELEMENT_FROM_KEYS(0x00)
sstore // [from, to, amount]
swap1 // [to, from, amount]
dup1 BALANCES_SLOT
STORE_ELEMENT_FROM_KEYS(0x00)
dup1 sload // [to_bal, to_slot, from, amount]
dup4 add // [new_to_bal, to_slot, from, amount]
swap1 sstore // [from, amount]
pop pop
0x01 0x00 mstore
0x20 0x00 return
fail:
0x00 0x00 revert
}
#define macro MAIN() = takes (0) returns (0) {
0x00 calldataload 0xE0 shr
dup1 0x70a08231 eq balanceOf jumpi
dup1 0xa9059cbb eq transfer jumpi
0x00 0x00 revert
balanceOf:
BALANCE_OF()
transfer:
TRANSFER()
}
Foundry에서 Huff 테스트 설정
Foundry에서 Huff를 테스트하는 핵심은 foundry-huff의 HuffDeployer 라이브러리를 사용하는 것입니다:
// test/SimpleToken.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "foundry-huff/HuffDeployer.sol";
interface ISimpleToken {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
contract SimpleTokenTest is Test {
ISimpleToken token;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
function setUp() public {
address deployed = HuffDeployer.deploy("SimpleToken");
token = ISimpleToken(deployed);
bytes32 slot = keccak256(abi.encode(alice, uint256(0)));
vm.store(address(token), slot, bytes32(uint256(1000e18)));
}
function test_balanceOf() public view {
assertEq(token.balanceOf(alice), 1000e18);
assertEq(token.balanceOf(bob), 0);
}
function test_transfer() public {
vm.prank(alice);
token.transfer(bob, 100e18);
assertEq(token.balanceOf(alice), 900e18);
assertEq(token.balanceOf(bob), 100e18);
}
function test_transferInsufficientBalance() public {
vm.prank(bob);
vm.expectRevert();
token.transfer(alice, 1);
}
}
타겟 매칭으로 실행합니다:
forge test --match-contract SimpleTokenTest -vvv
-vvv 플래그는 실패 시 전체 스택 트레이스를 보여주며, Huff에서는 에러 메시지가 바이트코드에 내장되지 않으므로 디버깅에 필수적입니다.
가스 스냅샷과 회귀 테스트
가스 효율성은 Huff를 작성하는 주된 이유입니다. Foundry의 가스 스냅샷 기능은 테스트 실행 간 가스 사용량을 추적하고 회귀를 감지할 수 있게 합니다:
forge snapshot --match-contract SimpleTokenTest
이것은 .gas-snapshot 파일을 생성합니다:
SimpleTokenTest:test_balanceOf() (gas: 5421)
SimpleTokenTest:test_transfer() (gas: 28934)
SimpleTokenTest:test_transferInsufficientBalance() (gas: 5102)
기준선과 비교하여 회귀를 감지합니다:
forge snapshot --diff .gas-snapshot
CI에서는 허용 임계값을 설정할 수 있습니다:
forge snapshot --check .gas-snapshot --tolerance 1
이것은 어떤 테스트의 가스 사용량이 1% 이상 증가하면 빌드를 실패시킵니다. .gas-snapshot 파일을 리포지토리에 커밋하여 모든 PR이 현재 기준선에 대해 검사되도록 합니다.
차등 테스트: Huff vs Solidity 참조 구현
Huff에 대한 가장 강력한 테스트 기법은 차등 테스트입니다. 정확성이 보장된 Solidity 구현을 작성한 다음, Huff 컨트랙트가 모든 입력에 대해 동일한 결과를 생성하는지 검증합니다:
contract DifferentialTest is Test {
ISimpleToken huffToken;
SolidityToken solToken;
function setUp() public {
huffToken = ISimpleToken(HuffDeployer.deploy("SimpleToken"));
solToken = new SolidityToken();
}
function testFuzz_balanceOf_differential(
address account,
uint256 balance
) public {
bytes32 huffSlot = keccak256(abi.encode(account, uint256(0)));
vm.store(address(huffToken), huffSlot, bytes32(balance));
deal(address(solToken), account, balance);
assertEq(
huffToken.balanceOf(account),
solToken.balanceOf(account),
"balanceOf mismatch"
);
}
}
foundry.toml에서 fuzz.runs = 10000으로 설정하면, Foundry는 10,000개의 랜덤 입력을 생성하여 두 구현이 일치하는지 검증합니다. 이는 스택 조작의 오프바이원 에러나 잘못된 스토리지 슬롯 계산과 같은 미묘한 버그를 잡아냅니다.
메인넷 상태에 대한 포크 테스트
포크 테스트는 실제 메인넷 상태에 대해 Huff 컨트랙트를 테스트할 수 있게 합니다. 기존 프로토콜과 상호작용하는 컨트랙트에 매우 유용합니다:
forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY \
--match-test testFork -vvv
function testFork_interactWithUniswap() public {
address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
uint256 amountOut = IHuffRouter(address(huffRouter)).getAmountOut(
1 ether, WETH, DAI
);
assertTrue(amountOut > 0, "Should return non-zero amount");
}
포크 테스트는 느리지만(네트워크 호출) 유닛 테스트가 놓치는 통합 문제를 잡아냅니다. 잘못된 인터페이스 인코딩이나 실제 컨트랙트의 예상치 못한 상태 등을 발견할 수 있습니다.
고급: 점프 테이블과 디스패처 테스트
Huff 컨트랙트는 일반적으로 자체 함수 디스패처를 구현합니다. 알 수 없는 셀렉터가 되돌려지는지, 모든 셀렉터가 올바르게 라우팅되는지 테스트합니다:
function test_unknownSelectorReverts() public {
(bool success,) = address(token).call(
abi.encodeWithSelector(bytes4(0xdeadbeef))
);
assertFalse(success, "Unknown selector should revert");
}
function test_allSelectorsRoute() public view {
token.balanceOf(address(0));
}
가스 비교: Huff vs Solidity vs Yul
구현 간 가스 차이를 추적하여 Huff의 복잡성을 정당화합니다:
| 연산 | Solidity | Yul | Huff |
|---|---|---|---|
balanceOf | 2,604 | 2,341 | 2,187 |
transfer | 29,412 | 27,891 | 26,534 |
| 배포 | 198,234 | 143,892 | 98,421 |
Huff는 일반적으로 핫 경로에서 Yul보다 10-15%, Solidity보다 20-30% 가스를 절약합니다. MEV 봇이나 고빈도 연산의 경우, 이러한 절약은 상당한 경쟁 우위로 누적됩니다.
결론
Huff 컨트랙트 테스트에는 체계적이고 다층적인 접근이 필요합니다: 기본 정확성을 위한 유닛 테스트, 엣지 케이스를 위한 퍼즈 테스트, 동등성을 위한 Solidity 참조에 대한 차등 테스트, 실제 통합을 위한 포크 테스트, 성능 회귀를 위한 가스 스냅샷. Foundry는 단일 툴킷으로 이 모든 기능을 제공합니다.
핵심 통찰은 저수준 코드일수록 고수준의 테스트 엄격성이 요구된다는 것입니다. 수동으로 작성하는 모든 옵코드는 미묘한 버그의 기회입니다. 포괄적인 테스트 스위트에 투자하면, Huff의 가스 절약은 잘 테스트된 Solidity와 동일한 신뢰도로 뒷받침될 것입니다.