🛡
CULTIVA IA
Servicio de Seguridad Web3
Auditoria de Seguridad Cairo/StarkNet
StarkBridge Protocol — Revision Pre-Lanzamiento
Fecha 16 Jun 2026
Auditores CULTIVA IA Security
Contratos bridge.cairo · token.cairo · vault.cairo
Herramientas Caracal · ripgrep · Starknet Foundry
Riesgo Global
CRITICO
No lanzar en este estado
Resumen de Hallazgos
3
Critico
2
Alto
1
Medio
0
Bajo
🤖 Escaneo automatico ejecutado con caracal detect src/ --detectors all + analisis manual de patrones. TVL en riesgo estimado: ~2,000,000 USD
Vulnerabilidades Detectadas
Critico
CVE-SB-001 — from_address no validado en L1 Handler
📄 src/bridge.cairo · lineas 145–155 · funcion handle_deposit
El handler L1 handle_deposit acepta mensajes de cualquier contrato Ethereum sin validar el campo from_address. Cualquier atacante puede desplegar un contrato L1 malicioso y enviar mensajes al bridge, minteando tokens L2 arbitrarios sin depositar fondos reales.
// bridge.cairo:145 — VULNERABLE
#[l1_handler]
fn handle_deposit(
    ref self: ContractState,
    from_address: felt252,  // ← NO validado!
    user: ContractAddress,
    amount: u256
) {
    let current_balance = self.balances.read(user);
    self.balances.write(user, current_balance + amount); // mint sin control
}
  • 1
    Atacante despliega contrato L1 malicioso en Ethereum mainnet
  • 2
    Llama a starknetCore.sendMessageToL2(l2Bridge, selector, [attacker_l2, 1_000_000_000])
  • 3
    El handler L2 procesa el mensaje sin verificar el remitente L1
  • 4
    Atacante recibe 1B tokens en StarkNet sin depositar nada
  • 5
    Vende tokens en DEX, drena liquidez real del protocolo
#[l1_handler]
fn handle_deposit(
    ref self: ContractState,
    from_address: felt252,
    user: ContractAddress,
    amount: u256
) {
    // Validar remitente L1 autorizado
    let authorized_l1 = self.l1_bridge_address.read();
    assert(from_address == authorized_l1, 'Unauthorized L1 sender');

    let current_balance = self.balances.read(user);
    self.balances.write(user, current_balance + amount);
}
caracal detect src/ --detectors unchecked-l1-handler-from
Critico
CVE-SB-002 — Desbordamiento Aritmetico en felt252 (token.cairo)
📄 src/token.cairo · lineas 78–95 · funciones transfer, mint
Los balances del token se almacenan como felt252. Este tipo no tiene comportamiento de overflow/underflow definido en Cairo 1.0 — las operaciones aritmeticas pueden producir resultados inesperados al aproximarse al limite del campo primo (P = 2^251 + 17*2^192 + 1), permitiendo manipulacion de balances.
// token.cairo:78 — VULNERABLE
#[storage]
struct Storage {
    balances: LegacyMap::<ContractAddress, felt252>,  // ← usar u256
    total_supply: felt252,                              // ← usar u256
}

fn transfer(ref self: ContractState, to: ContractAddress, amount: felt252) {
    let sender_balance = self.balances.read(get_caller_address());
    // Sin verificacion de saldo suficiente — underflow posible
    self.balances.write(get_caller_address(), sender_balance - amount);
    let recipient_balance = self.balances.read(to);
    self.balances.write(to, recipient_balance + amount);
}
  • 1
    Usuario con balance 0 llama a transfer(victim, 1)
  • 2
    felt252 hace 0 - 1 = P - 1 (wrap-around al primo del campo)
  • 3
    Atacante ahora tiene balance de ~3.6 × 10^75 tokens
  • 4
    Protocolo completamente comprometido
#[storage]
struct Storage {
    balances: LegacyMap::<ContractAddress, u256>,  // overflow seguro
    total_supply: u256,                              // overflow seguro
}

fn transfer(ref self: ContractState, to: ContractAddress, amount: u256) {
    let sender_balance = self.balances.read(get_caller_address());
    assert(sender_balance >= amount, 'Insufficient balance');
    self.balances.write(get_caller_address(), sender_balance - amount);
    let recipient_balance = self.balances.read(to);
    self.balances.write(to, recipient_balance + amount);
}
Critico
CVE-SB-003 — Replay de Firmas en Vault (sin nonce)
📄 src/vault.cairo · lineas 201–230 · funcion execute_withdrawal
La funcion execute_withdrawal verifica una firma ECDSA para autorizar retiros de la boveda, pero no implementa tracking de nonces. Una firma valida puede reutilizarse indefinidamente para drenar la boveda.
// vault.cairo:201 — VULNERABLE
fn execute_withdrawal(
    ref self: ContractState,
    amount: u256,
    signature: (felt252, felt252)
) {
    let msg_hash = pedersen_hash(amount.into(), get_caller_address().into());
    // Sin nonce — misma firma sirve infinitamente
    let is_valid = self.owner.read().verify_signature(msg_hash, signature);
    assert(is_valid, 'Invalid signature');
    self.transfer_tokens(get_caller_address(), amount);
}
  • 1
    Owner firma un retiro legitimo de 10,000 USDC para el usuario A
  • 2
    Usuario A (o cualquier observador de mempool) guarda la firma
  • 3
    Llama a execute_withdrawal(10000, signature) repetidamente
  • 4
    Cada llamada es valida — boveda drenada completamente
