[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-19-seumateeu-keonteulaekteu-sogseong-giban-teseuteu-peoging":3},{"article":4,"author":60},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":16,"meta_description":17,"focus_keyword":18,"og_image":19,"canonical_url":19,"robots_meta":20,"created_at":15,"updated_at":15,"tags":21,"category_name":39,"related_articles":40},"d5000000-0000-0000-0000-000000000119","a0000000-0000-0000-0000-000000000052","Deep EVM #19: 스마트 컨트랙트를 위한 속성 기반 테스트 — Foundry 퍼징","deep-evm-19-seumateeu-keonteulaekteu-sogseong-giban-teseuteu-peoging","Foundry를 사용한 스마트 컨트랙트의 속성 기반 테스트와 퍼징 탐구. 퍼즈 입력, 불변성 테스트, Huff\u002FYul\u002FSolidity 차등 테스트를 다룹니다.","## 퍼징이 유닛 테스트가 놓치는 버그를 잡는 이유\n\n유닛 테스트는 특정 시나리오를 검증합니다: Alice에서 Bob에게 100 토큰을 전송하고 잔액이 업데이트되었는지 확인합니다. 하지만 스마트 컨트랙트는 모든 주소에서, 모든 값으로, 모든 순서의 적대적 입력에 직면합니다. 10가지 시나리오를 확인하는 유닛 테스트는 엣지 케이스를 찾기 위해 100,000개의 랜덤 입력을 생성하는 퍼저와 경쟁할 수 없습니다.\n\n속성 기반 테스트는 패러다임을 뒤집습니다. 특정 입력에 대한 예상 출력을 지정하는 대신, 모든 입력에 대해 유지되어야 하는 속성을 정의합니다. 퍼저는 반례를 찾으려고 시도합니다:\n\n```solidity\n\u002F\u002F 유닛 테스트: 특정 입력\nfunction test_transfer() public {\n    token.transfer(bob, 100);\n    assertEq(token.balanceOf(bob), 100);\n}\n\n\u002F\u002F 속성 기반 테스트: 모든 입력에 대해\nfunction testFuzz_transfer(address to, uint256 amount) public {\n    uint256 totalBefore = token.balanceOf(alice) + token.balanceOf(to);\n    vm.prank(alice);\n    token.transfer(to, amount);\n    uint256 totalAfter = token.balanceOf(alice) + token.balanceOf(to);\n    assertEq(totalBefore, totalAfter, \"Conservation of tokens\");\n}\n```\n\nFoundry의 퍼저는 지능적 전략을 사용합니다: 경계값(0, 1, `type(uint256).max`)으로 시작한 다음, 랜덤 값을 혼합하고, 커버리지 기반 변이를 사용하여 새로운 코드 경로를 탐색합니다.\n\n## 퍼저 구성\n\nFoundry의 퍼즈 구성은 탐색의 깊이와 폭을 제어합니다:\n\n```toml\n# foundry.toml\n[profile.default.fuzz]\nruns = 10000            # 테스트당 랜덤 입력 수\nmax_test_rejects = 100000  # 실패 전 최대 거부 입력 수\nseed = 0x42             # 재현성을 위한 고정 시드\ndictionary_weight = 40  # 사전 기반 변이 가중치\n\n[profile.ci.fuzz]\nruns = 100000           # CI에서 더 많은 실행\n```\n\n`dictionary_weight` 매개변수는 퍼저가 실행 중 발견한 흥미로운 값(스토리지에서 발견된 주소 등)을 재사용하는 빈도와 완전히 랜덤 입력을 생성하는 빈도를 제어합니다.\n\n## bound() vs assume(): 입력 제약\n\n퍼징은 임의의 입력을 생성하지만, 컨트랙트에는 유효한 입력 범위가 있을 수 있습니다. 이를 처리하는 두 가지 방법이 있습니다:\n\n### assume(): 유효하지 않은 입력 건너뛰기\n\n```solidity\nfunction testFuzz_withdraw(uint256 amount) public {\n    vm.assume(amount > 0);\n    vm.assume(amount \u003C= token.balanceOf(alice));\n    \u002F\u002F 유효한 금액으로만 테스트\n}\n```\n\n`assume()`은 조건을 충족하지 않는 입력을 폐기합니다. 문제: 대부분의 입력이 유효하지 않으면 퍼저가 대부분의 실행을 낭비합니다.\n\n### bound(): 유효하지 않은 입력 변환\n\n```solidity\nfunction testFuzz_withdraw(uint256 amount) public {\n    amount = bound(amount, 1, token.balanceOf(alice));\n    \u002F\u002F 모든 입력이 유효 — 낭비되는 실행 없음\n}\n```\n\n`bound()`는 모듈러 산술을 사용하여 모든 uint256을 유효 범위로 매핑합니다. 실행이 낭비되지 않으므로 거의 항상 선호됩니다. 범위로 표현할 수 없는 복잡한 조건에만 `assume()`을 사용합니다.\n\n## 불변성 테스트: 상태 기반 퍼징\n\n위의 퍼즈 테스트는 상태 비저장입니다 — 각 실행이 독립적입니다. 불변성 테스트는 상태를 유지합니다: 퍼저가 랜덤 순서로 함수 시퀀스를 호출한 다음, 각 호출 후 불변성이 유지되는지 확인합니다:\n\n```solidity\n\u002F\u002F test\u002Finvariant\u002FTokenInvariant.t.sol\ncontract TokenInvariantTest is Test {\n    SimpleToken token;\n    TokenHandler handler;\n\n    function setUp() public {\n        token = new SimpleToken();\n        handler = new TokenHandler(token);\n        targetContract(address(handler));\n    }\n\n    \u002F\u002F 모든 호출 시퀀스 후에 반드시 유지되어야 함\n    function invariant_totalSupplyConstant() public view {\n        assertEq(\n            token.totalSupply(),\n            1_000_000e18,\n            \"Total supply must never change\"\n        );\n    }\n\n    function invariant_sumOfBalancesEqTotalSupply() public view {\n        uint256 sum = token.balanceOf(address(handler.alice()))\n            + token.balanceOf(address(handler.bob()))\n            + token.balanceOf(address(handler.charlie()));\n        assertEq(sum, token.totalSupply(), \"Balances must sum to total\");\n    }\n}\n\n\u002F\u002F 핸들러가 퍼저를 제한하고 안내\ncontract TokenHandler is Test {\n    SimpleToken token;\n    address public alice = makeAddr(\"alice\");\n    address public bob = makeAddr(\"bob\");\n    address public charlie = makeAddr(\"charlie\");\n\n    constructor(SimpleToken _token) {\n        token = _token;\n    }\n\n    function transfer(\n        uint256 fromSeed,\n        uint256 toSeed,\n        uint256 amount\n    ) external {\n        address from = _selectUser(fromSeed);\n        address to = _selectUser(toSeed);\n        amount = bound(amount, 0, token.balanceOf(from));\n        vm.prank(from);\n        token.transfer(to, amount);\n    }\n\n    function _selectUser(uint256 seed) internal view returns (address) {\n        uint256 index = seed % 3;\n        if (index == 0) return alice;\n        if (index == 1) return bob;\n        return charlie;\n    }\n}\n```\n\n불변성 테스트를 구성합니다:\n\n```toml\n[profile.default.invariant]\nruns = 256          # 호출 시퀀스 수\ndepth = 128         # 시퀀스당 호출 수\nfail_on_revert = false  # 핸들러 되돌림 시 실패하지 않음\n```\n\n퍼저는 128개의 랜덤 함수 호출로 구성된 256개의 시퀀스를 생성한 다음, 각 호출 후 모든 불변성을 확인합니다. 이는 상태 의존적 버그를 찾는 데 매우 강력합니다.\n\n## 차등 테스트: Huff vs Yul vs Solidity\n\n차등 테스트는 저수준 EVM 구현을 검증하는 최고의 기준입니다. 동일한 명세의 세 가지 구현을 배포하고, 모든 입력에 대해 동일한 결과를 생성하는지 검증합니다:\n\n```solidity\ncontract DifferentialFuzzTest is Test {\n    IToken huffToken;\n    IToken yulToken;\n    IToken solToken;\n\n    function setUp() public {\n        huffToken = IToken(HuffDeployer.deploy(\"HuffToken\"));\n        yulToken = IToken(deployYulContract(\"YulToken\"));\n        solToken = IToken(address(new SolidityToken()));\n\n        address[3] memory users = [alice, bob, charlie];\n        for (uint i = 0; i \u003C users.length; i++) {\n            _setBalance(address(huffToken), users[i], 1000e18);\n            _setBalance(address(yulToken), users[i], 1000e18);\n            _setBalance(address(solToken), users[i], 1000e18);\n        }\n    }\n\n    function testFuzz_transfer_differential(\n        uint8 fromIdx,\n        uint8 toIdx,\n        uint256 amount\n    ) public {\n        address from = _user(fromIdx);\n        address to = _user(toIdx);\n        amount = bound(amount, 0, 1000e18);\n\n        vm.prank(from);\n        (bool s1,) = address(huffToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n        vm.prank(from);\n        (bool s2,) = address(yulToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n        vm.prank(from);\n        (bool s3,) = address(solToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n\n        assertEq(s1, s2, \"Huff vs Yul success mismatch\");\n        assertEq(s2, s3, \"Yul vs Solidity success mismatch\");\n\n        if (s1) {\n            assertEq(\n                huffToken.balanceOf(from),\n                solToken.balanceOf(from),\n                \"Sender balance mismatch\"\n            );\n        }\n    }\n}\n```\n\n## 퍼징으로 발견된 실제 버그\n\n퍼징은 치명적 버그를 잡는 입증된 실적을 가지고 있습니다:\n\n### 버그 1: Huff ERC20의 팬텀 오버플로\n\nHuff ERC20 구현이 오버플로 검사 없이 잔액을 더했습니다. 퍼저는 `type(uint256).max - balance + 1` 토큰을 전송하면 수신자의 잔액이 거의 0으로 래핑된다는 것을 발견했습니다.\n\n### 버그 2: 자기 전송 이중 지출\n\n`from == to`일 때, 순진한 Huff 구현은 잔액 슬롯에서 빼고 같은 슬롯에 더하여 실질적으로 토큰을 두 배로 만들었습니다.\n\n```solidity\n\u002F\u002F 퍼저가 from == to 엣지 케이스 발견\nfunction testFuzz_selfTransfer(uint256 amount) public {\n    amount = bound(amount, 1, token.balanceOf(alice));\n    uint256 before = token.balanceOf(alice);\n    vm.prank(alice);\n    token.transfer(alice, amount);\n    assertEq(token.balanceOf(alice), before, \"Self-transfer must not change balance\");\n}\n```\n\n### 버그 3: 제로 주소 발행\n\n퍼저는 Huff 토큰에서 `address(0)`으로 전송해도 되돌려지지 않아 총 공급량 업데이트 없이 토큰이 소각되는 것을 발견했습니다. 이는 `sum(balances) == totalSupply` 불변성을 깨뜨렸습니다.\n\n## CI에 퍼징 통합\n\nCI에서 더 높은 반복 횟수로 퍼즈 테스트를 실행합니다:\n\n```yaml\n# .github\u002Fworkflows\u002Ftest.yml\njobs:\n  fuzz:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: foundry-rs\u002Ffoundry-toolchain@v1\n      - name: Run fuzz tests\n        run: |\n          forge test --match-path \"test\u002Ffuzz\u002F*\" \\\n            --fuzz-runs 100000 \\\n            --fuzz-seed ${{ github.run_id }}\n      - name: Run invariant tests\n        run: |\n          forge test --match-path \"test\u002Finvariant\u002F*\" \\\n            --invariant-runs 512 \\\n            --invariant-depth 256\n```\n\n`github.run_id`를 시드로 사용하면 실행 간에 입력을 변화시키면서도 재현 가능성을 보장합니다.\n\n## 결론\n\n퍼징은 스마트 컨트랙트 테스트를 \"이 10가지 케이스에서 작동하는가?\"에서 \"100,000개의 랜덤 입력 중 하나로 깨뜨릴 수 있는가?\"로 변환합니다. `bound()`를 사용한 속성 기반 테스트, 상태 기반 시퀀스를 사용한 불변성 테스트, 구현 간 차등 테스트가 포괄적인 안전망을 형성합니다. 컴파일러가 안전 보장을 제공하지 않는 Huff와 Yul 컨트랙트에서 퍼징은 선택이 아닙니다 — 프로덕션 버그에 대한 주요 방어 수단입니다.","\u003Ch2 id=\"\">퍼징이 유닛 테스트가 놓치는 버그를 잡는 이유\u003C\u002Fh2>\n\u003Cp>유닛 테스트는 특정 시나리오를 검증합니다: Alice에서 Bob에게 100 토큰을 전송하고 잔액이 업데이트되었는지 확인합니다. 하지만 스마트 컨트랙트는 모든 주소에서, 모든 값으로, 모든 순서의 적대적 입력에 직면합니다. 10가지 시나리오를 확인하는 유닛 테스트는 엣지 케이스를 찾기 위해 100,000개의 랜덤 입력을 생성하는 퍼저와 경쟁할 수 없습니다.\u003C\u002Fp>\n\u003Cp>속성 기반 테스트는 패러다임을 뒤집습니다. 특정 입력에 대한 예상 출력을 지정하는 대신, 모든 입력에 대해 유지되어야 하는 속성을 정의합니다. 퍼저는 반례를 찾으려고 시도합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F 유닛 테스트: 특정 입력\nfunction test_transfer() public {\n    token.transfer(bob, 100);\n    assertEq(token.balanceOf(bob), 100);\n}\n\n\u002F\u002F 속성 기반 테스트: 모든 입력에 대해\nfunction testFuzz_transfer(address to, uint256 amount) public {\n    uint256 totalBefore = token.balanceOf(alice) + token.balanceOf(to);\n    vm.prank(alice);\n    token.transfer(to, amount);\n    uint256 totalAfter = token.balanceOf(alice) + token.balanceOf(to);\n    assertEq(totalBefore, totalAfter, \"Conservation of tokens\");\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Foundry의 퍼저는 지능적 전략을 사용합니다: 경계값(0, 1, \u003Ccode>type(uint256).max\u003C\u002Fcode>)으로 시작한 다음, 랜덤 값을 혼합하고, 커버리지 기반 변이를 사용하여 새로운 코드 경로를 탐색합니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">퍼저 구성\u003C\u002Fh2>\n\u003Cp>Foundry의 퍼즈 구성은 탐색의 깊이와 폭을 제어합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-toml\"># foundry.toml\n[profile.default.fuzz]\nruns = 10000            # 테스트당 랜덤 입력 수\nmax_test_rejects = 100000  # 실패 전 최대 거부 입력 수\nseed = 0x42             # 재현성을 위한 고정 시드\ndictionary_weight = 40  # 사전 기반 변이 가중치\n\n[profile.ci.fuzz]\nruns = 100000           # CI에서 더 많은 실행\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>dictionary_weight\u003C\u002Fcode> 매개변수는 퍼저가 실행 중 발견한 흥미로운 값(스토리지에서 발견된 주소 등)을 재사용하는 빈도와 완전히 랜덤 입력을 생성하는 빈도를 제어합니다.\u003C\u002Fp>\n\u003Ch2 id=\"bound-vs-assume\">bound() vs assume(): 입력 제약\u003C\u002Fh2>\n\u003Cp>퍼징은 임의의 입력을 생성하지만, 컨트랙트에는 유효한 입력 범위가 있을 수 있습니다. 이를 처리하는 두 가지 방법이 있습니다:\u003C\u002Fp>\n\u003Ch3>assume(): 유효하지 않은 입력 건너뛰기\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function testFuzz_withdraw(uint256 amount) public {\n    vm.assume(amount &gt; 0);\n    vm.assume(amount &lt;= token.balanceOf(alice));\n    \u002F\u002F 유효한 금액으로만 테스트\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>assume()\u003C\u002Fcode>은 조건을 충족하지 않는 입력을 폐기합니다. 문제: 대부분의 입력이 유효하지 않으면 퍼저가 대부분의 실행을 낭비합니다.\u003C\u002Fp>\n\u003Ch3>bound(): 유효하지 않은 입력 변환\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function testFuzz_withdraw(uint256 amount) public {\n    amount = bound(amount, 1, token.balanceOf(alice));\n    \u002F\u002F 모든 입력이 유효 — 낭비되는 실행 없음\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>bound()\u003C\u002Fcode>는 모듈러 산술을 사용하여 모든 uint256을 유효 범위로 매핑합니다. 실행이 낭비되지 않으므로 거의 항상 선호됩니다. 범위로 표현할 수 없는 복잡한 조건에만 \u003Ccode>assume()\u003C\u002Fcode>을 사용합니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">불변성 테스트: 상태 기반 퍼징\u003C\u002Fh2>\n\u003Cp>위의 퍼즈 테스트는 상태 비저장입니다 — 각 실행이 독립적입니다. 불변성 테스트는 상태를 유지합니다: 퍼저가 랜덤 순서로 함수 시퀀스를 호출한 다음, 각 호출 후 불변성이 유지되는지 확인합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F test\u002Finvariant\u002FTokenInvariant.t.sol\ncontract TokenInvariantTest is Test {\n    SimpleToken token;\n    TokenHandler handler;\n\n    function setUp() public {\n        token = new SimpleToken();\n        handler = new TokenHandler(token);\n        targetContract(address(handler));\n    }\n\n    \u002F\u002F 모든 호출 시퀀스 후에 반드시 유지되어야 함\n    function invariant_totalSupplyConstant() public view {\n        assertEq(\n            token.totalSupply(),\n            1_000_000e18,\n            \"Total supply must never change\"\n        );\n    }\n\n    function invariant_sumOfBalancesEqTotalSupply() public view {\n        uint256 sum = token.balanceOf(address(handler.alice()))\n            + token.balanceOf(address(handler.bob()))\n            + token.balanceOf(address(handler.charlie()));\n        assertEq(sum, token.totalSupply(), \"Balances must sum to total\");\n    }\n}\n\n\u002F\u002F 핸들러가 퍼저를 제한하고 안내\ncontract TokenHandler is Test {\n    SimpleToken token;\n    address public alice = makeAddr(\"alice\");\n    address public bob = makeAddr(\"bob\");\n    address public charlie = makeAddr(\"charlie\");\n\n    constructor(SimpleToken _token) {\n        token = _token;\n    }\n\n    function transfer(\n        uint256 fromSeed,\n        uint256 toSeed,\n        uint256 amount\n    ) external {\n        address from = _selectUser(fromSeed);\n        address to = _selectUser(toSeed);\n        amount = bound(amount, 0, token.balanceOf(from));\n        vm.prank(from);\n        token.transfer(to, amount);\n    }\n\n    function _selectUser(uint256 seed) internal view returns (address) {\n        uint256 index = seed % 3;\n        if (index == 0) return alice;\n        if (index == 1) return bob;\n        return charlie;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>불변성 테스트를 구성합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-toml\">[profile.default.invariant]\nruns = 256          # 호출 시퀀스 수\ndepth = 128         # 시퀀스당 호출 수\nfail_on_revert = false  # 핸들러 되돌림 시 실패하지 않음\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>퍼저는 128개의 랜덤 함수 호출로 구성된 256개의 시퀀스를 생성한 다음, 각 호출 후 모든 불변성을 확인합니다. 이는 상태 의존적 버그를 찾는 데 매우 강력합니다.\u003C\u002Fp>\n\u003Ch2 id=\"huff-vs-yul-vs-solidity\">차등 테스트: Huff vs Yul vs Solidity\u003C\u002Fh2>\n\u003Cp>차등 테스트는 저수준 EVM 구현을 검증하는 최고의 기준입니다. 동일한 명세의 세 가지 구현을 배포하고, 모든 입력에 대해 동일한 결과를 생성하는지 검증합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract DifferentialFuzzTest is Test {\n    IToken huffToken;\n    IToken yulToken;\n    IToken solToken;\n\n    function setUp() public {\n        huffToken = IToken(HuffDeployer.deploy(\"HuffToken\"));\n        yulToken = IToken(deployYulContract(\"YulToken\"));\n        solToken = IToken(address(new SolidityToken()));\n\n        address[3] memory users = [alice, bob, charlie];\n        for (uint i = 0; i &lt; users.length; i++) {\n            _setBalance(address(huffToken), users[i], 1000e18);\n            _setBalance(address(yulToken), users[i], 1000e18);\n            _setBalance(address(solToken), users[i], 1000e18);\n        }\n    }\n\n    function testFuzz_transfer_differential(\n        uint8 fromIdx,\n        uint8 toIdx,\n        uint256 amount\n    ) public {\n        address from = _user(fromIdx);\n        address to = _user(toIdx);\n        amount = bound(amount, 0, 1000e18);\n\n        vm.prank(from);\n        (bool s1,) = address(huffToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n        vm.prank(from);\n        (bool s2,) = address(yulToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n        vm.prank(from);\n        (bool s3,) = address(solToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n\n        assertEq(s1, s2, \"Huff vs Yul success mismatch\");\n        assertEq(s2, s3, \"Yul vs Solidity success mismatch\");\n\n        if (s1) {\n            assertEq(\n                huffToken.balanceOf(from),\n                solToken.balanceOf(from),\n                \"Sender balance mismatch\"\n            );\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">퍼징으로 발견된 실제 버그\u003C\u002Fh2>\n\u003Cp>퍼징은 치명적 버그를 잡는 입증된 실적을 가지고 있습니다:\u003C\u002Fp>\n\u003Ch3>버그 1: Huff ERC20의 팬텀 오버플로\u003C\u002Fh3>\n\u003Cp>Huff ERC20 구현이 오버플로 검사 없이 잔액을 더했습니다. 퍼저는 \u003Ccode>type(uint256).max - balance + 1\u003C\u002Fcode> 토큰을 전송하면 수신자의 잔액이 거의 0으로 래핑된다는 것을 발견했습니다.\u003C\u002Fp>\n\u003Ch3>버그 2: 자기 전송 이중 지출\u003C\u002Fh3>\n\u003Cp>\u003Ccode>from == to\u003C\u002Fcode>일 때, 순진한 Huff 구현은 잔액 슬롯에서 빼고 같은 슬롯에 더하여 실질적으로 토큰을 두 배로 만들었습니다.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F 퍼저가 from == to 엣지 케이스 발견\nfunction testFuzz_selfTransfer(uint256 amount) public {\n    amount = bound(amount, 1, token.balanceOf(alice));\n    uint256 before = token.balanceOf(alice);\n    vm.prank(alice);\n    token.transfer(alice, amount);\n    assertEq(token.balanceOf(alice), before, \"Self-transfer must not change balance\");\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>버그 3: 제로 주소 발행\u003C\u002Fh3>\n\u003Cp>퍼저는 Huff 토큰에서 \u003Ccode>address(0)\u003C\u002Fcode>으로 전송해도 되돌려지지 않아 총 공급량 업데이트 없이 토큰이 소각되는 것을 발견했습니다. 이는 \u003Ccode>sum(balances) == totalSupply\u003C\u002Fcode> 불변성을 깨뜨렸습니다.\u003C\u002Fp>\n\u003Ch2 id=\"ci\">CI에 퍼징 통합\u003C\u002Fh2>\n\u003Cp>CI에서 더 높은 반복 횟수로 퍼즈 테스트를 실행합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># .github\u002Fworkflows\u002Ftest.yml\njobs:\n  fuzz:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: foundry-rs\u002Ffoundry-toolchain@v1\n      - name: Run fuzz tests\n        run: |\n          forge test --match-path \"test\u002Ffuzz\u002F*\" \\\n            --fuzz-runs 100000 \\\n            --fuzz-seed ${{ github.run_id }}\n      - name: Run invariant tests\n        run: |\n          forge test --match-path \"test\u002Finvariant\u002F*\" \\\n            --invariant-runs 512 \\\n            --invariant-depth 256\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>github.run_id\u003C\u002Fcode>를 시드로 사용하면 실행 간에 입력을 변화시키면서도 재현 가능성을 보장합니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">결론\u003C\u002Fh2>\n\u003Cp>퍼징은 스마트 컨트랙트 테스트를 “이 10가지 케이스에서 작동하는가?“에서 “100,000개의 랜덤 입력 중 하나로 깨뜨릴 수 있는가?“로 변환합니다. \u003Ccode>bound()\u003C\u002Fcode>를 사용한 속성 기반 테스트, 상태 기반 시퀀스를 사용한 불변성 테스트, 구현 간 차등 테스트가 포괄적인 안전망을 형성합니다. 컴파일러가 안전 보장을 제공하지 않는 Huff와 Yul 컨트랙트에서 퍼징은 선택이 아닙니다 — 프로덕션 버그에 대한 주요 방어 수단입니다.\u003C\u002Fp>\n","ko","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:28.261177Z","스마트 컨트랙트를 위한 속성 기반 테스트 — Foundry 퍼징","스마트 컨트랙트를 위한 속성 기반 테스트와 퍼징 심층 분석. 퍼즈 입력, 불변성 테스트, Huff vs Yul vs Solidity 차등 테스트 다룸.","스마트 컨트랙트 퍼징 Foundry",null,"index, follow",[22,27,31,35],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000016","EVM","evm","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000021","Foundry","foundry",{"id":32,"name":33,"slug":34,"created_at":26},"c0000000-0000-0000-0000-000000000013","Security","security",{"id":36,"name":37,"slug":38,"created_at":26},"c0000000-0000-0000-0000-000000000018","Yul","yul","블록체인",[41,48,54],{"id":42,"title":43,"slug":44,"excerpt":45,"locale":12,"category_name":46,"published_at":47},"d0000000-0000-0000-0000-000000000674","2026년, Bali가 동남아시아의 임팩트 테크 허브가 되고 있는 이유","bali-2026-dongnamasia-impaekteu-tekeu-heobeu-iyu","Bali는 동남아시아 스타트업 생태계에서 16위를 차지하고 있습니다. Web3 빌더, AI 지속가능성 스타트업, 에코 여행 테크 기업이 집중되면서, 이 섬은 지역 임팩트 테크의 수도로 자리매김하고 있습니다.","엔지니어링","2026-03-28T10:44:49.294484Z",{"id":49,"title":50,"slug":51,"excerpt":52,"locale":12,"category_name":46,"published_at":53},"d0000000-0000-0000-0000-000000000673","ASEAN 데이터 보호 패치워크: 개발자를 위한 컴플라이언스 체크리스트","asean-deiteo-boho-paechiwokeu-gaebaljaleul-wihan-keompeullaieonseuchekeuriseuteu","7개 ASEAN 국가가 포괄적인 데이터 보호법을 시행하고 있으며, 각각 다른 동의 모델, 현지화 요건, 벌칙 구조를 가지고 있습니다. 다중 국가 애플리케이션을 구축하는 개발자를 위한 실용적인 컴플라이언스 체크리스트입니다.","2026-03-28T10:44:49.286400Z",{"id":55,"title":56,"slug":57,"excerpt":58,"locale":12,"category_name":46,"published_at":59},"d0000000-0000-0000-0000-000000000672","Indonesia 290억 달러 디지털 전환: 소프트웨어 기업을 위한 기회","indonesia-290eok-dallleo-dijiteol-jeonhwan-sopeuteuweo-gieopui-gihoe","Indonesia IT 서비스 시장은 2026년 290.3억 달러에 달할 것으로 예상되며, 이는 2025년 243.7억 달러에서 증가한 수치입니다. 클라우드 인프라, AI, 전자상거래, 데이터센터가 동남아시아에서 가장 빠른 성장을 주도하고 있습니다.","2026-03-28T10:44:49.265609Z",{"id":13,"name":61,"slug":62,"bio":63,"photo_url":19,"linkedin":19,"role":64,"created_at":65,"updated_at":65},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]