Langsung ke konten utama
BlockchainMar 28, 2026

Property-Based Testing untuk Smart Contract — Fuzzing dengan Foundry

OS
Open Soft Team

Engineering Team

Mengapa Unit Test Tidak Cukup

Unit test memverifikasi bahwa kode Anda bekerja untuk input spesifik yang Anda pilih. Tetapi smart contract menangani input dari pengguna mana pun — termasuk penyerang yang secara aktif mencari edge case. Property-based testing membalikkan pendekatan: alih-alih menentukan input, Anda mendefinisikan properti yang harus selalu benar, dan fuzzer mencari input yang melanggarnya.

Properti vs Unit Test

// Unit test: satu input spesifik
function testTransfer() public {
    token.transfer(alice, 100);
    assertEq(token.balanceOf(alice), 100);
}

// Property test: untuk SEMUA input yang valid
function testFuzz_Transfer(address to, uint256 amount) public {
    amount = bound(amount, 0, token.balanceOf(address(this)));
    vm.assume(to != address(0));
    
    uint256 totalBefore = token.totalSupply();
    token.transfer(to, amount);
    uint256 totalAfter = token.totalSupply();
    
    // PROPERTI: total supply tidak berubah
    assertEq(totalBefore, totalAfter);
}

Mendefinisikan Invariant

Invariant adalah kondisi yang harus SELALU benar, terlepas dari urutan operasi:

Invariant ERC20

  1. totalSupply == sum(balances[semua_address])
  2. transfer tidak mengubah totalSupply
  3. balanceOf(x) tidak pernah negatif (dijamin oleh uint256)
  4. transfer(to, amount) mengurangi pengirim dan menambah penerima persis amount

Invariant AMM (Uniswap V2)

  1. reserve0 * reserve1 >= k (setelah fee)
  2. reserve0 > 0 && reserve1 > 0 (selalu memiliki likuiditas)
  3. Swap tidak pernah mengeluarkan lebih dari reserve yang ada
  4. price_impact meningkat secara monoton dengan ukuran swap

Invariant Lending Protocol

  1. total_deposited >= total_borrowed
  2. Posisi dengan health_factor > 1 tidak bisa dilikuidasi
  3. Likuidasi selalu mengurangi bad debt

Fuzzing dengan Foundry

Dasar: Fuzz Test

function testFuzz_DepositWithdraw(
    uint256 depositAmount,
    uint256 withdrawAmount
) public {
    depositAmount = bound(depositAmount, 1, 1000 ether);
    withdrawAmount = bound(withdrawAmount, 1, depositAmount);
    
    deal(address(token), address(this), depositAmount);
    token.approve(address(vault), depositAmount);
    vault.deposit(depositAmount);
    
    uint256 sharesBefore = vault.balanceOf(address(this));
    vault.withdraw(withdrawAmount);
    
    // Properti: shares berkurang proporsional
    assertLe(
        vault.balanceOf(address(this)),
        sharesBefore
    );
}

Lanjutan: Invariant Testing

contract VaultInvariant is Test {
    Vault vault;
    Token token;
    InvariantHandler handler;
    
    function setUp() public {
        token = new Token();
        vault = new Vault(address(token));
        handler = new InvariantHandler(vault, token);
        
        targetContract(address(handler));
    }
    
    // Foundry memanggil fungsi handler secara acak
    function invariant_totalAssetsMatchDeposits() public {
        assertEq(
            vault.totalAssets(),
            handler.ghost_totalDeposited() - handler.ghost_totalWithdrawn()
        );
    }
    
    function invariant_solvency() public {
        assertGe(
            token.balanceOf(address(vault)),
            vault.totalAssets()
        );
    }
}

contract InvariantHandler is Test {
    Vault vault;
    Token token;
    uint256 public ghost_totalDeposited;
    uint256 public ghost_totalWithdrawn;
    
    function deposit(uint256 amount) external {
        amount = bound(amount, 1, 100 ether);
        deal(address(token), address(this), amount);
        token.approve(address(vault), amount);
        vault.deposit(amount);
        ghost_totalDeposited += amount;
    }
    
    function withdraw(uint256 amount) external {
        uint256 max = vault.balanceOf(address(this));
        if (max == 0) return;
        amount = bound(amount, 1, max);
        vault.withdraw(amount);
        ghost_totalWithdrawn += amount;
    }
}

Konfigurasi Fuzzing

# foundry.toml
[fuzz]
runs = 10000           # Iterasi per fuzz test
max_test_rejects = 1000 # Skip input yang tidak valid
seed = 42              # Untuk reprodusibilitas

[invariant]
runs = 256             # Urutan operasi acak
depth = 100            # Operasi per urutan
fail_on_revert = false # Jangan gagal pada revert handler

Menemukan Kerentanan Nyata

Contoh kerentanan yang ditemukan oleh fuzzing:

1. Rounding Error di Vault

// Bug: deposit 1 wei menghasilkan 0 shares (pembulatan ke bawah)
// tetapi token tetap ditransfer ke vault
function testFuzz_MinDeposit(uint256 amount) public {
    amount = bound(amount, 1, 100);
    deal(address(token), address(this), amount);
    token.approve(address(vault), amount);
    
    uint256 shares = vault.deposit(amount);
    
    // Ditemukan: shares == 0 untuk amount == 1
    // Token hilang selamanya!
    assertGt(shares, 0, "Zero shares minted");
}

2. Overflow di Kalkulasi Harga

function testFuzz_PriceOverflow(
    uint256 reserve0,
    uint256 reserve1,
    uint256 amountIn
) public {
    reserve0 = bound(reserve0, 1e18, 1e30);
    reserve1 = bound(reserve1, 1e18, 1e30);
    amountIn = bound(amountIn, 1e18, 1e28);
    
    // Ditemukan: mul(amountIn, 997) overflow untuk amountIn besar
    uint256 output = getAmountOut(amountIn, reserve0, reserve1);
    assertGt(output, 0);
}

Differential Testing

Bandingkan implementasi Huff dengan referensi Solidity:

function testFuzz_Differential(
    uint256 a,
    uint256 b
) public {
    vm.assume(b > 0);  // Hindari division by zero
    
    // Referensi Solidity
    uint256 expected = a / b;
    
    // Implementasi Huff
    (bool ok, bytes memory data) = huffContract.call(
        abi.encodeWithSignature("div(uint256,uint256)", a, b)
    );
    assertTrue(ok);
    uint256 actual = abi.decode(data, (uint256));
    
    assertEq(actual, expected, "Huff div mismatch");
}

Kesimpulan

Property-based testing dan fuzzing adalah senjata paling kuat untuk menemukan bug smart contract. Definisikan invariant yang jelas, tulis handler yang mencakup semua operasi, dan jalankan ribuan iterasi. Untuk kontrak Huff, tambahkan differential testing terhadap referensi Solidity untuk memverifikasi kebenaran di tingkat opcode.