Saltar al contenido
TrautsLablab

Ejecutable local con Foundry

Oráculos y liquidación

Ver cómo el precio del oráculo decide una liquidación y por qué un precio viejo debe hacer fallar la lectura.

Duración
25–40 min
Nivel
Intermedio DeFi
Pasos
3
Versión
3.29.0-alpha.1

Modelo mental

El protocolo no ve el mercado: ve el número que le entrega el feed. Quien controla ese número controla las liquidaciones, y un precio viejo es tan peligroso como uno falso.

  1. 1Fixture
  2. 2Precio movido
  3. 3Precio viejo
  4. 4Reporte
  5. 5Cleanup

Antes de empezar

  • Foundry instalado: forge en el PATH (https://getfoundry.sh).
  • Primera ejecución con internet: forge descarga solc 0.8.28 si no lo tienes.

Pasos

  1. Paso 1Correr la suite

    Confirmar que los dos casos pasan antes de mirarlos por separado.

    Fixture y tests
    mkdir -p .tmp/oracle-liquidation-lab
    cp -R tools/labs/ethereum-oracle-liquidation-lab/templates/. .tmp/oracle-liquidation-lab/
    cd .tmp/oracle-liquidation-lab
    forge test -vv

    Comprueba

    • — forge test: 2 passed; 0 failed.

    Evidencia esperada

    Salida de forge test.

  2. Paso 2Precio movido

    Ver que el mismo colateral y la misma deuda cambian de estado solo porque cambió el precio.

    Traza
    forge test -vvvv --match-test testPriceChangesLiquidationDecision

    Comprueba

    • — Con precio 200, liquidatable devuelve false (health 200 >= 150).
    • — Con precio 100, liquidatable devuelve true (health 100 < 150).
    • — MockPriceFeed::set no tiene control de acceso: cualquiera que lo llame decide quién es liquidable.

    Evidencia esperada

    Traza con los dos valores de price() y los dos resultados de liquidatable.

  3. Paso 3Precio viejo

    Comprobar que el protocolo falla cerrado cuando el precio supera MAX_AGE.

    Traza
    forge test -vvvv --match-test testStalePriceReverts

    Comprueba

    • — healthAt con updatedAt 0 y observedAt 4001 revierte con "stale price" (4001 s > 3600 s).

    Evidencia esperada

    Línea de la traza con [Revert] stale price.

El código del laboratorio

Estos son los archivos que ejecutan los pasos, tal como están en el repositorio .

tools/labs/ethereum-oracle-liquidation-lab/runbook.md
# Ethereum oracle and liquidation lab

Version: `2.0.44-alpha.2`

Copy the fixture into `.tmp/oracle-liquidation-lab`, run `forge test -vv`, then inspect the stale-price and manipulated-price cases separately.

```bash
mkdir -p .tmp/oracle-liquidation-lab
cp -R tools/labs/ethereum-oracle-liquidation-lab/templates/. .tmp/oracle-liquidation-lab/
cd .tmp/oracle-liquidation-lab
forge test -vv
forge test -vvvv --match-test testPriceChangesLiquidationDecision
forge test -vvvv --match-test testStalePriceReverts
```

- Manipulated price: `MockPriceFeed::set` has no access control, so whoever writes the price decides who is liquidatable (200 → healthy, 100 → `liquidatable` returns `true`).
- Stale price: a price older than `MAX_AGE` makes `healthAt` revert with `stale price` instead of returning a number.

The mock exists to expose assumptions. It is not a production oracle implementation.

Cleanup: `rm -rf .tmp/oracle-liquidation-lab`.
tools/labs/ethereum-oracle-liquidation-lab/templates/foundry.toml
[profile.default]
src = "src"
test = "test"
out = "out"
solc_version = "0.8.28"
tools/labs/ethereum-oracle-liquidation-lab/templates/src/OracleLiquidationLab.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract MockPriceFeed {
    uint256 public price;
    uint256 public updatedAt;
    function set(uint256 price_, uint256 updatedAt_) external { price = price_; updatedAt = updatedAt_; }
}

contract LendingFixture {
    MockPriceFeed public immutable feed;
    uint256 public constant MAX_AGE = 1 hours;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;

    constructor(MockPriceFeed feed_) { feed = feed_; }
    function position(address user, uint256 collateral_, uint256 debt_) external {
        collateral[user] = collateral_;
        debt[user] = debt_;
    }
    function healthAt(address user, uint256 observedAt) public view returns (uint256) {
        require(observedAt >= feed.updatedAt(), "future price");
        require(observedAt - feed.updatedAt() <= MAX_AGE, "stale price");
        if (debt[user] == 0) return type(uint256).max;
        return collateral[user] * feed.price() * 100 / debt[user];
    }
    function health(address user) public view returns (uint256) { return healthAt(user, block.timestamp); }
    function liquidatable(address user) external view returns (bool) { return health(user) < 150; }
}
tools/labs/ethereum-oracle-liquidation-lab/templates/test/OracleLiquidationLab.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {MockPriceFeed, LendingFixture} from "../src/OracleLiquidationLab.sol";

contract OracleLiquidationLabTest {
    function testPriceChangesLiquidationDecision() public {
        MockPriceFeed feed = new MockPriceFeed();
        LendingFixture lending = new LendingFixture(feed);
        lending.position(address(this), 2, 200);
        feed.set(200, block.timestamp);
        require(!lending.liquidatable(address(this)), "healthy");
        feed.set(100, block.timestamp);
        require(lending.liquidatable(address(this)), "liquidatable");
    }

    function testStalePriceReverts() public {
        MockPriceFeed feed = new MockPriceFeed();
        LendingFixture lending = new LendingFixture(feed);
        lending.position(address(this), 2, 200);
        feed.set(200, 0);
        (bool ok,) = address(lending).call(abi.encodeCall(lending.healthAt, (address(this), 4001)));
        require(!ok, "stale price must fail closed");
    }
}

Criterios de aceptación

  • forge test muestra 2 passed.
  • Puedes explicar quién controla el precio en el mock y qué control le falta.
  • Puedes explicar por qué revertir es mejor que liquidar con un precio viejo.

Limpieza

Fixture
cd ../..
rm -rf .tmp/oracle-liquidation-lab

Si algo falla

Síntoma, causa probable y cómo corregirlo
SíntomaCausaQué hacer
--match-test no ejecuta nadaEl nombre del test está mal escrito.Copia el nombre exacto de la salida de forge test -vv.
testStalePriceReverts fallaSe subió MAX_AGE por encima de 4001 segundos.Ajusta observedAt en el test o vuelve a MAX_AGE = 1 hours.