Ir al contenido principal
BlockchainMar 28, 2026

Deep EVM #19: Testing Basado en Propiedades para Contratos Inteligentes — Fuzzing con Foundry

OS
Open Soft Team

Engineering Team

Más allá de los unit tests

Los unit tests verifican escenarios específicos que el desarrollador imagina. El testing basado en propiedades verifica que las propiedades del sistema se mantienen para TODOS los inputs posibles. Foundry lo hace generando miles de inputs aleatorios.

Propiedades fundamentales de tokens ERC-20

// La suma de todos los balances siempre iguala totalSupply
function invariant_balanceSumEqualsTotalSupply() public {
    uint256 sum = 0;
    for (uint i = 0; i < actors.length; i++) {
        sum += token.balanceOf(actors[i]);
    }
    assertEq(sum, token.totalSupply());
}

// Transfer no puede crear tokens de la nada
function testFuzz_transferConservesSupply(
    address from, address to, uint256 amount
) public {
    vm.assume(from != address(0) && to != address(0));
    
    uint256 supplyBefore = token.totalSupply();
    uint256 fromBefore = token.balanceOf(from);
    uint256 toBefore = token.balanceOf(to);
    
    deal(address(token), from, amount);
    vm.prank(from);
    token.transfer(to, amount);
    
    assertEq(token.totalSupply(), supplyBefore + amount);
}

Fuzzing guiado por cobertura

Foundry usa un fuzzer que aprende de ejecuciones anteriores para explorar nuevas rutas de código:

# foundry.toml
[fuzz]
runs = 10000          # Número de ejecuciones por test
max_test_rejects = 65536
seed = 0x42           # Semilla para reproducibilidad
dictionary_weight = 40

El fuzzer es especialmente efectivo para encontrar:

  • Overflows en aritmética sin checks
  • Condiciones de borde con valores extremos (0, type(uint256).max)
  • Interacciones no previstas entre funciones

Invariant testing avanzado

Foundry permite definir handlers que el fuzzer llama aleatoriamente:

contract TokenHandler is Test {
    ERC20 token;
    address[] actors;
    
    constructor(ERC20 _token, address[] memory _actors) {
        token = _token;
        actors = _actors;
    }
    
    function transfer(uint256 fromIdx, uint256 toIdx, uint256 amount) public {
        fromIdx = bound(fromIdx, 0, actors.length - 1);
        toIdx = bound(toIdx, 0, actors.length - 1);
        amount = bound(amount, 0, token.balanceOf(actors[fromIdx]));
        
        vm.prank(actors[fromIdx]);
        token.transfer(actors[toIdx], amount);
    }
    
    function approve(uint256 ownerIdx, uint256 spenderIdx, uint256 amount) public {
        ownerIdx = bound(ownerIdx, 0, actors.length - 1);
        spenderIdx = bound(spenderIdx, 0, actors.length - 1);
        
        vm.prank(actors[ownerIdx]);
        token.approve(actors[spenderIdx], amount);
    }
}

Testing de propiedades para AMMs

// La invariante k nunca decrece (excluyendo fees)
function invariant_kNeverDecreases() public {
    uint256 r0 = pool.reserve0();
    uint256 r1 = pool.reserve1();
    uint256 k = r0 * r1;
    assertGe(k, lastK, "k no debe decrecer");
    lastK = k;
}

// Ningún swap puede vaciar completamente las reservas
function invariant_reservesNeverZero() public {
    assertGt(pool.reserve0(), 0);
    assertGt(pool.reserve1(), 0);
}

Ejemplo: encontrando un bug real

Consideremos un contrato con un bug sutil en la lógica de allowance:

function testFuzz_doubleSpend(
    address owner, address spender, uint256 amount1, uint256 amount2
) public {
    vm.assume(owner != address(0) && spender != address(0));
    amount1 = bound(amount1, 1, type(uint128).max);
    amount2 = bound(amount2, 1, type(uint128).max);
    
    deal(address(token), owner, amount1 + amount2);
    
    vm.prank(owner);
    token.approve(spender, amount1);
    
    vm.prank(spender);
    token.transferFrom(owner, spender, amount1);
    
    // Segundo transferFrom debería fallar
    vm.prank(spender);
    vm.expectRevert();
    token.transferFrom(owner, spender, amount2);
}

Este fuzz test podría descubrir que el contrato no decrementa el allowance correctamente, permitiendo un doble gasto.

Conclusión

El testing basado en propiedades con Foundry es la herramienta más poderosa para descubrir vulnerabilidades en contratos inteligentes. Define propiedades que deben ser siempre verdaderas, deja que el fuzzer genere miles de escenarios, y confía en que los invariant tests capturen violaciones que ningún unit test individual habría encontrado.

Etiquetas