[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-19-property-based-testing-fuzzing-foundry":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},"d0000000-0000-0000-0000-000000000119","a0000000-0000-0000-0000-000000000002","Deep EVM #19: Property-Based Testing for Smart Contracts — Fuzzing with Foundry","deep-evm-19-property-based-testing-fuzzing-foundry","Explore property-based testing and fuzzing for smart contracts using Foundry. Covers fuzz inputs, invariant testing, and differential testing of Huff, Yul, and Solidity.","## Why Fuzzing Catches Bugs That Unit Tests Miss\n\nUnit tests verify specific scenarios: transfer 100 tokens from Alice to Bob, check balance is updated. But smart contracts face adversarial inputs from any address, with any value, in any order. A unit test that checks 10 scenarios cannot compete with a fuzzer that generates 100,000 random inputs looking for edge cases.\n\nProperty-based testing flips the paradigm. Instead of specifying expected outputs for specific inputs, you define properties that must hold for ALL inputs. The fuzzer then tries to find a counterexample:\n\n```solidity\n\u002F\u002F Unit test: specific input\nfunction test_transfer() public {\n    token.transfer(bob, 100);\n    assertEq(token.balanceOf(bob), 100);\n}\n\n\u002F\u002F Property-based test: for ALL inputs\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's fuzzer uses intelligent strategies: it starts with boundary values (0, 1, `type(uint256).max`), then mixes random values, then uses coverage-guided mutation to explore new code paths.\n\n## Configuring the Fuzzer\n\nFoundry's fuzz configuration controls the depth and breadth of exploration:\n\n```toml\n# foundry.toml\n[profile.default.fuzz]\nruns = 10000            # Number of random inputs per test\nmax_test_rejects = 100000  # Max rejected inputs before failure\nseed = 0x42             # Fixed seed for reproducibility\ndictionary_weight = 40  # Weight for dictionary-based mutations\n\n[profile.ci.fuzz]\nruns = 100000           # More runs in CI\n```\n\nThe `dictionary_weight` parameter controls how often the fuzzer reuses interesting values discovered during execution (like addresses found in storage) versus generating completely random inputs.\n\n## bound() vs assume(): Constraining Inputs\n\nFuzzing generates arbitrary inputs, but your contract may have valid input ranges. There are two ways to handle this:\n\n### assume(): Skip Invalid Inputs\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 Test with valid amounts only\n}\n```\n\n`assume()` discards inputs that do not meet the condition. Problem: if most inputs are invalid, the fuzzer wastes most of its runs. With `amount \u003C= balance`, approximately 50% of random uint256 values are too large, and the fuzzer will reject them.\n\n### bound(): Transform Invalid Inputs\n\n```solidity\nfunction testFuzz_withdraw(uint256 amount) public {\n    amount = bound(amount, 1, token.balanceOf(alice));\n    \u002F\u002F Every input is valid — no wasted runs\n}\n```\n\n`bound()` maps any uint256 into the valid range using modular arithmetic. This is almost always preferable because no runs are wasted. Use `assume()` only for complex conditions that cannot be expressed as a range.\n\n## Invariant Testing: Stateful Fuzzing\n\nFuzz tests above are stateless — each run is independent. Invariant tests are stateful: the fuzzer calls a sequence of functions in random order, then checks that invariants hold after each call:\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 This MUST hold after ANY sequence of calls\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 Handler bounds and guides the fuzzer\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\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\nConfigure invariant testing:\n\n```toml\n[profile.default.invariant]\nruns = 256          # Number of call sequences\ndepth = 128         # Calls per sequence\nfail_on_revert = false  # Don't fail on handler reverts\n```\n\nThe fuzzer generates 256 sequences of 128 random function calls, then checks all invariants after each call. This is enormously powerful for finding state-dependent bugs.\n\n## Differential Testing: Huff vs Yul vs Solidity\n\nDifferential testing is the gold standard for verifying low-level EVM implementations. Deploy three implementations of the same specification and verify they produce identical results for all inputs:\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        \u002F\u002F Give each implementation identical initial state\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        \u002F\u002F Call all three\n        vm.prank(from);\n        (bool s1,) = address(huffToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n\n        vm.prank(from);\n        (bool s2,) = address(yulToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n\n        vm.prank(from);\n        (bool s3,) = address(solToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n\n        \u002F\u002F All must agree on success\u002Ffailure\n        assertEq(s1, s2, \"Huff vs Yul success mismatch\");\n        assertEq(s2, s3, \"Yul vs Solidity success mismatch\");\n\n        \u002F\u002F If successful, balances must match\n        if (s1) {\n            assertEq(\n                huffToken.balanceOf(from),\n                solToken.balanceOf(from),\n                \"Sender balance mismatch\"\n            );\n            assertEq(\n                huffToken.balanceOf(to),\n                solToken.balanceOf(to),\n                \"Recipient balance mismatch\"\n            );\n        }\n    }\n}\n```\n\n## Real Bugs Caught by Fuzzing\n\nFuzzing has a proven track record of catching critical bugs:\n\n### Bug 1: Phantom Overflow in Huff ERC20\n\nA Huff ERC20 implementation added balances without checking for overflow. The fuzzer found that transferring `type(uint256).max - balance + 1` tokens would wrap the recipient's balance to near-zero:\n\n```\nCounterexample: transfer(to=bob, amount=115792089237316195423570985008687907853269984665640564039457584007913129639436)\n```\n\n### Bug 2: Self-Transfer Double-Spend\n\nWhen `from == to`, a naive Huff implementation would subtract from the balance slot, then add to the same slot, effectively doubling the tokens:\n\n```solidity\n\u002F\u002F Fuzzer found from == to edge case\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### Bug 3: Zero-Address Mint\n\nThe fuzzer discovered that transferring to `address(0)` in a Huff token did not revert, effectively burning tokens without updating total supply. This broke the `sum(balances) == totalSupply` invariant.\n\n## Integrating Fuzzing into CI\n\nRun fuzz tests with higher iteration counts in CI:\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\nUsing `github.run_id` as the seed ensures reproducibility while varying inputs across runs.\n\n## Conclusion\n\nFuzzing transforms smart contract testing from \"does it work for these 10 cases?\" to \"can I break it with any of 100,000 random inputs?\" Property-based testing with `bound()`, invariant testing with stateful sequences, and differential testing across implementations form a comprehensive safety net. For Huff and Yul contracts where the compiler provides no safety guarantees, fuzzing is not optional — it is the primary defense against production bugs.","\u003Ch2 id=\"why-fuzzing-catches-bugs-that-unit-tests-miss\">Why Fuzzing Catches Bugs That Unit Tests Miss\u003C\u002Fh2>\n\u003Cp>Unit tests verify specific scenarios: transfer 100 tokens from Alice to Bob, check balance is updated. But smart contracts face adversarial inputs from any address, with any value, in any order. A unit test that checks 10 scenarios cannot compete with a fuzzer that generates 100,000 random inputs looking for edge cases.\u003C\u002Fp>\n\u003Cp>Property-based testing flips the paradigm. Instead of specifying expected outputs for specific inputs, you define properties that must hold for ALL inputs. The fuzzer then tries to find a counterexample:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F Unit test: specific input\nfunction test_transfer() public {\n    token.transfer(bob, 100);\n    assertEq(token.balanceOf(bob), 100);\n}\n\n\u002F\u002F Property-based test: for ALL inputs\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’s fuzzer uses intelligent strategies: it starts with boundary values (0, 1, \u003Ccode>type(uint256).max\u003C\u002Fcode>), then mixes random values, then uses coverage-guided mutation to explore new code paths.\u003C\u002Fp>\n\u003Ch2 id=\"configuring-the-fuzzer\">Configuring the Fuzzer\u003C\u002Fh2>\n\u003Cp>Foundry’s fuzz configuration controls the depth and breadth of exploration:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-toml\"># foundry.toml\n[profile.default.fuzz]\nruns = 10000            # Number of random inputs per test\nmax_test_rejects = 100000  # Max rejected inputs before failure\nseed = 0x42             # Fixed seed for reproducibility\ndictionary_weight = 40  # Weight for dictionary-based mutations\n\n[profile.ci.fuzz]\nruns = 100000           # More runs in CI\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The \u003Ccode>dictionary_weight\u003C\u002Fcode> parameter controls how often the fuzzer reuses interesting values discovered during execution (like addresses found in storage) versus generating completely random inputs.\u003C\u002Fp>\n\u003Ch2 id=\"bound-vs-assume-constraining-inputs\">bound() vs assume(): Constraining Inputs\u003C\u002Fh2>\n\u003Cp>Fuzzing generates arbitrary inputs, but your contract may have valid input ranges. There are two ways to handle this:\u003C\u002Fp>\n\u003Ch3>assume(): Skip Invalid Inputs\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 Test with valid amounts only\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>assume()\u003C\u002Fcode> discards inputs that do not meet the condition. Problem: if most inputs are invalid, the fuzzer wastes most of its runs. With \u003Ccode>amount &lt;= balance\u003C\u002Fcode>, approximately 50% of random uint256 values are too large, and the fuzzer will reject them.\u003C\u002Fp>\n\u003Ch3>bound(): Transform Invalid Inputs\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 Every input is valid — no wasted runs\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>bound()\u003C\u002Fcode> maps any uint256 into the valid range using modular arithmetic. This is almost always preferable because no runs are wasted. Use \u003Ccode>assume()\u003C\u002Fcode> only for complex conditions that cannot be expressed as a range.\u003C\u002Fp>\n\u003Ch2 id=\"invariant-testing-stateful-fuzzing\">Invariant Testing: Stateful Fuzzing\u003C\u002Fh2>\n\u003Cp>Fuzz tests above are stateless — each run is independent. Invariant tests are stateful: the fuzzer calls a sequence of functions in random order, then checks that invariants hold after each call:\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 This MUST hold after ANY sequence of calls\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 Handler bounds and guides the fuzzer\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\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>Configure invariant testing:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-toml\">[profile.default.invariant]\nruns = 256          # Number of call sequences\ndepth = 128         # Calls per sequence\nfail_on_revert = false  # Don't fail on handler reverts\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The fuzzer generates 256 sequences of 128 random function calls, then checks all invariants after each call. This is enormously powerful for finding state-dependent bugs.\u003C\u002Fp>\n\u003Ch2 id=\"differential-testing-huff-vs-yul-vs-solidity\">Differential Testing: Huff vs Yul vs Solidity\u003C\u002Fh2>\n\u003Cp>Differential testing is the gold standard for verifying low-level EVM implementations. Deploy three implementations of the same specification and verify they produce identical results for all inputs:\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        \u002F\u002F Give each implementation identical initial state\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        \u002F\u002F Call all three\n        vm.prank(from);\n        (bool s1,) = address(huffToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n\n        vm.prank(from);\n        (bool s2,) = address(yulToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n\n        vm.prank(from);\n        (bool s3,) = address(solToken).call(\n            abi.encodeCall(IToken.transfer, (to, amount))\n        );\n\n        \u002F\u002F All must agree on success\u002Ffailure\n        assertEq(s1, s2, \"Huff vs Yul success mismatch\");\n        assertEq(s2, s3, \"Yul vs Solidity success mismatch\");\n\n        \u002F\u002F If successful, balances must match\n        if (s1) {\n            assertEq(\n                huffToken.balanceOf(from),\n                solToken.balanceOf(from),\n                \"Sender balance mismatch\"\n            );\n            assertEq(\n                huffToken.balanceOf(to),\n                solToken.balanceOf(to),\n                \"Recipient balance mismatch\"\n            );\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"real-bugs-caught-by-fuzzing\">Real Bugs Caught by Fuzzing\u003C\u002Fh2>\n\u003Cp>Fuzzing has a proven track record of catching critical bugs:\u003C\u002Fp>\n\u003Ch3>Bug 1: Phantom Overflow in Huff ERC20\u003C\u002Fh3>\n\u003Cp>A Huff ERC20 implementation added balances without checking for overflow. The fuzzer found that transferring \u003Ccode>type(uint256).max - balance + 1\u003C\u002Fcode> tokens would wrap the recipient’s balance to near-zero:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Counterexample: transfer(to=bob, amount=115792089237316195423570985008687907853269984665640564039457584007913129639436)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Bug 2: Self-Transfer Double-Spend\u003C\u002Fh3>\n\u003Cp>When \u003Ccode>from == to\u003C\u002Fcode>, a naive Huff implementation would subtract from the balance slot, then add to the same slot, effectively doubling the tokens:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F Fuzzer found from == to edge case\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>Bug 3: Zero-Address Mint\u003C\u002Fh3>\n\u003Cp>The fuzzer discovered that transferring to \u003Ccode>address(0)\u003C\u002Fcode> in a Huff token did not revert, effectively burning tokens without updating total supply. This broke the \u003Ccode>sum(balances) == totalSupply\u003C\u002Fcode> invariant.\u003C\u002Fp>\n\u003Ch2 id=\"integrating-fuzzing-into-ci\">Integrating Fuzzing into CI\u003C\u002Fh2>\n\u003Cp>Run fuzz tests with higher iteration counts in 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>Using \u003Ccode>github.run_id\u003C\u002Fcode> as the seed ensures reproducibility while varying inputs across runs.\u003C\u002Fp>\n\u003Ch2 id=\"conclusion\">Conclusion\u003C\u002Fh2>\n\u003Cp>Fuzzing transforms smart contract testing from “does it work for these 10 cases?” to “can I break it with any of 100,000 random inputs?” Property-based testing with \u003Ccode>bound()\u003C\u002Fcode>, invariant testing with stateful sequences, and differential testing across implementations form a comprehensive safety net. For Huff and Yul contracts where the compiler provides no safety guarantees, fuzzing is not optional — it is the primary defense against production bugs.\u003C\u002Fp>\n","en","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.125793Z","Property-Based Testing for Smart Contracts — Fuzzing with Foundry","Deep dive into property-based testing and fuzzing for smart contracts. Covers fuzz inputs, invariant testing, and differential testing of Huff vs Yul vs Solidity.","smart contract fuzzing 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","Blockchain",[41,48,54],{"id":42,"title":43,"slug":44,"excerpt":45,"locale":12,"category_name":46,"published_at":47},"d0200000-0000-0000-0000-000000000003","Why Bali Is Becoming Southeast Asia's Impact-Tech Hub in 2026","why-bali-becoming-southeast-asia-impact-tech-hub-2026","Bali ranks #16 among Southeast Asian startup ecosystems. With a growing concentration of Web3 builders, AI sustainability startups, and eco-travel tech companies, the island is carving a niche as the region's impact-tech capital.","Engineering","2026-03-28T10:44:37.748283Z",{"id":49,"title":50,"slug":51,"excerpt":52,"locale":12,"category_name":46,"published_at":53},"d0200000-0000-0000-0000-000000000002","ASEAN Data Protection Patchwork: A Developer's Compliance Checklist","asean-data-protection-patchwork-developer-compliance-checklist","Seven ASEAN countries now have comprehensive data protection laws, each with different consent models, localization requirements, and penalty structures. Here is a practical compliance checklist for developers building multi-country applications.","2026-03-28T10:44:37.374741Z",{"id":55,"title":56,"slug":57,"excerpt":58,"locale":12,"category_name":46,"published_at":59},"d0200000-0000-0000-0000-000000000001","Indonesia's $29 Billion Digital Transformation: Opportunities for Software Companies","indonesia-29-billion-digital-transformation-opportunities-software-companies","Indonesia's IT services market is projected to reach $29.03 billion in 2026, up from $24.37 billion in 2025. Cloud infrastructure, AI, e-commerce, and data centers are driving the fastest growth in Southeast Asia.","2026-03-28T10:44:37.349311Z",{"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"]