Seguridad de AMM en DeFi
Patrones de vulnerabilidades críticas e implementaciones endurecidas para contratos Solidity AMM, vaults de LP y funciones de swap.
Cuándo Usarlo
- Al escribir o auditar un contrato Solidity AMM o de pool de liquidez
- Al implementar flujos de swap, depósito, retiro, acuñación o quema que manejen saldos de tokens
- Al revisar cualquier contrato que use
token.balanceOf(address(this))en matemáticas de participaciones o reservas - Al añadir funciones administrativas como setters de tarifas, pausadores, actualizaciones de oráculos a un protocolo DeFi
Cómo Funciona
Úsalo como una biblioteca de lista de verificación más patrones. Revisa cada punto de entrada de usuario contra las categorías a continuación y prefiere los ejemplos endurecidos sobre variantes hechas a mano.
Seguridad de Ejecución
Los comandos de shell en esta habilidad son ejemplos locales de auditoría. Ejecútalos solo en un checkout de confianza o en un entorno aislado desechable, y no insertes nombres de contratos, rutas, URLs RPC, claves privadas o indicadores suministrados por el usuario no confiables en los comandos de shell. Pregunta antes de instalar herramientas o ejecutar trabajos largos de fuzzing/análisis estático que puedan consumir recursos locales o pagados significativos.
Nunca incluyas secretos, claves privadas, frases semilla, tokens de API o credenciales de firma de mainnet en ejemplos de comandos, registros o informes.
Ejemplos
Reentrancia: aplicar orden CEI
Vulnerable:
function retirar(uint256 cantidad) external {
require(saldos[msg.sender] >= cantidad);
token.transfer(msg.sender, cantidad);
saldos[msg.sender] -= cantidad;
}
Seguro:
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
function retirar(uint256 cantidad) external nonReentrant {
require(saldos[msg.sender] >= cantidad, "Insuficiente");
saldos[msg.sender] -= cantidad;
token.safeTransfer(msg.sender, cantidad);
}
No escribas tu propio guardia cuando existe una biblioteca endurecida.
Ataques de donación o inflación
Usar token.balanceOf(address(this)) directamente para matemáticas de participaciones permite a atacantes manipular el denominador enviando tokens al contrato fuera de la ruta prevista.
// Vulnerable
function depositar(uint256 activos) external returns (uint256 participaciones) {
participaciones = (activos * totalParticipaciones) / token.balanceOf(address(this));
}
// Seguro
uint256 private _totalActivos;
function depositar(uint256 activos) external nonReentrant returns (uint256 participaciones) {
uint256 saldoAntes = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), activos);
uint256 recibido = token.balanceOf(address(this)) - saldoAntes;
participaciones = totalParticipaciones == 0 ? recibido : (recibido * totalParticipaciones) / _totalActivos;
_totalActivos += recibido;
totalParticipaciones += participaciones;
}
Realiza un seguimiento de la contabilidad interna y mide los tokens realmente recibidos.
Manipulación de oráculos
Los precios spot son manipulables con préstamos flash. Prefiere TWAP.
uint32[] memory segundosAtras = new uint32[](2);
segundosAtras[0] = 1800;
segundosAtras[1] = 0;
(int56[] memory acumuladosTick,) = IUniswapV3Pool(pool).observe(segundosAtras);
int24 tickTwap = int24(
(acumuladosTick[1] - acumuladosTick[0]) / int56(uint56(30 minutes))
);
uint160 sqrtPrecioX96 = TickMath.getSqrtRatioAtTick(tickTwap);
Protección contra slippage
Cada ruta de swap necesita slippage proporcionado por el llamante y una fecha límite.
function swap(
uint256 cantidadEntrada,
uint256 cantidadMinimaSalida,
uint256 fechaLimite
) external returns (uint256 cantidadSalida) {
require(block.timestamp <= fechaLimite, "Expirado");
cantidadSalida = _calcularSalida(cantidadEntrada);
require(cantidadSalida >= cantidadMinimaSalida, "Slippage excedido");
_ejecutarSwap(cantidadEntrada, cantidadSalida);
}
Matemáticas seguras de reservas
import {FullMath} from "@uniswap/v3-core/contracts/libraries/FullMath.sol";
uint256 resultado = FullMath.mulDiv(a, b, c);
Para matemáticas de reservas grandes, evita a * b / c ingenuo cuando exista riesgo de desbordamiento.
Controles administrativos
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
contract MiAMM is Ownable2Step {
function establecerTarifa(uint256 tarifa) external onlyOwner { ... }
function pausar() external onlyOwner { ... }
}
Prefiere aceptación explícita para la transferencia de propiedad y controla cada ruta privilegiada.
Lista de Verificación de Seguridad
- Los puntos de entrada expuestos a reentrancia usan
nonReentrant - Se respeta el orden CEI
- Las matemáticas de participaciones no dependen de
balanceOf(address(this))sin procesar - Las transferencias ERC-20 usan
SafeERC20 - Los depósitos miden los tokens realmente recibidos
- Las lecturas de oráculos usan TWAP u otra fuente resistente a manipulación
- Los swaps requieren
amountOutMinydeadline - Las matemáticas de reservas sensibles a desbordamiento usan primitivas seguras como
mulDiv - Las funciones administrativas tienen control de acceso
- Existe una pausa de emergencia y está probada
- Se ejecutan análisis estático y fuzzing antes de producción
Herramientas de Auditoría
pip install slither-analyzer
slither . --exclude-dependencies
echidna-test . --contract TuAMM --config echidna.yaml
forge test --fuzz-runs 10000