ERC-4337 Riesgos de seguridad de cuentas inteligentes
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
La abstracción de cuentas ERC-4337 convierte wallets en sistemas programables. El flujo central es validar-antes-de-ejecutar a lo largo de todo un lote: el EntryPoint valida cada UserOperation antes de ejecutar cualquiera de ellas. Este orden crea una superficie de ataque no obvia cuando la validación es permisiva o con estado.
1) Evasión por llamada directa a funciones privilegiadas
Cualquier función execute invocable externamente (o que mueva fondos) que no esté restringida a EntryPoint (o a un módulo ejecutor verificado) puede ser llamada directamente para vaciar la cuenta.
function execute(address target, uint256 value, bytes calldata data) external {
(bool ok,) = target.call{value: value}(data);
require(ok, "exec failed");
}
Patrón seguro: restringir a EntryPoint y usar msg.sender == address(this) para flujos de administración/autogestión (instalación de módulos, cambios de validadores, actualizaciones).
address public immutable entryPoint;
function execute(address target, uint256 value, bytes calldata data) external {
require(msg.sender == entryPoint, "not entryPoint");
(bool ok,) = target.call{value: value}(data);
require(ok, "exec failed");
}
2) Campos de gas no firmados o no verificados -> drenaje de tarifas
Si la validación de la firma solo cubre la intención (callData) pero no los campos relacionados con gas, un bundler o frontrunner puede inflar las tarifas y drenar ETH. La carga firmada debe vincular al menos:
preVerificationGasverificationGasLimitcallGasLimitmaxFeePerGasmaxPriorityFeePerGas
Patrón defensivo: usa el EntryPoint-proporcionado userOpHash (que incluye los campos de gas) y/o limita estrictamente cada campo.
function validateUserOp(UserOperation calldata op, bytes32 userOpHash, uint256)
external
returns (uint256)
{
require(_isApprovedCall(userOpHash, op.signature), "bad sig");
return 0;
}
3) Stateful validation clobbering (bundle semantics)
Debido a que todas las validaciones se ejecutan antes de cualquier ejecución, almacenar resultados de validación en el estado del contrato es inseguro. Otra op en el mismo bundle puede sobrescribirlo, provocando que tu ejecución use un estado influenciado por el atacante.
Evita escribir en storage dentro de validateUserOp. Si es inevitable, indexa los datos temporales por userOpHash y elimínalos de forma determinista después de usarlos (preferir validación sin estado).
4) ERC-1271 replay across accounts/chains (missing domain separation)
isValidSignature(bytes32 hash, bytes sig) debe vincular las firmas a este contrato y esta cadena. Recuperar sobre un hash crudo permite que las firmas se repliquen entre cuentas o cadenas.
Usa EIP-712 typed data (el dominio incluye verifyingContract y chainId) y devuelve el valor mágico exacto de ERC-1271 0x1626ba7e en caso de éxito.
5) Reverts do not refund after validation
Una vez que validateUserOp tiene éxito, las tarifas quedan comprometidas incluso si la ejecución revierte después. Los atacantes pueden enviar repetidamente ops que fallarán y aun así cobrar las tarifas de la cuenta.
Para paymasters, pagar desde un pool compartido en validateUserOp y cobrar a los usuarios en postOp es frágil porque postOp puede revertir sin deshacer el pago. Asegura los fondos durante la validación (depósito en custodia por usuario), y mantén postOp mínimo y sin reversiones.
6) ERC-7702 initialization frontrun
ERC-7702 permite que una EOA ejecute código de smart-account para una sola tx. Si la inicialización es callable externamente, un frontrunner puede establecerse como owner.
Mitigación: permitir la inicialización solo en self-call y solo una vez.
function initialize(address newOwner) external {
require(msg.sender == address(this), "init: only self");
require(owner == address(0), "already inited");
owner = newOwner;
}
Chequeos rápidos antes del merge
- Validar firmas usando
userOpHashdeEntryPoint(vincula los campos de gas). - Restringir las funciones privilegiadas a
EntryPointy/oaddress(this)según corresponda. - Mantener
validateUserOpsin estado. - Aplicar la separación de dominio EIP-712 para ERC-1271 y devolver
0x1626ba7een caso de éxito. - Mantener
postOpmínimo, acotado y que no revierta; asegurar las tarifas durante la validación. - Para ERC-7702, permitir init solo en self-call y solo una vez.
Referencias
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.


