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.
- 1Fixture
- 2Precio movido
- 3Precio viejo
- 4Reporte
- 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
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 -vvComprueba
- — forge test: 2 passed; 0 failed.
Evidencia esperada
Salida de forge test.
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 testPriceChangesLiquidationDecisionComprueba
- — 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.
Paso 3Precio viejo
Comprobar que el protocolo falla cerrado cuando el precio supera MAX_AGE.
Traza forge test -vvvv --match-test testStalePriceRevertsComprueba
- — 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 .
# 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`.[profile.default]
src = "src"
test = "test"
out = "out"
solc_version = "0.8.28"// 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; }
}// 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
cd ../..
rm -rf .tmp/oracle-liquidation-labSi algo falla
| Síntoma | Causa | Qué hacer |
|---|---|---|
| --match-test no ejecuta nada | El nombre del test está mal escrito. | Copia el nombre exacto de la salida de forge test -vv. |
| testStalePriceReverts falla | Se subió MAX_AGE por encima de 4001 segundos. | Ajusta observedAt en el test o vuelve a MAX_AGE = 1 hours. |