---
title: "Vulnerabilidade de empacotamento de UserOperation no ERC-4337"
description: "Em 7 de março de 2023, a Alchemy e membros da comunidade open source identificaram problemas de decodificação de calldata no contrato ERC-4337 EntryPoint e no contrato de exemplo VerifyingPaymaster."
---

# Vulnerabilidade de empacotamento de UserOperation no ERC-4337

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

## Visão geral

Em 7 de março de 2023, a Alchemy e outros membros da comunidade open source de desenvolvedores, incluindo [@Gooong](https://github.com/Gooong), [@taylorjdawson](https://github.com/taylorjdawson), [@leekt](https://github.com/leekt) e [@livingrockrises](https://github.com/livingrockrises), identificaram problemas de decodificação de calldata no contrato EntryPoint do ERC-4337 e no contrato de exemplo VerifyingPaymaster.

Esses contratos estão atualmente implantados em diversas chains e geram hashes sobre user operations. A implementação resultava em hashes inconsistentes dependendo do método de assinatura, o que pode levar a diversos efeitos secundários, como hashes divergentes para as mesmas UserOperations e hashes colidentes para UserOperations diferentes.

A discussão foi conduzida por [@drortirosh ](https://github.com/drortirosh) e está documentada [nesta issue do Github](https://github.com/eth-infinitism/account-abstraction/issues/237#issuecomment-1466686252).

## Detalhamento

Abaixo está um detalhamento do código afetado, explicações sobre a Vulnerabilidade de Packing do EntryPoint, a Vulnerabilidade de Packing do VerifyingPaymaster, e seus respectivos impactos.

### Código afetado

O segmento de código em questão é o seguinte:

#### ‍[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)

Para contexto, a struct `UserOperation` é definida como:

Ambos os segmentos de código usam assembly para copiar uma grande porção do calldata para a memória, com a intenção de capturar parte de uma user operation para gerar o hash.

O método `pack` em `UserOperationLib` tem a intenção de capturar todos os campos da user operation, de `sender` a `maxPriorityFeePerGas`, incluindo os campos de tamanho variável (chamados de campos dinâmicos na codificação ABI) `initCode`, `callData` e `paymasterAndData`.

O método `pack` em `VerifyingPaymaster` inclui todos esses campos, exceto o campo `paymasterAndData`, já que este ainda não está definido.

Para implementar isso, ambos os métodos usam um campo de conveniência em [Yul](https://docs.soliditylang.org/en/v0.8.17/yul.html) fornecido para tipos dinâmicos no calldata, chamado `.offset`. Isso se refere ao valor fornecido na codificação ABI de uma struct, definido [aqui na especificação do Solidity](https://docs.soliditylang.org/en/v0.8.18/abi-spec.html#formal-specification-of-the-encoding). **(Na verdade, refere-se à palavra de memória após o offset, mas isso é apenas por conveniência no carregamento).**

Um codificador ABI padrão codificará os valores dos campos dinâmicos (chamados de "tail" no ABI coder) na ordem em que aparecem.

Considere a seguinte codificação de uma user operation em calldata que poderia ser gerada:

<CalloutBlock>

💡 **Nota:** Este exemplo mostra uma user operation em que todos os campos dinâmicos têm menos de uma palavra de comprimento, por questão de brevidade.

</CalloutBlock>

<CalloutBlock>

💡‍ **\*‍**\***Nota:** O espaço de endereços de memória aqui está dentro da própria struct da user operation. No calldata real, ele será colocado em outro lugar devido ao espaço ocupado pelo method selector e pela tupla de argumentos.

</CalloutBlock>

Neste exemplo, seguir `pnd.offset` para gerar um packing da user operation resultará nesta "fatia" do calldata:

Isso contém exatamente o que queremos!

No entanto, contratos que usam argumentos codificados em ABI não validam em que ordem os campos são definidos, nem mesmo se os offsets são válidos.

Usar `signature.offset` ou `pnd.offset` lerá o valor de "offset" correspondente diretamente do calldata.

Usando isso como limite, é possível construir representações válidas de user operations no calldata que têm propriedades de hash incomuns.

Vamos explorar como isso afeta o EntryPoint e o VerifyingPaymaster de forma independente.

### Vulnerabilidade de packing do EntryPoint

Para demonstrar essa vulnerabilidade, precisamos considerar um contrato de wallet diferente do `SimpleAccount.sol` fornecido, já que esse exemplo reutiliza o código vulnerável de `EntryPoint`.

A divergência de hash se torna relevante quando um esquema de hashing diferente é usado entre o EntryPoint e o contrato de wallet, ou se a wallet assina uma codificação de user operation não padrão.

Os riscos introduzidos em `EntryPoint` são que uma única user operation pode ser representada por múltiplos "user op hashes" e que o mesmo "user op hash" pode representar múltiplas user operations.

Considere esta conta, chamada `ExampleAccount`, que possui sua própria `ExampleAccountFactory`. A conta de exemplo usa um único signatário para validar user operations.

Para conceder permissão para executar uma user operation, é gerado e assinado um hash sobre todos os campos da user operation, exceto a própria assinatura.

O método `validateUserOp` é definido da seguinte forma:

Esta é uma implementação relativamente simples de validação de assinatura, pois verifica todos os campos de `userOp`, junto com o endereço do entrypoint e o chain id.

Como um dos objetivos da account abstraction, o método `validateUserOp` pode conter lógica arbitrária (embora limitada pelas restrições de acesso a storage), já que esse método representa as condições sob as quais uma user operation pode se originar.

Para esta conta de exemplo, as user operations partem de uma assinatura do owner. De forma mais geral, porém, user operations podem se originar de condições arbitrárias: estado on-chain, múltiplas assinaturas, ou assinaturas específicas de app – isso é um recurso da account abstraction.

Para demonstrar essa vulnerabilidade, vamos construir um calldata malicioso para `EntryPoint.handleOps` de forma que o `UserOperationEvent` emitido por `EntryPoint` tenha um valor inesperado.

Depois de definir uma struct `UserOperation memory uo` de exemplo, veja como podemos construir o calldata:

`rightPadBytes` é uma função auxiliar escrita para alinhar tipos `bytes` ao comprimento de palavra completo mais próximo.

Ela é definida da seguinte forma:

Agora, ao chamar `handleOps`, o evento emitido e o resultado de `EntryPoint.getUserOpHash\(\)` serão diferentes.

#### Impacto

Bundlers maliciosos, ou EOAs que não são bundlers chamando `EntryPoint.handleOps`, podem modificar sua representação de um `UserOp` no calldata para alterar o hash da UO nos eventos emitidos. Isso pode quebrar sistemas off-chain que integram com os eventos emitidos, já que agora se revela que os eventos são não-determinísticos para uma dada UO.

Além disso, o bundler terá que lidar com não-determinismo ao ler userOpHashes emitidos pelo contrato `EntryPoint`. Para verificar se um `UserOperationEvent` emitido pelo `EntryPoint` corresponde a uma user operation no mempool local do bundler, uma comparação do valor de hash não é mais suficiente, já que o calldata para `handleOps` pode ser modificado para alterar os valores de hash.

Em vez disso, os bundlers terão que buscar transaction receipts, obter o calldata enviado para `handleOps`, decodificá-lo, e então obter os hashes "canônicos" recodificando via o ABI coder padrão e chamando `EntryPoint.getUserOpHash\(...\)`. Isso é necessário para determinar se as user operations no mempool local foram mineradas ou não. Além disso, como chamadas para `EntryPoint.handleOps` podem ocorrer de dentro de outras chamadas de contrato, a decodificação pode estar em uma parte profunda da call stack.

Essa divergência também afetará a implementação dos métodos RPC do bundler, já que um user op hash é usado para identificação em `eth\_getUserOperationByHash` e `eth\_getUserOperationReceipt`.

Os bundlers precisarão realizar buscas, parsing e decodificação custosos do calldata para `EntryPoint.handleOps\(...\)` a fim de traduzir os hashes emitidos nos eventos para hashes "canônicos" a partir de `EntryPoint.getUserOpHash\(...\)`.

<CalloutBlock>

💡 **Nota:** Essa vulnerabilidade é distinta do fato de que SCWs mal-intencionadas podem reutilizar user operations. User operations reutilizadas, e de forma mais geral, todas as user operations, deveriam ter um hash determinístico. Outras aplicações e serviços construídos sobre o ERC-4337 terão que implementar sua própria mitigação, a menos que isso seja resolvido.

</CalloutBlock>

Como o ERC-4337 está nos estágios iniciais de adoção em geral, é difícil descrever o impacto potencial dessa vulnerabilidade no ecossistema mais amplo. O escopo do impacto depende das implementações de bundlers, exploradores de user operations, indexadores e outros serviços off-chain.

No **mínimo**, causaria uma experiência de usuário confusa, já que o user operation hash (semelhante ao transaction hash) pode mudar entre o momento do envio e o momento da inclusão, de forma que algumas wallets podem não considerar essa diferença e deixar de exibir atualizações a seus usuários.

Em um **caso de risco médio**, wallets podem ser projetadas de forma a evitar intencionalmente a indexação, definindo todos os seus user op hashes como iguais (veja o [exemplo disso fornecido por @leekt](https://github.com/leekt/foundry-4337/compare/PoC/sameUserOpHash)).

Em um **caso de risco alto**, um serviço off-chain que monitora a inclusão de user operations poderia deixar de detectar a inclusão de uma determinada user operation, e tentar reenviar ou lidar incorretamente com dados e chaves.

#### Prova de conceito

Veja a prova de conceito completa [neste repositório](https://github.com/alchemyplatform/entrypoint-hash-poc).

### Vulnerabilidade de packing do VerifyingPaymaster

Os riscos introduzidos em `VerifyingPaymaster` são que uma user operation pode conter conteúdo diferente entre o momento da assinatura e a inclusão on-chain. Isso pode acontecer quando duas user operations diferentes retornam o mesmo hash de `VerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\)`.

Vamos construir o calldata para essa função para mostrar como isso pode ocorrer:

#### Calldata 1
Observe como essa codificação muda a ordem dos campos dinâmicos, mas continua válida de outra forma - os offsets apontam para os locais corretos e os comprimentos são todos válidos. Porém, como o offset de `paymasterAndData` tem um valor inesperado, a fatia que obtemos de `pack\(\)` será a seguinte:

Veja como `initCode` e `callData` são excluídos da fatia!

Vamos construir um segundo calldata de entrada, desta vez modificando maliciosamente ambos os campos:

- `initCode` mudará de `1517c0de` para `1517c0de02`
- `callData` mudará de `ca11dada1` para `ca11data02`

Isso nos dá o seguinte:

#### Calldata 2
Se realizarmos a mesma operação `pack` nesse calldata diferente, o resultado será a mesma fatia de antes! E podemos verificar que os hashes são iguais com o seguinte:

Executando isso em um ambiente de teste no foundry revela que ambas as user ops têm o mesmo hash: `0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06`

Veja [este arquivo de teste](https://github.com/alchemyplatform/entrypoint-hash-poc/blob/main/test/VerifyingPaymasterHashPOC.t.sol) para o POC completo.

#### Impacto

Nessa vulnerabilidade, o hash pode ser modificado para cobrir menos elementos do que o esperado, permitindo que initCode, callData, e possivelmente outros campos estáticos sejam excluídos do hash, e assim variar entre a assinatura e o uso. Isso pode resultar em assinaturas de sponsorship do paymaster sendo usadas para finalidades diferentes das pretendidas.

Por exemplo, a factory de deploy do contrato de wallet e a chamada para a função `execute` de uma wallet podem ser substituídas. Se um paymaster anteriormente queria patrocinar apenas usuários de sua wallet, e apenas quando eles mintassem um NFT específico, essas regras poderiam ser contornadas.

Para contornar as regras, o sender mudaria `userOp.initCode` e `userOp.callData` depois de obter uma assinatura. Então, o token nativo do paymaster (ETH ou outro) seria usado para um propósito diferente da intenção original de um mint de NFT sem custo de gas.

Signatários off-chain que recebem user operations para assinar em um formato codificado em ABI, ou signatários que têm integrações de contrato para preparar dados para assinatura, são vulneráveis. Esse é um escopo limitado, já que essencialmente estão "explorando a si mesmos", mas isso apresenta um risco para a operação de um serviço de paymaster.

Medidas defensivas contra isso incluem implantar uma versão atualizada de `VerifyingPaymaster`, ou lidar com o processo de codificação ABI eles mesmos a partir da entrada do usuário.

## Conclusão

Após diversas conversas excelentes com membros do ecossistema, **@drortirosh** fez o merge de um [patch otimizado e legível para o contrato Entrypoint](https://github.com/eth-infinitism/account-abstraction/commit/6a7cbe7c6c786d6302d52b94405b88c6596f95cb) para resolver essa vulnerabilidade. Uma vez reimplantados, os contratos Entrypoint não apresentarão mais o comportamento documentado acima.

Além disso, há uma [proposta](https://docs.google.com/document/d/1MywdH_TCkyEjD3QusLZ_kUZg4ZEI00qp97mBze9JI4k/edit#heading=h.gyhqxhuyd59n) para abstrair o suporte a nonce no Entrypoint, que também reforça esse sistema. Dado que o risco para os paymasters é limitado, nenhum patch oficial upstream foi feito, e os operadores de paymaster podem decidir como lidar com isso conforme necessário para suas implementações.

Queremos agradecer à comunidade do 4337 aqui, incluindo **@drortirosh**, **@Gooong**, **@taylorjdawson**, **@leekt**, e **@livingrockrises**, por trabalharem conosco nessa vulnerabilidade.

Tem alguma dúvida ou tópico que gostaria de discutir?

Entre em contato com a gente em account-abstraction@alchemy.com.
