Escrow no custodial para intercambiar dos tokens ERC20 de forma atómica entre dos partes que no se conocen ni confían entre sí. CodeCrypto Módulo 9.
La DApp está desplegada en producción sobre Ethereum Sepolia (contratos verificados en Etherscan) y servida en Vercel:
▶ escrow-d-app-omega.vercel.app
Para interactuar (crear, completar o cancelar operaciones) necesitas MetaMask apuntando a Sepolia y una wallet con algo de Sepolia ETH de un faucet para pagar el gas. La lectura del estado (operaciones, balances, timeline de actividad) funciona sin conectar wallet; solo crear/completar/cancelar requiere conectar y firmar.
Red: Ethereum Sepolia (chainId 11155111) · deploy block 11300758. El enlace #code lleva
directo a la pestaña del contrato verificado en Etherscan (prueba del despliegue verificado):
| Contrato | Dirección | Etherscan |
|---|---|---|
| Escrow | 0x426F0B446139098D1CAc0883474b7B53c40F285A |
ver código |
| TKA (Token A) | 0xb6eD2eC6Cb1136a7183166cbd52A9dB0D5E86BDD |
ver código |
| TKB (Token B) | 0xEc7A30c09ab46b4a19Ea733C6545FBdE9dBe6154 |
ver código |
- Demo en vivo
- El problema
- Arquitectura
- Decisión técnica clave: CEI hecho arquitectura
- Stack
- Prerequisitos
- Puesta en marcha local
- Cómo usarla
- Despliegue en Sepolia
- Tests
- Variables de entorno
- Estructura del repo
- Ramas
Dos personas quieren intercambiar tokens: Alice ofrece 100 TKA y quiere 150 TKB de Bob. Sin un tercero de confianza, el que mueve primero pierde: si Alice envía sus TKA, Bob puede quedárselos y no pagar.
Un escrow rompe esa asimetría. Alice bloquea sus 100 TKA en el contrato al crear la operación; Bob la completa pagando 150 TKB, y en esa misma transacción el contrato entrega los 100 TKA a Bob y los 150 TKB a Alice. O se ejecutan las dos piernas del swap, o no se ejecuta ninguna: atomicidad. Si nadie completa, Alice cancela y recupera sus TKA. Nadie custodia fondos de terceros de forma insegura y nadie puede quedarse a medias con los tokens del otro.
Vista compacta del sistema. El detalle está en docs/ARCHITECTURE.md.
flowchart LR
subgraph Cliente
MM["MetaMask (window.ethereum)"]
UI["Next.js 15 - UI rol-aware"]
end
subgraph Servidor["Next.js API routes (server-side)"]
UP["/api/upload-ipfs"]
TL["/api/timeline"]
end
subgraph Cadena["Anvil - chainId 31337"]
ESC["Escrow.sol"]
OL["OperationLib"]
TKL["TokenLib"]
TT["TestToken TKA/TKB"]
end
PIN["Pinata / IPFS"]
MM <--> UI
UI -->|"lecturas + firma (ethers v6)"| ESC
UI -->|"memo opcional"| UP
UI -->|"timeline"| TL
UP -->|"pinJSONToIPFS (JWT)"| PIN
TL -->|"getLogs / getBlock"| ESC
ESC -. usa .-> OL
ESC -. usa .-> TKL
ESC -->|"safeTransfer / safeTransferFrom"| TT
El proyecto lleva el patrón Checks-Effects-Interactions a la propia separación de ficheros:
- Las librerías (
OperationLib,TokenLib) hacen los EFFECTS: mutan el estado (crear una operación, marcarla completada/cancelada, gestionar el allowlist). No transfieren tokens jamás. - El contrato (
Escrow.sol) hace los CHECKS (validaciones + custom errors) y las INTERACTIONS (transferencias, siempre víaSafeERC20), en ese orden estricto.
Que el estado se mute antes de la llamada externa es lo que hace el swap seguro: cuando el token
devuelve el control (un ERC20 malicioso podría reentrar), la operación ya está marcada como
Completed/Cancelled, así que no se puede volver a ejecutar. ReentrancyGuard es la segunda capa de
defensa sobre esa base. El razonamiento completo, con diagramas, está en
docs/ARCHITECTURE.md.
| Capa | Tecnología |
|---|---|
| Contratos | Solidity 0.8.28 · Foundry (Forge/Anvil/Cast) · OpenZeppelin v5.6.1 |
| Frontend | Next.js 15 (App Router) · TypeScript strict · Tailwind v4 · ethers v6 |
| Off-chain | API routes de Next.js · Pinata (IPFS) · indexer de eventos propio |
| Tests | Foundry (100% en los contratos core) · Playwright E2E (window.ethereum mockeado) |
- Foundry (
forge,anvil,cast). - Node.js 20+ y pnpm vía corepack (
corepack enable pnpm). - MetaMask en el navegador.
Se necesitan tres terminales. Desde la raíz del repo:
1. Arranca Anvil (terminal 1):
anvilLevanta una blockchain local en http://localhost:8545 (chainId 31337) e imprime 10 cuentas de test
con sus claves privadas.
2. Despliega y siembra estado (terminal 2):
./deploy.shEste script: despliega TestToken TKA y TKB + el Escrow, autoriza ambos tokens en el allowlist,
siembra 1000 de cada token a las 3 primeras cuentas de Anvil, y escribe las direcciones (+
DEPLOY_BLOCK, chainId 31337) a web/.env.development.local y a deployment-info.txt. El frontend
lee esas direcciones vía variables NEXT_PUBLIC_* en web/lib/contracts.ts (versionado, con defaults
de Anvil), así que no hay que configurar nada.
3. Arranca el frontend (terminal 3):
cd web
pnpm install
pnpm devAbre http://localhost:3000.
4. Configura MetaMask:
- Añade una red manual: RPC
http://localhost:8545, chainId31337. - Importa una o dos cuentas de test de Anvil (usa las claves privadas que imprimió
anvil) para actuar como creador y contraparte.
Nota:
web/.env.development.locales un artefacto generado y gitignored (web/lib/contracts.tssí está versionado). Como las direcciones de Anvil son deterministas, casi nunca cambian; pero si reinicias Anvil, vuelve a ejecutar./deploy.shpara reescribir ese fichero.
La UI es rol-aware: muestra acciones distintas según quién esté conectado, comparado con el estado on-chain.
- Owner (la cuenta que desplegó) → ve el panel Token Allowlist y puede autorizar más tokens.
- Creador → crea una operación: elige tokens y cantidades, opcionalmente escribe un memo (se sube a
IPFS). El flujo son 2 pasos on-chain:
approvedel token que ofrece ycreateOperation, que bloquea ese token en el Escrow. - Contraparte (cualquiera que no sea el creador) → en una operación activa ajena ve Complete:
approvedel token que paga +completeOperation, que liquida el swap atómicamente. - Creador → en su operación activa ve Cancel: recupera el token bloqueado.
La sección Activity muestra el timeline de eventos con timestamps (alimentada por el indexer).
El despliegue a la testnet pública de Ethereum (Sepolia) usa un script separado del local,
deploy-sepolia.sh: a diferencia del Anvil local, gasta gas real y es
irreversible, así que firma con un keystore cifrado de Foundry (--account, nunca la clave en
claro) y verifica los contratos en Etherscan.
1. Crea un .env en la raíz del repo (gitignored) con estas cinco variables:
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/TU_API_KEY # RPC de Sepolia (p.ej. Alchemy)
ETHERSCAN_API_KEY=TU_ETHERSCAN_API_KEY # para forge --verify
DEPLOYER_ACCOUNT=alebeta-admin # alias del keystore de Foundry
DEPLOYER_ADDRESS=0x... # dirección de ese keystore = OWNER
CLIENT_ADDRESS=0x... # cuenta "cliente demo" (contraparte)DEPLOYER_ACCOUNTes el alias de un keystore ya importado en Foundry (cast wallet import). Esa cuenta será el owner del Escrow (Ownable(msg.sender)) y firma el despliegue; Foundry pedirá su passphrase de forma interactiva.DEPLOYER_ADDRESSdebe ser la dirección pública de ese keystore (verifícala concast wallet address --account <alias>). Se usa como--sendery como primer destinatario del seed.DEPLOYER_ADDRESSyCLIENT_ADDRESSreciben 1000 de cada token (TKA/TKB) para poder ejecutar el swap de la demo. Internamente el script exportaMINT_RECIPIENTS="$DEPLOYER_ADDRESS,$CLIENT_ADDRESS", quescript/Deploy.s.sollee víavm.envOr(sin esa variable, en local, siembra a las 3 cuentas de Anvil).
2. Ejecuta (desde la raíz, con Foundry y el keystore configurados):
./deploy-sepolia.shEl script hace un health-check de que el RPC es Sepolia (chainId 11155111), despliega con --slow
(una tx a la vez, sin huecos de nonce) y --verify (verificación en Etherscan con los constructor args
correctos por contrato). Al terminar genera deployment-info-sepolia.txt (direcciones, deploy block y
enlaces a Etherscan) e imprime las cinco variables NEXT_PUBLIC_* listas para pegar en Vercel.
Si la verificación en Etherscan falla por indexado tardío (el despliegue sí se completó), el propio script documenta en comentarios cómo reverificar por contrato con
forge verify-contract.
Contratos (Foundry):
forge test # 32 tests
forge coverage # 100% en Escrow.sol, OperationLib.sol y TokenLib.solforge coverage reporta 100% de líneas, statements, branches y funciones en los tres contratos core.
(El total agregado es menor solo porque script/Deploy.s.sol no se testea, algo esperado en un script
de despliegue.)
Frontend (Playwright E2E):
cd web
pnpm test:e2e # 6 testsLos tests arrancan su propio Anvil efímero, despliegan contra él y ejercitan los flujos reales
(connect, gate de rol, addToken, create/complete/cancel) firmando transacciones reales. Se mockea
únicamente el popup de la extensión: window.ethereum es un provider EIP-1193 respaldado por una
wallet de ethers con claves de test. Detalle en docs/ARCHITECTURE.md.
⚠️ Gotcha real: no encadenespnpm build && pnpm test:e2e. El.nextde producción colisiona con elnext devque levantan los tests. Ejecútalos por separado.
El frontend usa variables para Pinata (subida de memos, server-side) y el indexer (RPC de logs). Están
documentadas en web/README.md; copia web/.env.example a web/.env.local y
rellénalas. Ninguna es obligatoria para probar el escrow: sin PINATA_JWT simplemente no se suben
memos, y el resto de la app funciona igual.
Escrow-DApp/
├── src/
│ ├── Escrow.sol # Contrato principal: CHECKS + INTERACTIONS
│ ├── EscrowTypes.sol # Tipos compartidos (evita import cíclico lib↔contrato)
│ ├── libraries/
│ │ ├── OperationLib.sol # EFFECTS sobre Operation
│ │ └── TokenLib.sol # EFFECTS sobre el allowlist (TokenSet)
│ └── mocks/
│ └── TestToken.sol # ERC20 de prueba (TKA/TKB)
├── test/
│ ├── Escrow.t.sol
│ ├── OperationLib.t.sol
│ ├── TokenLib.t.sol
│ └── harness/ # Exponen las funciones internal de las libs para testearlas
├── script/
│ └── Deploy.s.sol # Despliegue local (Anvil)
├── deploy.sh # Orquesta el deploy y genera web/lib/contracts.ts
├── web/ # Frontend Next.js 15 (ver web/README.md)
│ ├── app/ # App Router + API routes (upload-ipfs, timeline)
│ ├── components/ # UI rol-aware
│ ├── hooks/ # Lecturas on-chain + timeline
│ ├── lib/ # abis, contracts (generado), ethereum, ipfs, errors
│ └── e2e/ # Playwright (mock EIP-1193)
├── docs/
│ └── ARCHITECTURE.md # Deep-dive con diagramas mermaid
└── .github/workflows/test.yml # CI (forge fmt + build + test)
anvil-local— desarrollo y demo en local contra Anvil (rama histórica).testnet(esta rama) — en producción: desplegada en Ethereum Sepolia (contratos verificados) y servida en Vercel. Hereda toda la documentación anterior y añade el flujo de Despliegue en Sepolia. URLs y contratos en Demo en vivo.