Una blockchain es una máquina que no puede sorprenderse a sí misma. Cada nodo tiene que llegar al mismo estado desde los mismos inputs. Esa propiedad es exactamente lo que hace difícil la aleatoriedad onchain: nada dentro de un sistema determinista puede producir un output que, en principio, no sea predecible.
Este es el segundo hito de nuestra serie de producción con Stylus. La Parte 1 preguntaba cómo coordinar múltiples fuentes de liquidez bajo condiciones reales de mercado. Esta vez la pregunta es distinta: ¿cómo construir una aplicación onchain donde el resultado tiene que ser demostrablemente aleatorio, verificable por cualquiera y determinado por completo por infraestructura en la que el usuario no tiene que confiar?
Ver el patrón, no el producto
La superficie es un juego de azar. El problema de ingeniería es un oráculo asíncrono que no puede romper la solvencia, dejar fondos varados ni dejar que alguien influya el resultado. Este recorrido es el flujo en vivo: pedir, esperar, resolver o recuperar.
Referencia open source
Contratos Stylus, integración VRF, paths de Permit2 y el runbook de deploy. Para estudiar el patrón, hacer fork y extenderlo.
- Rust
- Arbitrum Stylus
- Chainlink VRF v2.5
- Open source
Recorrido en vivo
End-to-end contra el deploy real en Sepolia: placement, callback de VRF y un reembolso realmente inducido. No es un mock.
- Sepolia
- VRF v2.5
- E2E
Por qué este problema, después de DEX aggregation
Un DEX aggregator falla en voz alta. Las quotes fallan, las rutas revierten, el usuario vuelve a intentar. Una aplicación que depende de una respuesta aleatoria externa no puede tolerar un resultado ambiguo, demorado para siempre o influenciable por una sola parte, incluidos nosotros.
Publicamos esto como arquitectura de referencia, y después un equipo separado lanzó un producto en vivo encima. Ese deploy no es nuestro, así que no lo nombramos ni lo linkeamos. Lo que importa acá es más fuerte que un demo de testnet: el patrón procesó requests reales y callbacks reales de VRF en condiciones live.
Implementación de referencia, no un producto. Este post describe una arquitectura open source que WakeUp Labs publicó para el ecosistema Stylus. WakeUp Labs no opera ninguna aplicación de consumidor construida sobre ella.
Arquitectura: cuatro contratos, un solo lugar donde puede sentarse el dinero
Si te queda un diagrama, que sea este: todo lo que puede hold tokens vive en un solo contrato. Todo lo demás es reemplazable.
User / Mini App
|
v
[ periphery ] -- router sin estado, Permit2, vistas de lectura
|
v
[ core ] -- UNICA custodia. Lifecycle. VRF. Payouts. Reembolsos.
| \
| \--> Chainlink VRF v2.5 (via trait RandomnessSource)
v
[ play token ] + [ faucet ] -- sin redención a nada de valor realcoreEl monolito. Custodia cada token que entra al sistema y es dueño de todo el lifecycle: placement, request de VRF, resolución del callback, payout, reembolso, accounting de fees. Chainlink VRF v2.5 vive detrás de un trait RandomnessSource, así que cambiar el provider después toca un solo bloque impl, nunca la lógica que consume la aleatoriedad.
peripherySin estado a propósito. Sin counters, sin estado trackeado por el owner, nunca hold un token. Mete fondos via Permit2, los forwardea al core como router de confianza y recomputa vistas de solo lectura (pool disponible, stake máximo, elegibilidad de reembolso) para que el frontend tenga un solo ABI. La solvencia se aplica en el core en cada path de entrada. El periphery no es un security boundary. Se puede rotar o matar sin tocar la custodia.
play tokenUn ERC-20 mínimo hecho a mano. Todos los stakes se denominan en él. No tiene path de redención a nada de valor real.
faucetEl onramp: un mint fijo por wallet por ventana de cooldown. Se mantiene habilitado en todas las redes, mainnet incluida, porque el asset es play money por diseño.
Regla de diseño: si un contrato puede hold funds, es core. Si no puede, se le permite ser conveniente, tonto y descartable.
La parte difícil es el gap
Un número aleatorio síncrono es una contradicción onchain. El core tiene que aceptar un stake, pedirle entropía a un proceso externo y resolver recién cuando ese proceso hace callback. Eso puede tardar varios bloques. En el peor caso, nunca pasa.
El software interesante no es la llamada al oráculo. Es todo lo que tiene que seguir siendo verdadero en el silencio entre el request y la respuesta.
t0 place stake
lock del payout worst-case en el pool [la solvencia se decide aca]
request VRF
status = Pending
... pasan bloques. tal vez muchos. tal vez nunca. ...
t1a llega el callback --> resolve, libera liability, payout o no
t1b deadline + buffer --> cualquiera reembolsa, stake completo, sin fee
t1c late / unknown --> no-op + event, nunca revertBloquear la pesadilla antes de que exista la respuesta
Cuando se coloca un stake, el core hace de inmediato locked_liability += win_payout(amount). Esa única reserva es lo que mantiene el sistema solvente bajo cualquier intercalado de requests in-flight: el pool nunca puede prometer más de lo que puede pagar, no importa cuántos requests estén pending ni cuánto tarde VRF.
Si se espera a lockear hasta el callback, se está apostando a que el futuro sea amable. Los sistemas de producción no pueden asumir eso.
Elegir un fallo. Ver qué hace el contrato.
Estos son los casos para los que realmente diseñamos. Esta es la parte interactiva de la arquitectura: la state machine tiene que ser aburridamente correcta en cada uno.
1. El oráculo nunca responde
Cada request recibe un fulfill_deadline al placement. Si VRF nunca hace callback, el request se vuelve reembolsable cuando now >= fulfill_deadline + DEAD_BUFFER. El buffer es de cinco minutos fijos y no es tunable por el owner. Eso es deliberado: una perilla que un admin pudiera acortar o extender dejaría correr la ventana de reembolso contra un callback tardío.
Cualquiera, no solo el usuario original, puede disparar el reembolso. El stake completo vuelve sin fee, porque el juego nunca se resolvió.
2. El callback llega cuando el request ya murió
Un callback para un request expirado, ya resuelto o desconocido es un no-op más un event. No hace revert.
Un revert acá quemaría el request de Chainlink sin saldar al usuario, dejando la posición varada entre "todavía no es reembolsable" y "nunca se va a resolver." Tratar input adversarial o tardío como no-op es cómo se mantiene la state machine imposible de trabar.
3. Falla el transfer del payout
La resolución sigue checks-effects-interactions estricto: cada cambio de storage (status, release de liability, accounting de fees) ocurre antes de cualquier llamada externa de token.
Un payout ganador se envía via transfer directo. Si ese transfer falla, por ejemplo un token pausado o mal comportado del lado que recibe, el payout cae a un crédito claimable de pull-payment en vez de revertir toda la resolución. El request queda settled. El usuario puede claimear. El oráculo no queda griefed en un loop de retry.
4. Hay muchos requests in flight a la vez
Por eso el liability se lockea en el placement, no en el callback. Diez requests pending son diez payouts worst-case reservados. El pool disponible se achica en tiempo real. Los stakes nuevos que sobre-prometerían se rechazan. El timing no puede crear insolvencia, porque la insolvencia se volvió irrepresentable.
5. Alguien trata al callback como un amigo de confianza
rawFulfillRandomWords lo llama un contrato externo. El wrapper es trusted; el input igual se trata como adversarial. El ordering, los no-ops y el fallback a pull-payment existen porque "el oráculo es honesto" no es un threat model completo. Los oráculos honestos igual llegan tarde, duplican, o pegan contra un token que se niega a recibir.
Cómo entran los fondos: dos paths de Permit2
El periphery soporta dos formas de meter tokens en un request, más un fallback clásico.
| Path | Cómo se siente | Por qué existe |
|---|---|---|
Mini App SignatureTransfer | Un permit EIP-712 de un solo uso por request. Sin approval previa. | Las firmas pasan por Permit2 sin modificar, así que las smart accounts ERC-1271 funcionan igual que las EOAs. |
Standalone AllowanceTransfer | Un approve(Permit2, MAX) de por vida, después un permit semanal. | Cada request dentro de esa ventana necesita cero firmas extra. |
Classic approve + call | El path aburrido. | Fallback cuando ninguno de los flujos de permit está disponible. |
Entrar en el WASM gate
Arbitrum aplica un límite de 24KB de WASM comprimido por fragmento de contrato. No es una constraint blanda. El core necesitó un build nightly de Rust pineado con -Zbuild-std y -Cpanic=immediate-abort para bajar a 23.7KB. Lo suficientemente cerca del techo como para que el toolchain stable de stock no fuera una opción. El periphery, a 18.9KB, entra cómodo en la imagen stable.
24.0 KB techo, duro
23.7 KB core en nightly pineado + immediate-abort
18.9 KB periphery en stable
||||||||||||||||||||||||| core
|||||||||||||||| peripheryEsto no tiene un equivalente directo en el desarrollo con Solidity, y forma decisiones reales: qué panics se pueden permitir, cuánta lógica vive en un contrato versus repartida en varios, y qué receta de build hay que comprometerse a publicar y re-verificar.
Lo que se puede observar de verdad
Los cuatro contratos están deployed y verificados en Arbiscan en Arbitrum Sepolia a través del build reproducible con Docker de cargo-stylus, no un atajo --no-verify. El build nightly custom del core se verifica contra una imagen publicada pineada por digest, así cualquiera puede confirmar que el bytecode deployed matchea el source.
Una suite end-to-end en vivo ejercita el deploy real: el path standalone, el path Permit2 de Mini App, el path AllowanceTransfer y un reembolso realmente inducido. Todo es open source.
Las tres propiedades que generalizan
1. Lockear el outcome worst-case antes de que exista la respuesta externa, para que la solvencia nunca dependa del timing.
2. Publicar un path de recovery determinista y permissionless para cuando el proceso externo nunca responde.
3. Manejar el callback para que no se pueda griefear, double-spend o dejar varado, sin importar el estado en el que llega.
La superficie específica acá es un juego de azar. El patrón no. Un oráculo de aleatoriedad, un mensaje de bridge, un price feed offchain, un callback de settlement cross-chain: cualquiera de esos es la misma forma de problema. Construirlo en Stylus nos dejó expresar la state machine, el accounting de locked-liability y la abstracción del oráculo basada en traits en Rust, con el límite de tamaño WASM forzando tradeoffs reales en vez de teóricos.
Qué sigue
Este es el segundo hito de una serie open source que valida Stylus con implementaciones de referencia de producción. La Parte 1 cubrió DEX aggregation. Cada hito responde una pregunta de ingeniería distinta, y todos se publican fully open source para que otros equipos que construyen en Stylus puedan estudiar, hacer fork y extender el patrón.
Source completo, notas de arquitectura y el runbook de deploy: github.com/wakeuplabs/ArbitrumMiniApp-2-verifiable-randomness.
Si estás construyendo algo en Stylus que depende de que un proceso externo se resuelva bien, nos encantaría comparar notas.
Acerca de WakeUp Labs
En WakeUp Labs construimos infraestructura blockchain y productos digitales de grado productivo para ecosistemas líderes, startups y empresas.
Nuestra filosofía de ingeniería es simple: la infraestructura nueva solo demuestra su valor cuando corre en producción. En lugar de evaluar tecnologías emergentes con prototipos aislados, las validamos construyendo sistemas reales que interactúan con usuarios reales y restricciones operativas reales.
Si estás construyendo en Arbitrum o explorando Stylus para sistemas de producción, nos encantaría conectar.
