---
title: "Vulnerabilidad de empaquetado de UserOperation en ERC-4337"
description: "El 7 de marzo de 2023, Alchemy y miembros de la comunidad de código abierto identificaron problemas de decodificación de calldata en el contrato EntryPoint de ERC-4337 y en el contrato de ejemplo VerifyingPaymaster."
---

# Vulnerabilidad de empaquetado de UserOperation en ERC-4337

**Colaboradores adicionales**:
[fangting@alchemy.com](mailto:fangting@alchemy.com), @drortirosh, @Gooong, @taylorjdawson, @leekt, @livingrockrises

## Resumen

El 7 de marzo de 2023, Alchemy y otros miembros de la comunidad de desarrollo de código abierto, incluyendo [@Gooong](https://github.com/Gooong), [@taylorjdawson](https://github.com/taylorjdawson), [@leekt](https://github.com/leekt) y [@livingrockrises](https://github.com/livingrockrises), identificaron problemas de decodificación de calldata en el contrato EntryPoint de ERC-4337 y en el contrato de ejemplo VerifyingPaymaster.

Estos contratos están actualmente desplegados en varias chains y generan hashes sobre las user operations. La implementación producía hashes inconsistentes según el método de firma usado, lo que puede derivar en varios efectos de segundo orden, como hashes divergentes para las mismas UserOperations y hashes colisionantes para UserOperations distintas.

La discusión fue facilitada por [@drortirosh ](https://github.com/drortirosh)y está documentada en [este issue de Github](https://github.com/eth-infinitism/account-abstraction/issues/237#issuecomment-1466686252).

## Análisis detallado

A continuación se presenta un análisis del código afectado, explicaciones de la vulnerabilidad de empaquetado del EntryPoint, la vulnerabilidad de empaquetado del VerifyingPaymaster y su respectivo impacto.

### Código afectado

El segmento de código en cuestión es el siguiente:

#### ‍[UserOperation.sol:61-75](https://github.com/eth-infinitism/account-abstraction/blob/556f03fadcaba0d7d04cd901e6eb9601db50a998/contracts/interfaces/UserOperation.sol#L61-l75)

#### [VerifyingPaymaster.sol:35-49](https://github.com/eth-infinitism/account-abstraction/blob/556f03fadcaba0d7d04cd901e6eb9601db50a998/contracts/samples/VerifyingPaymaster.sol#L35-l49)

Como contexto, el struct `UserOperation` se define así:

Ambos segmentos de código usan assembly para copiar una porción grande del calldata a memoria, con la intención de capturar parte de una user operation para hashearla.

El método `pack` en `UserOperationLib` pretende capturar todos los campos de la user operation desde `sender` hasta `maxPriorityFeePerGas`, incluyendo los campos de tamaño variable (llamados campos dinámicos en la codificación ABI) `initCode`, `callData` y `paymasterAndData`.

El método `pack` en `VerifyingPaymaster` incluye todos esos campos excepto el campo `paymasterAndData`, ya que este aún no está definido.

Para implementar esto, ambos métodos usan un campo de conveniencia en [Yul](https://docs.soliditylang.org/en/v0.8.17/yul.html) provisto a los tipos dinámicos en calldata, llamado `.offset`. Esto se refiere al valor provisto en la codificación ABI de un struct, definido [aquí en la especificación de Solidity](https://docs.soliditylang.org/en/v0.8.18/abi-spec.html#formal-specification-of-the-encoding). **(En realidad se refiere a la palabra de memoria posterior al offset, pero eso es solo por conveniencia al momento de cargarlo).**

Un codificador ABI estándar codificará los valores de los campos dinámicos (llamados su "tail" en el codificador ABI) en el orden en que aparecen.

Consideremos la siguiente codificación de una user operation en calldata que podría generarse:

<CalloutBlock>

💡 **Nota:** Este ejemplo muestra una user operation donde todos los campos dinámicos tienen una longitud menor a una palabra, por brevedad.

</CalloutBlock>

<CalloutBlock>

💡‍ **\*‍**\***Nota:** El espacio de direcciones de memoria aquí es interno al struct de la user operation. En calldata real, estará ubicado en otro lugar debido al espacio ocupado por el selector de método y la tupla de argumentos.

</CalloutBlock>

En este ejemplo, seguir `pnd.offset` para generar un empaquetado de la user operation dará como resultado este "slice" de calldata:

¡Esto contiene exactamente lo que queremos!

Sin embargo, los contratos que usan argumentos codificados en ABI no validan en qué orden se definen los campos, ni siquiera que los offsets sean válidos.

Usar `signature.offset` o `pnd.offset` leerá el valor de "offset" correspondiente directamente desde calldata.

Usando ese valor como límite, es posible construir representaciones válidas de user operations en calldata que tienen propiedades de hash inusuales.

Exploremos cómo esto afecta al EntryPoint y al VerifyingPaymaster de forma independiente.

### Vulnerabilidad de empaquetado del EntryPoint

Para demostrar esta vulnerabilidad, debemos considerar un contrato de wallet distinto del `SimpleAccount.sol` provisto, ya que ese ejemplo reutiliza el código vulnerable de `EntryPoint`.

La divergencia de hashes se vuelve relevante cuando se usa un esquema de hasheo diferente entre el EntryPoint y el contrato de wallet, o si la wallet firma una codificación de user operation no estándar.

El riesgo que esto introduce a `EntryPoint` es que una sola user operation puede estar representada por múltiples "user op hashes" y que un mismo "user op hash" puede representar múltiples user operations.

Consideremos esta cuenta, llamada `ExampleAccount`, que tiene su propio `ExampleAccountFactory`. La cuenta de ejemplo usa un único firmante para validar user operations.

Para otorgar permiso de ejecutar una user operation, se genera y firma un hash sobre todos los campos de la user operation, excepto la firma misma.

El método `validateUserOp` se define así:

Esta es una implementación relativamente simple de validación de firmas, ya que verifica todos los campos de `userOp`, junto con la dirección del entrypoint y el chain id.

Como uno de los objetivos de la account abstraction, el método `validateUserOp` puede contener lógica arbitraria (aunque limitada por restricciones de acceso a storage), ya que este método representa las condiciones bajo las cuales puede originarse una user operation.

Para esta cuenta de ejemplo, las user operations parten de una firma del owner. Más en general, sin embargo, las user operations pueden originarse a partir de condiciones arbitrarias: estado on-chain, múltiples firmas o firmas específicas de la app; es una característica de la account abstraction.

Para demostrar esta vulnerabilidad, construyamos calldata malicioso hacia `EntryPoint.handleOps` de modo que el `UserOperationEvent` emitido por `EntryPoint` tenga un valor inesperado.

Tras definir un struct de ejemplo `UserOperation memory uo`, así podemos construir el calldata:

`rightPadBytes` es una función de ayuda escrita para alinear los tipos `bytes` a la longitud de palabra completa más cercana.

Se define así:

Ahora, al llamar a `handleOps`, el evento emitido y el resultado de `EntryPoint.getUserOpHash\(\)` serán diferentes.

#### Impacto

Bundlers maliciosos, o EOAs sin rol de bundler que llamen a `EntryPoint.handleOps`, pueden modificar su representación de un `UserOp` en calldata para cambiar el hash de UO en los eventos emitidos. Esto puede romper sistemas off-chain que integran con los eventos emitidos, ya que ahora queda expuesto que los eventos no son determinísticos para una UO dada.

Adicionalmente, el bundler deberá lidiar con no-determinismo al leer los userOpHashes emitidos desde el contrato `EntryPoint`. Para ver si un `UserOperationEvent` emitido desde el `EntryPoint` corresponde a una user operation en el mempool local del bundler, ya no basta con comparar el valor de hash, ya que el calldata hacia `handleOps` puede modificarse para cambiar los valores de hash.

En su lugar, los bundlers tendrán que buscar recibos de transacción, obtener el calldata enviado a `handleOps`, decodificar el calldata y luego obtener los hashes "canónicos" recodificando mediante el codificador ABI estándar y llamando a `EntryPoint.getUserOpHash\(...\)`. Esto es necesario para determinar si las user operations en el mempool local ya fueron minadas o no. Además, dado que las llamadas a `EntryPoint.handleOps` pueden ocurrir desde dentro de otras llamadas a contratos, la decodificación puede estar profunda en la pila de llamadas.

Esta divergencia también afectará la implementación de los métodos RPC del bundler, ya que se usa un user op hash para identificación en `eth\_getUserOperationByHash` y `eth\_getUserOperationReceipt`.

Los bundlers necesitarán realizar búsquedas, parseo y decodificación costosos del calldata hacia `EntryPoint.handleOps\(...\)` para traducir los hashes emitidos por los eventos en hashes "canónicos" desde `EntryPoint.getUserOpHash\(...\)`.

<CalloutBlock>

💡 **Nota:** Esta vulnerabilidad es distinta del hecho de que SCWs maliciosas puedan reutilizar user operations. Las user operations reutilizadas, y en general todas las user operations, deberían tener un hash determinístico. Otras aplicaciones y servicios construidos sobre ERC-4337 tendrán que implementar su propia mitigación a menos que esto se resuelva.

</CalloutBlock>

Dado que ERC-4337 se encuentra en etapas tempranas de adopción en general, es difícil describir el impacto potencial de esta vulnerabilidad en el ecosistema más amplio. El alcance del impacto depende de las implementaciones de bundlers, exploradores de user operations, indexadores y otros servicios off-chain.

Como **mínimo**, causaría una experiencia de usuario confusa, ya que el user operation hash (similar al hash de transacción) puede cambiar entre el momento de envío y el de inclusión, por lo que algunas wallets podrían no contemplar esa diferencia y fallar al mostrar actualizaciones a sus usuarios.

En un **caso de riesgo medio**, las wallets pueden diseñarse de modo que eviten intencionalmente la indexación configurando todos sus user op hashes para que sean iguales (ver el [ejemplo provisto por @leekt](https://github.com/leekt/foundry-4337/compare/PoC/sameUserOpHash)).

En un **caso de riesgo alto**, un servicio off-chain que monitorea la inclusión de user operations podría no detectar la inclusión de una determinada user operation e intentar reenviarla o manejar mal datos y claves.

#### Prueba de concepto

Vea la prueba de concepto completa en [este repositorio](https://github.com/alchemyplatform/entrypoint-hash-poc).

### Vulnerabilidad de empaquetado del VerifyingPaymaster

Los riesgos que esto introduce a `VerifyingPaymaster` son que una user operation puede contener un contenido diferente entre el momento de la firma y su inclusión on-chain. Esto puede ocurrir cuando dos user operations distintas devuelven el mismo hash desde `VerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\)`.

Construyamos calldata para esta función para mostrar cómo puede darse este caso:

#### Calldata 1
Note cómo esta codificación cambia el orden de los campos dinámicos, pero por lo demás sigue siendo válida: los offsets apuntan a las ubicaciones correctas y las longitudes son todas válidas. Pero, dado que el offset de `paymasterAndData` tiene un valor inesperado, el slice que obtenemos de `pack\(\)` será el siguiente:

¡Note cómo `initCode` y `callData` quedan excluidos del slice!

Construyamos un segundo calldata de entrada, esta vez modificando maliciosamente ambos campos:

- `initCode` pasará de `1517c0de` a `1517c0de02`
- `callData` pasará de `ca11dada1` a `ca11data02`

Esto nos da lo siguiente:

#### Calldata 2
Si realizamos la misma operación `pack` sobre este calldata diferente, ¡se obtendrá el mismo slice que antes! Y podemos verificar que los hashes son iguales con lo siguiente:

Al ejecutar esto en un entorno de pruebas en foundry se revela que ambas user ops tienen el mismo hash: `0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06`

Vea [este archivo de pruebas](https://github.com/alchemyplatform/entrypoint-hash-poc/blob/main/test/VerifyingPaymasterHashPOC.t.sol) para la POC completa.

#### Impacto

En esta vulnerabilidad, el hash puede modificarse para cubrir menos elementos de los esperados, permitiendo que initCode, callData y posiblemente otros campos estáticos queden excluidos del hash, y por lo tanto varíen entre la firma y el uso. Esto puede resultar en que firmas de sponsorship del paymaster se usen para propósitos distintos de los previstos.

Por ejemplo, el factory de despliegue del contrato de la wallet y la llamada a la función `execute` de una wallet pueden ser sustituidos. Si un paymaster previamente quería patrocinar solo a usuarios de su wallet, y solo patrocinar cuando acuñaran un NFT específico, esas reglas podrían ser eludidas.

Para eludir las reglas, el sender cambiaría `userOp.initCode` y `userOp.callData` después de obtener una firma. Luego, el token nativo del paymaster (ETH u otro) se usaría para un propósito distinto al de su intención original de un mint de NFT sin gas.

Los firmantes off-chain que reciben user operations para firmar en un formato codificado en ABI, o firmantes que tienen integraciones con contratos para preparar datos para firma, son vulnerables. Este es un alcance limitado, ya que esencialmente "se explotan a sí mismos", pero representa un riesgo para operar un servicio de paymaster.

Las medidas defensivas contra esto incluyen desplegar una versión actualizada de `VerifyingPaymaster`, o manejar ellos mismos el proceso de codificación ABI a partir de la entrada del usuario.

## Conclusión

Tras varias conversaciones excelentes con miembros del ecosistema, **@drortirosh** fusionó un [parche optimizado y legible para el contrato Entrypoint](https://github.com/eth-infinitism/account-abstraction/commit/6a7cbe7c6c786d6302d52b94405b88c6596f95cb) para resolver esta vulnerabilidad. Una vez redesplegados, los contratos Entrypoint ya no presentarán el comportamiento documentado arriba.

Adicionalmente, existe una [propuesta](https://docs.google.com/document/d/1MywdH_TCkyEjD3QusLZ_kUZg4ZEI00qp97mBze9JI4k/edit#heading=h.gyhqxhuyd59n) para abstraer el soporte de nonce en el Entrypoint que también refuerza este sistema. Dado que el riesgo para los paymasters es limitado, no se ha realizado un parche oficial upstream, y los operadores de paymasters pueden decidir cómo manejar esto según lo necesiten para sus implementaciones.

Queremos agradecer a la comunidad de 4337 aquí, incluyendo a **@drortirosh**,** @Gooong**,** @taylorjdawson**,** @leekt** y** @livingrockrises** por trabajar en esta vulnerabilidad junto con nosotros.

¿Tiene alguna pregunta o algún tema que quiera discutir?

Contáctenos en account-abstraction@alchemy.com.
