Auditoría de Seguridad del Protocolo
Análisis de seguridad del Escrow Inteligente de P2PMoney.xyz — protocolo descentralizado para el intercambio de stablecoins por dinero fiat, sin intermediarios que custodien tus fondos. Verificado contra los contratos reales desplegados.
P2PMoney Protocol · P2PMoney.xyz · Documento vigente · Junio 2026
1. Descripción del Protocolo
P2PMoney es un protocolo descentralizado de intercambio entre personas (P2P) que opera sobre un Escrow Inteligente. Permite a cualquier persona comprar y vender stablecoins (como USDC, USDT o COPm) contra dinero fiat sin depender de ningún intermediario centralizado. El protocolo está desplegado en varias redes blockchain (ver Especificaciones Técnicas) y funciona de forma idéntica en todas ellas.
El Escrow Inteligente actúa como intermediario neutral. Custodia los tokens del vendedor bajo condiciones contractuales y los libera únicamente cuando el comprador confirma el pago. Nadie — ni el operador, ni los desarrolladores — puede alterar este proceso unilateralmente.
El mecanismo es análogo al de una custodia notarial, pero ejecutado automáticamente por código:
| Custodia notarial tradicional | Escrow Inteligente |
|---|---|
| El notario guarda el dinero | El contrato custodia los tokens |
| El dinero se libera al firmar | Los tokens se liberan al confirmar el pago |
| El notario actúa bajo reglas escritas | El código ejecuta las reglas automáticamente, 24/7 |
| El notario no puede apropiarse del dinero | Ninguna función del contrato permite extraer fondos arbitrariamente |
2. Activos Soportados
El Escrow Inteligente opera exclusivamente con stablecoins aprobadas. El conjunto exacto depende de la red (ver Especificaciones Técnicas); los principales son:
| Activo | Descripción | Valor de referencia |
|---|---|---|
| USDC | Dólar digital emitido por Circle, entidad regulada en Estados Unidos | 1 USDC ≈ 1 USD |
| USDT | Dólar digital emitido por Tether Limited | 1 USDT ≈ 1 USD |
| COPm | Peso colombiano digital del protocolo Mento, nativo de Celo | 1 COPm ≈ 1 COP |
3. Arquitectura de Participantes
4. Modelo de Órdenes y Operaciones
Dos conceptos clave: Orden vs Deal
Para entender el protocolo hay que distinguir dos cosas. La Orden es el anuncio publicado en el marketplace (una sola persona: quien publica). El Deal es la operación concreta que nace cuando alguien toma esa orden (dos personas: comprador y vendedor). La orden es el aviso; el deal es el apretón de manos.
Orden de Venta
El vendedor publica un anuncio indicando la cantidad de tokens disponibles y las condiciones de la operación. Los tokens quedan bajo custodia del contrato desde el momento de la publicación.
Orden de Compra
El comprador publica un anuncio indicando la cantidad que desea adquirir. Los tokens entran al contrato únicamente cuando un vendedor acepta la orden.
¿Quién deposita los tokens?
En ambos tipos de orden el resultado es el mismo: el vendedor entrega tokens y el comprador paga fiat. Solo cambia cuándo entran los tokens al escrow:
| Orden de Venta | Orden de Compra | |
|---|---|---|
| Quién publica | El vendedor | El comprador |
| Quién deposita los tokens | El que publica, al crear la orden | El que la acepta (vendedor), al aceptarla |
| Quién la toma | El comprador | El vendedor |
Ciclo de vida completo de una operación
Esta es la visión general del recorrido de una operación, de principio a fin:
5. Flujos de Ejecución
Flujo A — Orden de Venta
Flujo B — Orden de Compra
6. Resolución de Disputas
Si surge un desacuerdo — por ejemplo, el comprador afirma haber pagado pero el vendedor no lo reconoce — cualquiera de las dos partes puede abrir una disputa. Mientras la disputa está activa, los tokens permanecen custodiados por el contrato: ninguna de las partes puede retirarlos hasta que se resuelva.
¿Quién resuelve y cómo?
Las disputas las resuelve un árbitro independiente cuya wallet es diferente e independiente del operador del protocolo. El contrato impide explícitamente que el operador se asigne a sí mismo como árbitro (ArbiterCannotBeOwner). La resolución sigue un proceso de tres pasos obligatorios que el contrato hace cumplir automáticamente:
| Paso | Función | Qué ocurre |
|---|---|---|
| 1 | commitEvidence | El árbitro registra el hash del expediente (keccak256 del chat de la operación) en blockchain. Sin este paso no puede resolver. |
| 2 | Ventana de espera | Debe transcurrir el tiempo mínimo de evidencia (configurable, nunca inferior a 15 minutos) entre el commit y la resolución. |
| 3 | resolveDispute | El árbitro dirige los tokens al comprador o los devuelve al vendedor. Solo ejecutable tras la ventana mínima. |
Resultados posibles
| Resolución | Qué ocurre con los tokens |
|---|---|
| A favor del comprador | Los tokens se envían al comprador (menos la comisión). La operación se marca como completada. |
| A favor del vendedor | Los tokens vuelven al vendedor. La orden queda cancelada permanentemente — el maker debe crear una nueva orden si desea continuar. |
7. Máquina de Estados
Estados de una Orden
| Estado | Descripción |
|---|---|
| Abierta | La orden está visible en el marketplace. Cualquier usuario puede aceptarla. |
| Aceptada | Un usuario tomó la orden. El intercambio está en curso. |
| Completada | El intercambio finalizó exitosamente. Estado permanente e inmutable. |
| Cancelada | La orden fue cancelada. Estado permanente — aplica tanto a cancelaciones directas como a resoluciones de disputa. |
Estados de un Deal
8. Estructura de Comisiones
| Tipo | Quién paga | Porcentaje |
|---|---|---|
| Comisión del Maker (publicador) | Quien crea la orden | 0% — Gratuito |
| Comisión del Taker (aceptante) | Quien acepta la orden | 0.20% (20 bps) |
Ejemplo con 100 USDC
Comisión (0.20%): −0.20 USDC
Comprador recibe: 99.80 USDC
Comisión destinada al treasury del protocolo (Safe Multisig)
Inmutabilidad de la comisión por deal
La comisión aplicable a cada operación queda fijada contractualmente en el momento en que se acepta la orden. Ningún cambio posterior de parámetros puede afectar deals ya en curso.
9. Control Administrativo
Separación de roles
El protocolo separa estrictamente dos roles que el contrato impide que recaigan en la misma wallet: el Owner gestiona la configuración del protocolo, y el Árbitro resuelve disputas. Esta separación elimina el conflicto de intereses que existiría si quien opera el protocolo también decide quién gana las disputas.
Frenos de emergencia (dos niveles)
El Owner dispone de dos pausas independientes, pensadas como freno de emergencia y gobernadas por el multisig:
| Freno | Qué hace | Para qué sirve |
|---|---|---|
| Pausa suave | Detiene la creación de nuevas órdenes y operaciones; las operaciones en curso pueden completarse y todos pueden retirar sus fondos | Contener riesgo / frenar spam sin atrapar a nadie |
| Pausa de retiros | Congela todas las salidas de tokens (incluidas las legítimas, que quedan congeladas, no perdidas) | Detener un ataque en curso; se reactiva con una sola transacción |
Límites operativos
El protocolo impone límites configurables para prevenir abuso y spam en el marketplace:
| Límite | Valor actual | Propósito |
|---|---|---|
| Monto mínimo por orden | 5 USDC (configurable) | Impide órdenes de importes irrisorios que saturarían el marketplace |
| Máximo de órdenes abiertas por maker | 5 (configurable) | Previene que una wallet bloquee liquidez con decenas de órdenes sin intención de operar |
| Ventana mínima de evidencia | 1 hora · mínimo absoluto: 15 min | Tiempo entre commitEvidence y resolveDispute; impide resoluciones instantáneas sin revisión |
| Preaviso para cambio de árbitro | 1 hora · mínimo absoluto: 1 hora | Timelock on-chain para que los usuarios vean el cambio propuesto antes de que aplique |
10. Flujo de Fondos
Cómo se mueven los fondos en cada escenario:
✅ Operación exitosa (orden de venta)
↩️ Cancelación
Si el comprador no paga a tiempo, el deal se cancela y los fondos vuelven a su dueño:
⚠️ Disputa
Si hay desacuerdo, el árbitro independiente decide a quién van los tokens tras verificar la evidencia on-chain:
11. Seguridad de los Fondos
La pregunta más importante de cualquier custodia: ¿puede alguien retirar fondos que no le pertenecen? La respuesta es no. Solo existen cinco funciones capaces de sacar tokens del contrato, y cada una verifica que quien la llama sea la persona correcta.
Las únicas 5 salidas de fondos
| Acción | Quién puede | A dónde van los tokens |
|---|---|---|
| Liberar tokens | Solo el vendedor del deal | Al comprador (menos la comisión) |
| Cancelar deal | Comprador o vendedor del deal | Devuelve el depósito a su dueño |
| Cancelar orden | Solo quien la creó | Devuelve el depósito a su dueño |
| Resolver disputa | Solo el árbitro | Al comprador o al vendedor de ese deal |
| Retirar comisiones | Solo el owner | A la tesorería (fija) |
¿Cómo decide el contrato si dejarte sacar tokens?
¿Y si un atacante lo intenta sin haber participado?
No tiene ningún camino: todas las salidas le serían rechazadas por control de acceso.
¿Podría crear órdenes falsas para robar?
Tampoco. Crear órdenes solo compromete sus propios tokens, y no puede operar consigo mismo.
Modelo de riesgo
12. Análisis Técnico de Seguridad
Esta sección resume las técnicas de seguridad del código que protegen contra los ataques más comunes a smart contracts.
Control de acceso — quién puede llamar cada función
| Función | Quién puede llamarla |
|---|---|
| Pausar, comisiones, tesorería, tokens, retirar comisiones, configurar parámetros | Solo el owner (Safe Multisig) |
| commitEvidence, resolveDispute | Solo el árbitro |
| proposeArbiter, applyArbiter | Solo el owner (con timelock entre propuesta y aplicación) |
| Confirmar pago | Solo el comprador del deal |
| Liberar tokens | Solo el vendedor del deal |
| Cancelar deal | Comprador o vendedor del deal |
| Abrir disputa | Comprador o vendedor del deal |
| Cancelar disputa | Solo quien la abrió |
| Cancelar orden | Solo quien la creó |
| Cancelar orden expirada | Cualquiera (no mueve fondos) |
| Calificar contraparte | Comprador o vendedor del deal |
Manejo de fondos y solvencia
Todas las transferencias usan SafeERC20, la librería estándar de OpenZeppelin que maneja correctamente cualquier token. Los tokens entran al escrow al crear una orden de venta o al aceptar una de compra, y salen solo por las 5 vías analizadas en la sección 11. Seguridad de los Fondos.
Patrón CEI (Checks-Effects-Interactions)
Toda función que mueve fondos sigue un orden estricto de tres pasos. Transferir los tokens es siempre el último paso:
Protección contra reentrancy
Además del patrón CEI, todas las funciones que mueven fondos llevan un "candado" (nonReentrant) que impide que se ejecuten de forma recursiva. Es una doble protección contra el tipo de ataque que ha drenado a otros protocolos DeFi.
Aritmética segura
El contrato usa Solidity 0.8+, que revierte automáticamente ante cualquier desbordamiento numérico. Las comisiones tienen un techo del 1% por lado, y el cálculo del monto a entregar nunca puede dar un resultado negativo ni mayor que lo depositado.
13. Hallazgos de Seguridad
Sobre la auditoría
Una auditoría de smart contracts es un análisis sistemático del código fuente para identificar vulnerabilidades, comportamientos no intencionados y riesgos de diseño. El análisis cubre el control de acceso, los flujos de fondos, la aritmética, los patrones de reentrancy y la lógica de estados.
Resumen de Hallazgos
| Nivel | Cantidad | Descripción |
|---|---|---|
| 🔴 Crítico | 0 | Vulnerabilidades que permitirían robo o pérdida irreversible de fondos |
| 🟠 Alto | 0 | Riesgos de configuración o control de acceso |
| 🟡 Medio | 0 | Pérdidas potenciales en escenarios específicos |
| 🟢 Bajo | 0 | Impacto limitado o poco probable |
| ℹ️ Informativo | 5 | Observaciones de diseño sin riesgo para los fondos de los usuarios |
El control del Escrow Inteligente está protegido por un Safe Multisig 2-de-3 desplegado en Celo Mainnet. Cualquier acción administrativa — retirar comisiones, pausar el contrato, configurar parámetros — requiere la aprobación independiente de los tres firmantes. Ninguna persona individual puede tomar decisiones unilaterales sobre el protocolo.
Dirección del Safe Multisig:
0x8C2A86F1253a14961dCaA1eb82b8bd57bcfc9DC3 · Celo Mainnet · 2 de 3 firmantesRiesgo identificado: Un actor malicioso podía aceptar una orden de venta, abrir una disputa, y cuando la disputa se resolvía a favor del vendedor, la orden se reabría automáticamente en el marketplace. El atacante podía aceptarla de inmediato nuevamente, repitiendo el ciclo indefinidamente. El resultado: el capital del maker quedaba bloqueado sin posibilidad de cancelar la orden ni recuperar los fondos.
Solución implementada: Tanto cancelDeal como resolveDispute(favorBuyer=false) ahora marcan la orden como Cancelada permanentemente. Los tokens se devuelven al depositante de inmediato. Si el maker desea continuar operando, crea una nueva orden. El contrato elimina la ruta de re-aceptación.
Riesgo original: El operador del protocolo podía resolver disputas arbitrariamente a su favor — incluso actuando como comprador mediante una wallet secundaria — sin que existiera ningún mecanismo de rendición de cuentas. El poder de resolución era completamente unilateral.
Solución implementada: La resolución de disputas se transfirió a un rol arbiter separado e independiente del owner. El contrato rechaza explícitamente que el owner sea el árbitro (ArbiterCannotBeOwner).
El flujo de resolución tiene 3 capas de protección:
- Rol separado: Solo el árbitro puede llamar resolveDispute. El owner solo administra el protocolo.
- Evidencia comprometida on-chain: El árbitro debe llamar primero a commitEvidence(dealId, keccak256(chatJSON)). El hash queda registrado en blockchain antes de la resolución.
- Ventana mínima configurable: Tras commitEvidence, debe esperarse la ventana mínima (configurable, con un floor de 15 minutos que el contrato impone) antes de resolver. Impide resoluciones instantáneas sin revisión.
El cambio de árbitro también está protegido: proposeArbiter inicia un timelock configurable (mínimo absoluto 1 hora) visible on-chain, bloqueado si hay disputas activas (ArbiterLockedByActiveDisputes). Los usuarios pueden ver el cambio propuesto en /explorar y cerrar sus operaciones antes de que aplique.
Árbitro activo en Celo Mainnet:
0x9DCaBb8B928dCe75D8EAb0a015012a5D562404C0 · Celo MainnetEl campo de razón al abrir una disputa ahora tiene un límite máximo (500 bytes), validado por el contrato. Esto evita entradas de texto de tamaño arbitrario que incrementarían el costo de gas. La interfaz muestra un contador y un mensaje claro si se supera el límite.
El contrato almacena el historial completo de órdenes y deals por usuario sin límite. Para el horizonte temporal actual no existe problema de rendimiento. La aplicación consulta el historial mediante eventos de blockchain sin depender de funciones on-chain.
Si el operador deshabilita un token, las órdenes abiertas previamente pueden seguir siendo aceptadas. Las nuevas órdenes de ese token ya no pueden crearse. Este comportamiento es intencional: protege a vendedores que tienen fondos bloqueados en órdenes abiertas al momento de la deshabilitación.
Cuando el árbitro resuelve una disputa a favor del vendedor, el deal se cancela y no se incrementa el contador de deals completados. Solo contabilizan las operaciones finalizadas exitosamente. Decisión de diseño intencional del sistema de reputación.
En algunas funciones, el evento de notificación se emite antes de que se actualice un contador interno. No existe riesgo de reentrancy — el contrato aplica el patrón CEI en todas las rutas críticas. Observación de estilo para futuras iteraciones.
14. Mecanismos de Protección
El Escrow Inteligente implementa las siguientes protecciones de seguridad de forma nativa:
✅ Árbitro independiente del operador
Las disputas las resuelve un árbitro cuya wallet es diferente e independiente del owner del contrato. El contrato rechaza explícitamente (ArbiterCannotBeOwner) que el operador se asigne a sí mismo como árbitro. Cualquier cambio de árbitro requiere un preaviso on-chain configurable (mínimo 1 hora) visible públicamente, y está bloqueado mientras haya disputas activas.
✅ Evidencia comprometida on-chain antes de resolver
El árbitro debe registrar el hash del expediente (keccak256 del chat de disputa) en blockchain antes de poder resolver. Después de commitEvidence, existe una ventana mínima configurable (con un floor de seguridad de 15 minutos que el contrato impone y no puede reducirse) antes de que resolveDispute pueda ejecutarse. Cualquier persona puede verificar qué evidencia respaldó cada decisión.
✅ Cancelación permanente ante resolución de disputa
Cuando el árbitro resuelve a favor del vendedor, la orden queda cancelada permanentemente — no reabre en el marketplace. Esto elimina el vector de ataque donde un bot podía aceptar la misma orden repetidamente tras cada resolución, bloqueando el capital del maker indefinidamente.
✅ Límites anti-flood configurables
El contrato impone un monto mínimo por orden (5 USDC por defecto) y un máximo de órdenes abiertas por maker (5 por defecto). Ambos límites son configurables por el owner para adaptarse a las condiciones del mercado. Previenen que una sola wallet sature el marketplace con decenas de micrórdenes o anuncios fantasma.
✅ Ventana de evidencia con floor de seguridad
La ventana mínima entre commitEvidence y resolveDispute es configurable por el owner, pero el contrato impone un floor absoluto de 15 minutos que nunca puede reducirse. Los cambios quedan registrados on-chain vía evento MinEvidenceWindowUpdated, garantizando transparencia total.
✅ Comisión fijada al momento de aceptar
La comisión aplicable a cada deal queda registrada contractualmente en el momento exacto en que se acepta la orden. Cualquier modificación posterior de los parámetros de comisión por el operador únicamente afecta a las nuevas operaciones, nunca a las que ya están en curso.
✅ Límites de longitud en campos de texto
Los campos de texto tienen límites máximos: método de pago (100 bytes) e instrucciones de pago (600 bytes). Esto previene abusos mediante entradas de texto de tamaño arbitrario que incrementarían el costo de gas de forma maliciosa.
✅ Gestión de tokens sin duplicados
El contrato utiliza un algoritmo eficiente de tipo swap-and-pop para mantener la lista de tokens habilitados. Garantiza que no pueden existir entradas duplicadas y que las operaciones de habilitación y deshabilitación siempre resultan en un estado consistente.
✅ Verificación de solvencia en tiempo constante
La función isSolvent() verifica instantáneamente que el balance del contrato cubre la totalidad de los fondos comprometidos en deals activos. Opera en tiempo constante O(1) gracias al contador _totalLocked, independientemente del número de operaciones históricas.
✅ Validación de identificadores
Todas las funciones del contrato rechazan explícitamente identificadores con valor cero (ID 0). Esto elimina un vector de comportamiento indefinido donde llamadas con parámetros inválidos podrían ejecutarse contra la orden o deal equivocado.
✅ Expiración de órdenes de compra
Las órdenes de compra abiertas tienen un tiempo máximo de vigencia configurable por el operador. Una vez expiradas no pueden aceptarse, y cualquier persona puede retirarlas del marketplace. Esto evita que el marketplace se sature con anuncios abandonados, sin que ningún fondo quede en riesgo.
✅ Freno de emergencia de dos niveles
El protocolo cuenta con dos pausas independientes: una suave que detiene la entrada de nuevas operaciones permitiendo que todos retiren sus fondos, y una de retiros que congela todas las salidas de tokens para detener un eventual ataque. Ambas son reversibles, quedan registradas en blockchain y requieren las firmas del multisig.
✅ Consistencia entre operaciones y métricas
Toda operación que se completa emite un registro verificable en blockchain, de modo que las métricas públicas del protocolo (volumen, operaciones exitosas) reflejan fielmente lo ocurrido on-chain, incluidas las operaciones que se cierran mediante la resolución de una disputa a favor del comprador.
✅ Autenticación por wallet (SIWE)
El acceso a todos los datos privados (chat, notificaciones, comprobantes) requiere una firma criptográfica de la wallet. Sin sesión válida, la base de datos devuelve cero filas en todas las tablas con información personal — incluso conociendo la clave pública de lectura.
✅ Comprobantes de pago en almacenamiento privado
Los archivos enviados en el chat (comprobantes bancarios, capturas) se almacenan en un bucket privado. Solo son accesibles mediante una URL firmada de 2 minutos de duración, generada por el servidor tras verificar que el solicitante es participante del trade.
15. Seguridad de Datos e Identidad
Arquitectura de autenticación
El flujo combina el estándar EIP‑4361 (SIWE) con JWT firmados con el mismo secreto que usa Supabase internamente, de modo que las políticas de Row Level Security (RLS) pueden leer el claim wallet del token y aplicar reglas por dirección.
Modelo de acceso por tabla
| Tabla | Lectura | Escritura |
|---|---|---|
| messages | Solo sender o receiver | Solo sender |
| dispute_messages | Partes del trade + árbitro | Partes del trade + árbitro |
| notifications | Solo el destinatario | Solo servidor (service role) |
| push_subscriptions | Solo el dueño | Solo el dueño |
| orders, ratings | Público | Solo wallet propia |
| dispute_resolutions, chain_config | Público | Solo árbitro / admin |
| event_cache | Solo servidor | Solo servidor |
Comprobantes de pago privados
Separación de claves
| Clave | Uso | Acceso |
|---|---|---|
| anon key | Cliente web (pública, embebida en el frontend) | Solo lo que RLS permite según el JWT |
| service role key | Servidor (rutas API, cron jobs) | Bypassa RLS — nunca expuesta al cliente |
16. Especificaciones Técnicas
El protocolo está desplegado en las siguientes redes. Cada contrato es público, verificable e inmutable. Despliega cada red para ver sus detalles: