[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-19-property-based-testirovanie-fazzing-foundry":3},{"article":4,"author":56},{"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":35,"related_articles":36},"d0000000-0000-0000-0000-000000000219","a0000000-0000-0000-0000-000000000012","Deep EVM #19: Property-based тестирование смарт-контрактов — фаззинг в Foundry","deep-evm-19-property-based-testirovanie-fazzing-foundry","Как использовать фаззинг и инвариантное тестирование в Foundry для обнаружения уязвимостей, которые пропускают юнит-тесты.","## Почему юнит-тестов недостаточно\n\nЮнит-тесты проверяют конкретные сценарии, которые вы предусмотрели. Но уязвимости смарт-контрактов чаще всего скрываются в сценариях, которые никто не предвидел. Property-based тестирование (PBT) переворачивает подход: вместо проверки конкретных входных данных вы формулируете свойства, которые должны выполняться для любых входных данных, и позволяете фаззеру искать контрпримеры.\n\nВ экосистеме Ethereum PBT уже предотвратил множество взломов. Фаззинг обнаружил уязвимости в Uniswap v3, Compound и десятках других протоколов на стадии аудита.\n\n## Фаззинг в Foundry: основы\n\nFoundry поддерживает фаззинг «из коробки». Любой тест с параметрами автоматически становится фазз-тестом:\n\n```solidity\nfunction testFuzz_transfer(address to, uint256 amount) public {\n    vm.assume(to != address(0));\n    vm.assume(amount \u003C= token.balanceOf(alice));\n\n    uint256 totalBefore = token.balanceOf(alice) + token.balanceOf(to);\n\n    vm.prank(alice);\n    token.transfer(to, amount);\n\n    uint256 totalAfter = token.balanceOf(alice) + token.balanceOf(to);\n    assertEq(totalBefore, totalAfter, \"Total supply changed!\");\n}\n```\n\nFoundry генерирует случайные значения `to` и `amount`, прогоняя тест тысячи раз. `vm.assume` фильтрует невалидные комбинации.\n\n## Настройка фаззера\n\nВ `foundry.toml`:\n\n```toml\n[profile.default.fuzz]\nruns = 10000          # Количество итераций\nmax_test_rejects = 100000  # Макс. отклонённых vm.assume\nseed = 0x42           # Фиксированный seed для воспроизводимости\n\n[profile.ci.fuzz]\nruns = 50000          # Больше итераций в CI\n```\n\nУвеличение `runs` линейно повышает вероятность обнаружения бага. Для критичных контрактов рекомендуется 50 000+ итераций.\n\n## Инвариантное тестирование\n\nИнвариантные тесты — это более мощная форма фаззинга. Вместо одного вызова функции фаззер выполняет последовательность случайных вызовов и после каждого проверяет инварианты:\n\n```solidity\ncontract InvariantTest is Test {\n    ERC20Handler handler;\n\n    function setUp() public {\n        handler = new ERC20Handler(token);\n        targetContract(address(handler));\n    }\n\n    function invariant_totalSupplyConstant() public view {\n        assertEq(\n            token.totalSupply(),\n            INITIAL_SUPPLY,\n            \"Total supply must never change\"\n        );\n    }\n\n    function invariant_balanceSumEqualsTotalSupply() public view {\n        uint256 sum = 0;\n        for (uint i = 0; i \u003C handler.actorsCount(); i++) {\n            sum += token.balanceOf(handler.actors(i));\n        }\n        assertEq(sum, token.totalSupply());\n    }\n}\n```\n\n## Handler-паттерн\n\nHandler — это контракт-обёртка, направляющий фаззер на осмысленные действия:\n\n```solidity\ncontract ERC20Handler is Test {\n    ERC20 token;\n    address[] public actors;\n\n    constructor(ERC20 _token) {\n        token = _token;\n        actors.push(makeAddr(\"actor1\"));\n        actors.push(makeAddr(\"actor2\"));\n        actors.push(makeAddr(\"actor3\"));\n    }\n\n    function transfer(\n        uint256 actorSeed, uint256 toSeed, uint256 amount\n    ) external {\n        address from = actors[actorSeed % actors.length];\n        address to = actors[toSeed % actors.length];\n        amount = bound(amount, 0, token.balanceOf(from));\n\n        vm.prank(from);\n        token.transfer(to, amount);\n    }\n\n    function actorsCount() external view returns (uint256) {\n        return actors.length;\n    }\n}\n```\n\nФункция `bound` из forge-std ограничивает значение диапазоном, избегая `vm.assume` и связанного с ним отклонения тестов.\n\n## Стратегии формулировки свойств\n\nКлючевые категории свойств для DeFi-контрактов:\n\n1. **Сохранение инвариантов** — totalSupply не меняется при transfer\n2. **Монотонность** — баланс стейкера растёт со временем\n3. **Коммутативность** — порядок операций не влияет на результат\n4. **Roundtrip** — deposit + withdraw возвращает исходное состояние\n5. **Невозможность** — нельзя вывести больше, чем внесли\n\n## Реальный пример: фаззинг AMM\n\nРассмотрим инвариантный тест для AMM (Automated Market Maker):\n\n```solidity\nfunction invariant_constantProduct() public view {\n    uint256 reserve0 = pool.reserve0();\n    uint256 reserve1 = pool.reserve1();\n    uint256 k = reserve0 * reserve1;\n    assertGe(k, initialK, \"Product must never decrease\");\n}\n\nfunction invariant_noFreeTokens() public view {\n    uint256 poolBalance0 = token0.balanceOf(address(pool));\n    assertGe(poolBalance0, pool.reserve0());\n}\n```\n\n## Медиан-фаззинг и guided fuzzing\n\nFoundry's фаззер использует корпус-ориентированный подход. Он сохраняет входные данные, которые увеличивают покрытие кода, и мутирует их. Для улучшения эффективности:\n\n- Используйте словари (`fuzz.dictionary_weight`) для подстановки значимых констант\n- Настройте `fuzz.include_storage` для использования текущего состояния хранилища\n- Включите `fuzz.include_push_bytes` для извлечения констант из байткода\n\n## Интеграция с CI\u002FCD\n\n```yaml\ntest-fuzz:\n  script:\n    - forge test --match-test \"testFuzz\" --fuzz-runs 50000\n    - forge test --match-test \"invariant\" --fuzz-runs 1000\n  timeout: 30m\n```\n\nВажно: инвариантные тесты с высокой глубиной могут занимать минуты. Установите разумные таймауты и не блокируйте быстрый фидбек-цикл.\n\n## Заключение\n\nProperty-based тестирование — это не замена юнит-тестам, а дополнение. Юнит-тесты подтверждают ожидаемое поведение, PBT ищет неожиданное. Для смарт-контрактов, управляющих реальными деньгами, это различие может стоить миллионы долларов.","\u003Ch2 id=\"\">Почему юнит-тестов недостаточно\u003C\u002Fh2>\n\u003Cp>Юнит-тесты проверяют конкретные сценарии, которые вы предусмотрели. Но уязвимости смарт-контрактов чаще всего скрываются в сценариях, которые никто не предвидел. Property-based тестирование (PBT) переворачивает подход: вместо проверки конкретных входных данных вы формулируете свойства, которые должны выполняться для любых входных данных, и позволяете фаззеру искать контрпримеры.\u003C\u002Fp>\n\u003Cp>В экосистеме Ethereum PBT уже предотвратил множество взломов. Фаззинг обнаружил уязвимости в Uniswap v3, Compound и десятках других протоколов на стадии аудита.\u003C\u002Fp>\n\u003Ch2 id=\"foundry\">Фаззинг в Foundry: основы\u003C\u002Fh2>\n\u003Cp>Foundry поддерживает фаззинг «из коробки». Любой тест с параметрами автоматически становится фазз-тестом:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function testFuzz_transfer(address to, uint256 amount) public {\n    vm.assume(to != address(0));\n    vm.assume(amount &lt;= token.balanceOf(alice));\n\n    uint256 totalBefore = token.balanceOf(alice) + token.balanceOf(to);\n\n    vm.prank(alice);\n    token.transfer(to, amount);\n\n    uint256 totalAfter = token.balanceOf(alice) + token.balanceOf(to);\n    assertEq(totalBefore, totalAfter, \"Total supply changed!\");\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Foundry генерирует случайные значения \u003Ccode>to\u003C\u002Fcode> и \u003Ccode>amount\u003C\u002Fcode>, прогоняя тест тысячи раз. \u003Ccode>vm.assume\u003C\u002Fcode> фильтрует невалидные комбинации.\u003C\u002Fp>\n\u003Ch2 id=\"\">Настройка фаззера\u003C\u002Fh2>\n\u003Cp>В \u003Ccode>foundry.toml\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-toml\">[profile.default.fuzz]\nruns = 10000          # Количество итераций\nmax_test_rejects = 100000  # Макс. отклонённых vm.assume\nseed = 0x42           # Фиксированный seed для воспроизводимости\n\n[profile.ci.fuzz]\nruns = 50000          # Больше итераций в CI\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Увеличение \u003Ccode>runs\u003C\u002Fcode> линейно повышает вероятность обнаружения бага. Для критичных контрактов рекомендуется 50 000+ итераций.\u003C\u002Fp>\n\u003Ch2 id=\"\">Инвариантное тестирование\u003C\u002Fh2>\n\u003Cp>Инвариантные тесты — это более мощная форма фаззинга. Вместо одного вызова функции фаззер выполняет последовательность случайных вызовов и после каждого проверяет инварианты:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract InvariantTest is Test {\n    ERC20Handler handler;\n\n    function setUp() public {\n        handler = new ERC20Handler(token);\n        targetContract(address(handler));\n    }\n\n    function invariant_totalSupplyConstant() public view {\n        assertEq(\n            token.totalSupply(),\n            INITIAL_SUPPLY,\n            \"Total supply must never change\"\n        );\n    }\n\n    function invariant_balanceSumEqualsTotalSupply() public view {\n        uint256 sum = 0;\n        for (uint i = 0; i &lt; handler.actorsCount(); i++) {\n            sum += token.balanceOf(handler.actors(i));\n        }\n        assertEq(sum, token.totalSupply());\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"handler\">Handler-паттерн\u003C\u002Fh2>\n\u003Cp>Handler — это контракт-обёртка, направляющий фаззер на осмысленные действия:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract ERC20Handler is Test {\n    ERC20 token;\n    address[] public actors;\n\n    constructor(ERC20 _token) {\n        token = _token;\n        actors.push(makeAddr(\"actor1\"));\n        actors.push(makeAddr(\"actor2\"));\n        actors.push(makeAddr(\"actor3\"));\n    }\n\n    function transfer(\n        uint256 actorSeed, uint256 toSeed, uint256 amount\n    ) external {\n        address from = actors[actorSeed % actors.length];\n        address to = actors[toSeed % actors.length];\n        amount = bound(amount, 0, token.balanceOf(from));\n\n        vm.prank(from);\n        token.transfer(to, amount);\n    }\n\n    function actorsCount() external view returns (uint256) {\n        return actors.length;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Функция \u003Ccode>bound\u003C\u002Fcode> из forge-std ограничивает значение диапазоном, избегая \u003Ccode>vm.assume\u003C\u002Fcode> и связанного с ним отклонения тестов.\u003C\u002Fp>\n\u003Ch2 id=\"\">Стратегии формулировки свойств\u003C\u002Fh2>\n\u003Cp>Ключевые категории свойств для DeFi-контрактов:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Сохранение инвариантов\u003C\u002Fstrong> — totalSupply не меняется при transfer\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Монотонность\u003C\u002Fstrong> — баланс стейкера растёт со временем\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Коммутативность\u003C\u002Fstrong> — порядок операций не влияет на результат\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Roundtrip\u003C\u002Fstrong> — deposit + withdraw возвращает исходное состояние\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Невозможность\u003C\u002Fstrong> — нельзя вывести больше, чем внесли\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"amm\">Реальный пример: фаззинг AMM\u003C\u002Fh2>\n\u003Cp>Рассмотрим инвариантный тест для AMM (Automated Market Maker):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function invariant_constantProduct() public view {\n    uint256 reserve0 = pool.reserve0();\n    uint256 reserve1 = pool.reserve1();\n    uint256 k = reserve0 * reserve1;\n    assertGe(k, initialK, \"Product must never decrease\");\n}\n\nfunction invariant_noFreeTokens() public view {\n    uint256 poolBalance0 = token0.balanceOf(address(pool));\n    assertGe(poolBalance0, pool.reserve0());\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"guided-fuzzing\">Медиан-фаззинг и guided fuzzing\u003C\u002Fh2>\n\u003Cp>Foundry’s фаззер использует корпус-ориентированный подход. Он сохраняет входные данные, которые увеличивают покрытие кода, и мутирует их. Для улучшения эффективности:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Используйте словари (\u003Ccode>fuzz.dictionary_weight\u003C\u002Fcode>) для подстановки значимых констант\u003C\u002Fli>\n\u003Cli>Настройте \u003Ccode>fuzz.include_storage\u003C\u002Fcode> для использования текущего состояния хранилища\u003C\u002Fli>\n\u003Cli>Включите \u003Ccode>fuzz.include_push_bytes\u003C\u002Fcode> для извлечения констант из байткода\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"ci-cd\">Интеграция с CI\u002FCD\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-yaml\">test-fuzz:\n  script:\n    - forge test --match-test \"testFuzz\" --fuzz-runs 50000\n    - forge test --match-test \"invariant\" --fuzz-runs 1000\n  timeout: 30m\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Важно: инвариантные тесты с высокой глубиной могут занимать минуты. Установите разумные таймауты и не блокируйте быстрый фидбек-цикл.\u003C\u002Fp>\n\u003Ch2 id=\"\">Заключение\u003C\u002Fh2>\n\u003Cp>Property-based тестирование — это не замена юнит-тестам, а дополнение. Юнит-тесты подтверждают ожидаемое поведение, PBT ищет неожиданное. Для смарт-контрактов, управляющих реальными деньгами, это различие может стоить миллионы долларов.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.912127Z","Property-based тестирование смарт-контрактов — фаззинг в Foundry","Фаззинг и инвариантное тестирование смарт-контрактов в Foundry: свойства, handler-паттерн и CI\u002FCD.","фаззинг смарт-контрактов foundry",null,"index, follow",[22,27,31],{"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","Блокчейн",[37,44,50],{"id":38,"title":39,"slug":40,"excerpt":41,"locale":12,"category_name":42,"published_at":43},"d0200000-0000-0000-0000-000000000013","Почему Бали становится хабом импакт-технологий Юго-Восточной Азии в 2026 году","pochemu-bali-stanovitsya-khabom-impakt-tekhnologiy-2026","Бали занимает 16-е место среди стартап-экосистем Юго-Восточной Азии. Растущая концентрация Web3-разработчиков, ИИ-стартапов в области устойчивого развития и компаний в сфере эко-тревел-технологий формирует нишу столицы импакт-технологий региона.","Инженерия","2026-03-28T10:44:37.953039Z",{"id":45,"title":46,"slug":47,"excerpt":48,"locale":12,"category_name":42,"published_at":49},"d0200000-0000-0000-0000-000000000012","Защита данных в ASEAN: чек-лист разработчика для мультистранового комплаенса","zashchita-dannykh-asean-chek-list-razrabotchika-komplaens","Семь стран ASEAN имеют собственные законы о защите данных с разными моделями согласия, требованиями к локализации и штрафами. Практический чек-лист для разработчиков мультистрановых приложений.","2026-03-28T10:44:37.944001Z",{"id":51,"title":52,"slug":53,"excerpt":54,"locale":12,"category_name":42,"published_at":55},"d0200000-0000-0000-0000-000000000011","Цифровая трансформация Индонезии на $29 миллиардов: возможности для софтверных компаний","tsifrovaya-transformatsiya-indonezii-29-milliardov-vozmozhnosti-dlya-kompaniy","Рынок IT-услуг Индонезии вырастет с $24,37 млрд в 2025 году до $29,03 млрд в 2026 году. Облачная инфраструктура, искусственный интеллект, электронная коммерция и дата-центры обеспечивают самый быстрый рост в Юго-Восточной Азии.","2026-03-28T10:44:37.917095Z",{"id":13,"name":57,"slug":58,"bio":59,"photo_url":19,"linkedin":19,"role":60,"created_at":61,"updated_at":61},"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"]