Volver al blog

ProCat Solutions

Desvío por Web3: smart contracts, sistemas de preventa y lecciones

Preventa de tokens con Solidity y OpenZeppelin, whitelist y referidos, gas en Polygon y Arbitrum, backend con Ethers.js, tests, auditoría y lo que perdura.

ProCat Solutions web3solidityopenzeppelinetherspolygonarbitrum
Desvío por Web3: smart contracts, sistemas de preventa y lecciones

En los últimos dos años hemos trabajado en varios proyectos Web3: sistemas de preventa de tokens, lógica de whitelist y referidos, integración de pagos en cripto en redes compatibles con EVM. Esta entrada no trata del blockchain como fenómeno, sino de lo que significa desarrollar y operar smart contracts para un equipo que, por lo demás, construye sistemas backend. Al final también contamos qué nos hemos traído de todo esto a nuestro trabajo “normal”.

Solidity y OpenZeppelin: no escribas el tuyo

La primera y más importante regla que aprendimos en el desarrollo con Solidity: los componentes estándar (ERC-20, ERC-721, control de acceso, protección contra reentrada) no los escribimos nosotros. La librería OpenZeppelin los ofrece auditados y ampliamente usados, y cualquier implementación propia solo añade riesgo.

El código propio empieza donde empieza la lógica de negocio: el contrato de preventa, que define quién, cuándo, a qué precio y cuánto puede comprar. Algunos principios que seguimos ahí:

  • el estado del contrato es mínimo; todo lo que no hace falta para una decisión dentro de la cadena se queda fuera de ella,
  • cada función que modifica estado está ligada a un permiso explícito (Ownable o AccessControl),
  • el contrato se puede pausar (Pausable), porque ante un error es el único freno de emergencia,
  • el movimiento de fondos sigue el patrón “checks-effects-interactions” y está protegido por ReentrancyGuard.

Un contrato desplegado no se puede corregir. Esta frase transforma cada decisión: el manejo de errores, las pruebas y los permisos son más estrictos que en cualquier backend que hubiéramos escrito antes.

Preventa con whitelist y referidos

El sistema de preventa típico tiene tres partes: el contrato, que gestiona la compra; el backend, que mantiene la whitelist y los códigos de referido; y el frontend, en el que el usuario conecta su wallet.

En el caso de la whitelist, la solución ingenua (una lista de direcciones en el contrato) es cara: dar de alta cada dirección es una transacción. En su lugar usamos un árbol de Merkle: el backend genera la raíz del árbol de direcciones autorizadas, solo eso va al contrato, y el usuario envía al comprar una prueba (Merkle proof) que el contrato verifica. Actualizar la whitelist es así una única transacción.

La lógica de referidos la repartimos deliberadamente entre la cadena y el backend. El contrato solo registra como evento qué dirección compró y con qué identificador de referido; el cálculo de comisiones, la comprobación de elegibilidad y la programación de los pagos ocurren en el backend. Así las reglas pueden cambiarse sin redesplegar el contrato, y la cadena no contiene datos que no deberían hacerse públicos.

Polygon, Arbitrum y el gas

Las comisiones de transacción de la red principal de Ethereum son inasumibles para la mayoría de las preventas, así que los proyectos corren en Polygon o Arbitrum. Ambas son compatibles con EVM y el código del contrato se despliega sin cambios, pero la operación difiere:

  • el precio del gas y el tiempo de bloque son distintos, así que la lógica de espera de confirmación de transacciones la configuramos por red,
  • la estructura de comisiones de Arbitrum como L2 incluye también el coste de datos en L1, que depende del tamaño de la transacción; un calldata grande (por ejemplo, una Merkle proof larga) sale aquí más caro de lo que uno pensaría,
  • Polygon produce de vez en cuando congestión de red y bloques reorganizados (reorg), así que el procesamiento de eventos solo da por definitiva una transacción tras varias confirmaciones.

La optimización de gas en el código del contrato vale dinero real: empaquetar variables de almacenamiento, evitar escrituras innecesarias, usar eventos en lugar de almacenamiento. El informe de gas de Hardhat avisa en cada ejecución de tests si algo se ha encarecido.

Backend con Ethers.js y la observación de la cadena

Nuestro backend, en Node.js, habla con la cadena mediante Ethers.js. Tiene dos tareas: enviar transacciones (por ejemplo, actualizar la raíz de la whitelist, pagos) y observar eventos (compras).

La observación de eventos es lo que más dolores de cabeza operativos nos causó. La escucha por websocket es poco fiable, la conexión se corta en silencio. Por eso pasamos a una solución basada en consultas (polling): el backend registra el último bloque procesado y consulta con regularidad los eventos generados desde entonces, quedándose siempre unos bloques por detrás de la cabeza de la cadena por los reorgs. El procesamiento de eventos es idempotente, con el hash de la transacción y el índice del log como clave.

En el envío de transacciones, la gestión del nonce y la sustitución de transacciones “atascadas” exige lógica aparte: una transacción enviada con un precio de gas bajo puede quedar colgada durante horas cuando la red se encarece, y mientras está ahí, el siguiente nonce tampoco pasa.

En cuanto a pruebas y auditoría: los smart contracts los probamos con bastante más rigor que el código backend: tests unitarios bajo Hardhat para cada rama, tests sobre un fork con el estado real de la red, tests de fuzzing para las funciones de cálculo. Antes del despliegue pedimos una auditoría independiente; no es opcional, porque el coste de un error es el dinero de los usuarios.

La auditoría la mayoría de las veces no encuentra un error crítico, sino supuestos que el desarrollador no dejó por escrito: qué pasa si se pierde la clave del propietario, qué ocurre con el saldo que queda en el contrato cuando termina la preventa, quién puede invocar la parada de emergencia.

¿Qué es lo que se puede trasladar?

Web3 se quedó en un desvío para nosotros, no en una dirección principal, pero varias cosas se nos han quedado:

  • la mentalidad de “esto no se puede corregir”: las migraciones, los flujos de pago y las integraciones externas las diseñamos como si no se pudieran deshacer,
  • el procesamiento de eventos idempotente y reproducible se convirtió en el patrón por defecto para todo manejo de webhooks y colas de mensajes,
  • la descripción explícita y las pruebas del modelo de permisos para cada operación que modifica estado,
  • la exigencia de una revisión independiente en las partes críticas del código, incluso cuando no hay una auditoría formal.

Estos patrones también resultan útiles en nuestros siguientes proyectos, los sistemas basados en voz, donde el procesamiento de los eventos de llamada plantea exactamente los mismos problemas.

QR Code