Foundry es un conjunto de herramientas moderno y potente para el desarrollo de smart contracts en Ethereum, escrito en Rust. Este repositorio documenta mi aprendizaje y práctica con Foundry, incluyendo ejemplos prácticos y explicaciones detalladas.
- Forge: Framework de testing para Ethereum (similar a Truffle/Hardhat)
- Cast: Herramienta CLI para interactuar con contratos EVM
- Anvil: Nodo local de Ethereum (similar a Ganache)
- Chisel: REPL interactivo para Solidity
Este repositorio contiene varios ejemplos prácticos que demuestran diferentes aspectos del desarrollo con Foundry:
Counter.sol
&Counter.t.sol
: Implementación y testing de un contador simpleHolaMundo.sol
&HolaMundo.t.sol
: Primer contrato y sus tests
InvariantExample1.t.sol
: Demostración de testing invarianteForking.t.sol
: Ejemplos de testing con forking de mainnetOwnerUpOnly.sol
&OwnerUpOnly.t.sol
: Testing de control de acceso
Factory.sol
&Factory.t.sol
: Implementación del patrón FactoryUniversity.sol
&University.t.sol
: Ejemplo de interacción entre contratos
SigningExample.sol
&SigningExample.t.sol
: Verificación de firmasSafe.sol
&Safe.t.sol
: Ejemplos de seguridad en contratos
EmitContract.sol
&EmitContract.t.sol
: Testing de eventos
# Compilar contratos
forge build
# Ejecutar todos los tests
forge test
# Ejecutar tests específicos
forge test --match-contract CounterTest
forge test --match-test testIncrement
# Ejecutar tests con más verbosidad
forge test -vvv
# Ejecutar tests con gas reporting
forge test --gas-report
# Ejecutar tests específicos por ruta
forge test --match-path test/Counter.t.sol
# Generar snapshot de gas
forge snapshot
# Comparar con snapshot anterior
forge snapshot --diff
# Análisis detallado de gas
# Ver reporte de gas para todos los tests
forge test --gas-report
# Ver reporte de gas para un contrato específico
forge test --match-contract ContractName --gas-report
# Ver reporte de gas para una función específica
forge test --match-test testFunctionName --gas-report
# Ver gas usado en una función específica con trazas
forge test --match-test testFunctionName -vvvv
# Ver gas usado en todas las funciones de un contrato
forge test --match-contract ContractName -vvvv
# Generar reporte de gas en formato JSON
forge test --gas-report --json > gas-report.json
# Ver gas usado en una transacción específica
forge test --match-test testFunctionName --trace-gas
El reporte de gas muestra información detallada sobre el consumo de gas:
| Contract | Method | Min | Max | Avg | # calls |
|--------------------|---------------|---------|---------|---------|---------|
| Counter | increment | 28347 | 28347 | 28347 | 1 |
| Counter | count | 2214 | 2214 | 2214 | 2 |
- Usar
--gas-report
para obtener una visión general del consumo - Combinar con
-vvvv
para ver trazas detalladas con gas - Comparar diferentes implementaciones con
forge snapshot --diff
- Usar
--trace-gas
para análisis profundo de transacciones específicas - Exportar a JSON para análisis posterior o comparativas
# Iniciar nodo local (Anvil)
anvil
# Desplegar contrato
forge create Contract --private-key <PRIVATE_KEY>
forge create Contract --interactive
# Script de deployment
forge script script/Deploy.s.sol --rpc-url <RPC_URL> --private-key <PRIVATE_KEY>
# Llamar a función de contrato
cast call <CONTRACT_ADDRESS> "functionName()"
# Enviar transacción
cast send <CONTRACT_ADDRESS> "functionName()" --private-key <PRIVATE_KEY>
# Obtener balance
cast balance <ADDRESS>
-
Testing Eficiente
- Usar
forge test -vvv
para debugging detallado - Implementar fuzzing tests para mayor cobertura
- Utilizar invariantes para testing de propiedades
- Entender las trazas de los tests:
forge test -vvv
: Muestra trazas para tests fallidosforge test -vvvv
: Muestra trazas para todos los tests- Formato de trazas:
[<Gas Usage>] <Contract>::<Function>(<Parameters>) ├─ [<Gas Usage>] <Contract>::<Function>(<Parameters>) │ └─ ← <Return Value> └─ ← <Return Value>
- Colores en las trazas:
- Verde: Llamadas exitosas
- Rojo: Llamadas revertidas
- Azul: Llamadas a códigos de trucos
- Cian: Registros emitidos
- Amarillo: Implementaciones de contratos
- El gas mostrado incluye operaciones entre llamadas (aritmética, lecturas/escrituras)
- Usar
-
Gas Optimization
- Revisar reportes de gas regularmente
- Comparar optimizaciones con
forge snapshot --diff
- Usar
forge test --gas-report
para análisis detallado
-
Debugging
- Usar
console.log()
para debugging - Implementar
vm.trace()
para seguimiento de transacciones - Utilizar
forge test --debug
para debugging interactivo
- Usar
Foundry proporciona un conjunto de cheatcodes que permiten manipular el estado de la blockchain y facilitar el testing. Aquí están los más comunes:
Cheatcode | Descripción | Ejemplo |
---|---|---|
vm.warp(uint256) |
Cambia el timestamp del bloque | vm.warp(100) |
vm.roll(uint256) |
Cambia el número de bloque | vm.roll(1000) |
vm.deal(address, uint256) |
Establece el balance de ETH | vm.deal(address, 1 ether) |
vm.startPrank(address) |
Cambia el msg.sender | vm.startPrank(alice) |
vm.stopPrank() |
Restaura el msg.sender original | vm.stopPrank() |
vm.prank(address) |
Cambia msg.sender para una sola llamada | vm.prank(alice) |
vm.mockCall(address, bytes, bytes) |
Mockea una llamada a contrato | vm.mockCall(target, data, returnData) |
Cheatcode | Descripción | Ejemplo |
---|---|---|
deal(address, address, uint256) |
Establece balance de ERC20 | deal(token, user, 1000e18) |
dealERC721(address, address, uint256) |
Transfiere NFT | dealERC721(nft, user, tokenId) |
dealERC1155(address, address, uint256, uint256) |
Establece balance ERC1155 | dealERC1155(token, user, id, amount) |
Cheatcode | Descripción | Ejemplo |
---|---|---|
vm.expectRevert() |
Espera que la siguiente llamada revierta | vm.expectRevert() |
vm.expectRevert(bytes) |
Espera revert con mensaje específico | vm.expectRevert("Error") |
vm.assume(bool) |
Filtra casos en fuzzing | vm.assume(amount > 0) |
vm.expectCall(address, bytes) |
Verifica llamada a contrato | vm.expectCall(target, data) |
Cheatcode | Descripción | Ejemplo |
---|---|---|
vm.createSelectFork(string) |
Crea fork de una red | vm.createSelectFork("mainnet") |
vm.createSelectFork(string, uint256) |
Fork en bloque específico | vm.createSelectFork("mainnet", 12345678) |
vm.snapshot() |
Crea snapshot del estado | uint256 id = vm.snapshot() |
vm.revertTo(uint256) |
Revierte a snapshot | vm.revertTo(id) |
// Cambiar timestamp y número de bloque
vm.warp(block.timestamp + 1 days);
vm.roll(block.number + 1);
// Manipular balances y cuentas
vm.deal(address(this), 1 ether);
vm.startPrank(alice);
contract.function();
vm.stopPrank();
// Mockear llamadas
vm.mockCall(
address(token),
abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)),
abi.encode(1000e18)
);
// Testing de revert
vm.expectRevert("Insufficient balance");
contract.transfer(address(0), 1000e18);
// Forking
vm.createSelectFork("mainnet", 12345678);
-
Organización
- Usar
setUp()
para configuración común - Agrupar cheatcodes relacionados
- Documentar casos de uso complejos
- Usar
-
Buenas Prácticas
- Limpiar estado después de cada test
- Usar
vm.stopPrank()
después destartPrank()
- Verificar aserciones después de manipular estado
-
Debugging
- Usar
-vvv
para ver trazas detalladas - Combinar con
console.log()
para debugging - Verificar estado antes y después de operaciones
- Usar
Foundry permite gestionar múltiples wallets para testing y deployment. Aquí te explico cómo configurarlas y usarlas:
Para ver las wallets configuradas:
cast wallet list
✅ Dónde se guardan las wallets:
- Las wallets se guardan en tu directorio home (
~/.foundry/
o similar) - Son globales para todo tu sistema
- Puedes usarlas desde cualquier proyecto de Foundry
✅ Dónde ejecutar los comandos:
- Puedes ejecutar
cast wallet
desde cualquier directorio - No necesitas estar fuera de
~/tutorial_foundry
- Puedes ejecutarlos desde dentro del proyecto
# Importar wallet con private key
cast wallet import <WALLET_NAME> --private-key <PRIVATE_KEY>
# Ejemplo:
cast wallet import deployer --private-key 0x1234567890abcdef...
# Crear nueva wallet (genera nueva private key)
# Nota: En algunas versiones, el nombre debe ser un directorio
mkdir my-wallet
cast wallet new my-wallet
# O usar un nombre sin guiones
cast wallet new mywallet
# Importar desde mnemónica
cast wallet import <WALLET_NAME> --mnemonic "word1 word2 word3..."
# Ejemplo:
cast wallet import test-wallet --mnemonic "test test test test test test test test test test test junk"
# Listar todas las wallets
cast wallet list
# Ver detalles de una wallet específica
cast wallet show <WALLET_NAME>
# Eliminar una wallet
cast wallet remove <WALLET_NAME>
# Cambiar wallet por defecto
cast wallet default <WALLET_NAME>
# Usar wallet específica en comando
cast send <CONTRACT_ADDRESS> "function()" --wallet <WALLET_NAME>
# Ejemplo:
cast send 0x123... "increment()" --wallet deployer
# Usar wallet por defecto
cast send <CONTRACT_ADDRESS> "function()"
# 1. Crear wallet nueva (opción 1: crear directorio)
mkdir my-wallet
cast wallet new my-wallet
# O crear wallet nueva (opción 2: nombre simple)
cast wallet new mywallet
# 2. Ver la wallet creada
cast wallet show mywallet
# 3. Usar la wallet
cast send <CONTRACT> "function()" --wallet mywallet
# 4. Verificar balance
cast balance <ADDRESS> --wallet mywallet
Este proyecto incluye una configuración específica para la wallet scaffold-eth-default
que se usa para despliegues automáticos:
1. Se generó e importó una cuenta con cast wallet import
cast wallet import --private-key 0xTU_CLAVE_PRIVADA --unsafe-password 'localhost' scaffold-eth-default
Qué hace este comando:
- Importa la private key para pruebas o despliegues locales
- Crea un keystore cifrado usando la contraseña 'localhost'
- Asigna el alias
scaffold-eth-default
a esa wallet
🔐 Nota: --unsafe-password
solo debe usarse en entornos de desarrollo. ¡Nunca en producción!
2. Se almacenó en:
~/.foundry/keystores/scaffold-eth-default
Este archivo contiene la clave privada cifrada.
3. Se configuró para ser usada en los despliegues de forge
Gracias al Makefile personalizado, se añadieron condiciones para detectar si se usaba el keystore scaffold-eth-default
. Esto permitió ejecutar despliegues con:
make deploy
Y automáticamente se usó la wallet importada con:
forge script script/Deploy.s.sol \
--rpc-url <tu_rpc> \
--broadcast \
--password localhost \
--legacy \
--ffi
4. Se integró en el Makefile
La sección setup-anvil-wallet
del Makefile se encargaba de crear todo esto automáticamente:
setup-anvil-wallet:
shx rm ~/.foundry/keystores/scaffold-eth-default 2>/dev/null; \
shx rm -rf broadcast/Deploy.s.sol/31337
cast wallet import --private-key 0x2a871d... --unsafe-password 'localhost' scaffold-eth-default
Para confirmar que la wallet fue correctamente importada y está lista para usarse:
cast wallet address --account scaffold-eth-default
Este repositorio está en constante evolución mientras continúo aprendiendo y practicando con Foundry. Los ejemplos y documentación se actualizarán regularmente con nuevos conceptos y mejores prácticas.
Las contribuciones son bienvenidas. Por favor, abre un issue o pull request para sugerir mejoras o correcciones.
MIT