Fase 1: enlaces de pago basados en contratos en EVM
20 de abril de 2026: la Fase 1 es el verdadero inicio de ingeniería de la hoja de ruta no custodial. Sustituimos las EOA derivadas de HD por enlace y las smart accounts de Alchemy por un patrón de factoría + receptor como clon mínimo en EVM. Los fondos se reparten de forma atómica en la transacción de pago; sin barrido, sin custodia.
En resumen
PaymentLinkFactory.computeAddress(linkId)devuelve una dirección CREATE2 determinista. Mostramos esa dirección al pagador antes de que se despliegue nada.PaymentLinkReceiveres un clon mínimo EIP-1167. Acepta token nativo o ERC-20, emitePaidy llama aFeeSplitter.split()en la misma transacción.- api/src/payments/wallet.service.ts deja de generar HD wallets para los enlaces EVM; en su lugar llama a la vista de la factoría.
- El camino de barrido (
drainSmartAccountWallet,getSmartAccountClientForDepositWallet) se omite para los enlaces creados con el flaguseOnChainReceiver.
Qué cambia en esta fase
En el lado del «después» no hay ninguna caja que controlemos reteniendo los fondos.
Contratos introducidos
Todos en un nuevo paquete contracts/evm/.
PaymentLinkFactory
computeAddress(bytes32 linkId) view returns (address): dirección CREATE2 determinista derivada delinkId(el UUID actual del enlace de pago, con hash abytes32).deployAndPay(bytes32 linkId, Split split, address token, uint256 amount): despliegue diferido + pago en una sola transacción. La factoría clona la implementación dePaymentLinkReceiver, la inicializa con elSplity reenvía los fondos.isDeployed(bytes32 linkId) view returns (bool): auxiliar que usan el indexador y la interfaz.- Actualizable detrás de un proxy UUPS, propiedad de la multisig de despliegue (Safe 3 de 5), con timelock de 48 horas en
upgradeTo.
PaymentLinkReceiver (clon)
- Inmutable tras la inicialización. Guarda la struct
Splity la dirección deFeeSplitter. pay(address token, uint256 amount): gestiona token nativo y ERC-20 en un único punto de entrada. UsaSafeERC20para que funcionen tokens no estándar (USDT). EmitePaid(linkId, payer, token, amount).- Llama a
FeeSplitter.split()de forma atómica. Si el reparto revierte, el pago revierte. refund(bytes32 txId)explícito, invocable solo por la EOA del keeper del comercio o por la multisig de despliegue.
FeeSplitter
split(Split s, address token, uint256 amount): reparte por puntos básicos. El wei restante va al comercio (sin pérdida de polvo).- Emite
Split(txId, merchant, platform, referral, bridgeFee)para que el indexador mantenga paridad con los payloads de webhook heredados.
Cambios en la API
El mayor cambio de código está en api/src/payments/wallet.service.ts. Hoy, getOrCreateAddressForPaymentLink deriva desde hd_wallet_seed, calcula la dirección contrafactual de Alchemy y la guarda en wallet_address. Tras la Fase 1, para las cadenas EVM con el feature flag, esa función pasa a ser:
async function getOrCreateAddressForPaymentLink(paymentLinkId, network) {
if (isEvm(network) && featureFlags.useOnChainReceiver(network.chainId)) {
const address = await factory.computeAddress(hashLinkId(paymentLinkId))
return { address, walletAddressId: null, derivationPath: null }
}
// legacy path unchanged
}
Sin derivación HD. Sin material privado en la base de datos para los enlaces nuevos.
Un nuevo servicio, payments/indexer/evm-paid-event.service.ts, se suscribe a los eventos Paid de cada cadena habilitada mediante viem.watchContractEvent. Cuando hay coincidencia:
- Obtiene el
paymentLinkIddel evento (indexado enbytes32). - Inserta o actualiza una fila
PaymentTransaction, con la misma forma que hoy. - Emite los mismos webhooks que emite hoy el camino de finalización del barrido (api/src/webhooks/webhooks.service.ts).
El feature flag useOnChainReceiver se define por chain id. Un enlace creado con el flag desactivado se queda para siempre en el camino de barrido heredado; no migramos enlaces en curso.
Obsolescencias
En esta fase omitimos, pero no eliminamos:
SmartAccountWalletService.drainSmartAccountWallet: ya no se llama en las cadenas con el flag.SmartAccountWalletService.getSmartAccountClientForDepositWallet: no hace falta cuando las direcciones vienen de la factoría.- Filas de
wallet_address: no se escriben en las cadenas EVM con el flag.
La eliminación real ocurre en la Fase 7, cuando todos los enlaces heredados de la cadena estén liquidados.
Plan de migración
- Enlaces nuevos en Base: reciben una dirección CREATE2 calculada por la factoría. La interfaz la muestra igual que hoy.
- Enlaces abiertos existentes en Base: se quedan en el camino de barrido heredado hasta que caduquen o se liquiden. Sin migración forzada.
- Otras cadenas EVM: en el camino heredado hasta que la Fase 3 active su flag.
Riesgos y mitigaciones
- Se muestra una dirección CREATE2 antes de que exista código. Mitigación: documentar que la recuperabilidad depende del desplegador de la factoría. Limitar
maxLinkValueen la factoría durante los primeros 30 días. - USDT y otros ERC-20 no estándar. Mitigación:
SafeERC20tanto enPaymentLinkReceivercomo enFeeSplitter; tests de fuzzing de Foundry contra un fork de mainnet con USDT. - Tokens con comisión por transferencia. Mitigación: lista de tokens permitidos; los que no estén en ella vuelven al camino heredado.
- Retraso del indexador de la cadena. Mitigación: doble suscripción (Alchemy + RPC pública de respaldo); trabajos de conciliación cada hora; inserción/actualización idempotente.
- Hallazgo de auditoría durante el desarrollo. Mitigación: auditoría como requisito; sin despliegue en mainnet antes del informe con todo corregido.
Lista de estado
- Proyecto Foundry creado en
contracts/evm/ -
PaymentLinkFactory+PaymentLinkReceiver+FeeSplitterimplementados - Tests de fuzzing + invariantes para la aritmética del reparto (el comercio siempre recibe ≥ lo esperado menos comisiones)
- Tests en fork de USDT / ERC-20 no estándar pasando
- Feature flag
useOnChainReceiverconectado enapi/src/payments - Indexador
evm-paid-event.service.tspublicado y emitiendo los webhooks existentes - Auditoría iniciada y correcciones integradas
- Despliegue en Base mainnet desde la multisig
- Grupo canario de comercios con el flag activado
Qué sigue
Con los comercios de Base usando receptores en contrato, la Fase 2 hace lo mismo con las suscripciones: Suscripciones por allowance.
Documentación de referencia: Estado de la Fase 1 · Referencia de contratos · Arquitectura