#[storage]
struct Storage {
    nonces: LegacyMap::<ContractAddress, u64>,  // tracking por usuario
}

fn execute_withdrawal(
    ref self: ContractState,
    amount: u256,
    nonce: u64,
    signature: (felt252, felt252)
) {
    let caller = get_caller_address();
    let current_nonce = self.nonces.read(caller);
    assert(nonce == current_nonce, 'Invalid nonce');
    let msg_hash = pedersen_hash_multi([amount, nonce, caller]);
    let is_valid = self.owner.read().verify_signature(msg_hash, signature);
    assert(is_valid, 'Invalid signature');
    self.nonces.write(caller, current_nonce + 1);  // incrementar ANTES del transfer
    self.transfer_tokens(caller, amount);
}
Alto
CVE-SB-004 — Conversion EthAddress sin validacion de rango
📄 src/bridge.cairo · lineas 88–102 · funcion convert_l1_address
Al convertir direcciones Ethereum (20 bytes) a felt252 para uso en StarkNet, no se valida que el valor resultante sea menor que el primo del campo de StarkNet. Aunque las direcciones ETH de 20 bytes son menores que P por definicion, la funcion acepta felt252 arbitrario como input, permitiendo valores invalidos que causen comportamiento indefinido o fondos enviados a la direccion cero.
  • 1
    Atacante pasa direccion L1 malformada con valor >= P
  • 2
    Conversion produce direccion cero o direccion invalida
  • 3
    Fondos enviados a direccion irrecuperable
Validar assert(addr < STARKNET_FIELD_PRIME, 'Address out of range') antes de cualquier conversion. Usar el tipo EthAddress nativo de StarkNet que enforza el rango automaticamente.
Alto
CVE-SB-005 — Mensajes L1-L2 sin mecanismo de cancelacion
📄 src/bridge.cairo · lineas 312–340 · funcion send_to_l2
Los mensajes enviados de L1 a L2 mediante send_message_to_l2 pueden quedar bloqueados si el handler L2 revierte (gas insuficiente, contrato pausado, etc.). El contrato L1 no implementa cancelL1ToL2Message, dejando los fondos atrapados permanentemente en el core de StarkNet.
  • 1
    Usuario deposita 50,000 USDC en el bridge L1
  • 2
    El contrato L2 esta pausado por mantenimiento o bug
  • 3
    El mensaje L1→L2 no puede procesarse
  • 4
    Sin cancelL1ToL2Message, los fondos quedan bloqueados indefinidamente
Implementar en el contrato L1 la funcion cancelDeposit() que llame a starknetCore.cancelL1ToL2Message() despues del periodo de espera (5 dias). Almacenar el hash del mensaje en storage para poder cancelarlo.
Medio
CVE-SB-006 — Overconstrained: L2 no acepta mensajes L1 validos
📄 src/bridge.cairo · lineas 155–170 · validacion en receive_l1_message
La funcion receive_l1_message impone una restriccion demasiado estricta: compara la direccion L1 del remitente con un hash hardcodeado en lugar del valor almacenado en storage. Si se actualiza la direccion del bridge L1 (por upgrade), todos los mensajes comenzaran a rechazarse, bloqueando deposits y retiros.
  • 1
    El contrato L1 se actualiza por un bug de seguridad
  • 2
    La nueva direccion L1 no coincide con el hash hardcodeado en L2
  • 3
    Todos los depositos desde L1 son rechazados en L2
  • 4
    Bridge completamente inutilizable hasta nuevo upgrade L2
Leer la direccion autorizada desde storage (self.l1_bridge.read()) y exponer una funcion set_l1_bridge() protegida por owner para actualizarla sin necesidad de redeploy del contrato L2.
Checklist de Cierre de Auditoria
Seguridad Aritmetica
No se usa felt252 para balances/cantidades (usar u128/u256)
Aritmetica felt252 tiene verificacion explicita de limites
Escenarios de overflow/underflow testeados
Seguridad de Handlers L1 (CRITICO)
TODOS los #[l1_handler] validan from_address
from_address comparado contra direccion L1 en storage
No es posible bypass desplegando contrato L1 alterno
Mensajeria L1-L2
L1 bridge valida direcciones < STARKNET_FIELD_PRIME
L1 bridge implementa cancelacion de mensajes
Handlers L2 verifican from_address
Reglas de validacion simetricas L1 ↔ L2
Seguridad de Firmas
Firmas incluyen tracking de nonce
Nonce incrementado despues de cada uso
Domain separator incluye chain ID y direccion del contrato
Replay de firmas testeado y prevenido
Herramientas
Escaneo Caracal completado (findings criticos pendientes)
Tests unitarios cubren todos los escenarios de vulnerabilidad
Tests de integracion verifican flujos L1-L2 completos
Despliegue en testnet completado antes de mainnet
⚠ Recomendacion: NO Desplegar en Mainnet
Con 3 vulnerabilidades criticas activas — incluyendo un vector de mint infinito y replay de firmas — el despliegue en mainnet resultaria en la perdida total de los ~2M USD de TVL planificados en horas o dias. Se recomienda:

1. Corregir los 3 hallazgos criticos (CVE-SB-001, 002, 003) · 2. Re-auditoria parcial de los componentes modificados · 3. Despliegue en testnet con suite de tests completa · 4. Auditoria final antes de mainnet