Back to Blog

Integração Letal: Vulnerabilidades em Hooks Devido a Interações Arriscadas

Code Auditing
November 20, 2023
10 min read

Conforme destacado em nosso artigo anterior, mais de 30% dos projetos no repositório Awesome Uniswap v4 Hooks[1] apresentam vulnerabilidades. Vale ressaltar que as vulnerabilidades às quais nos referimos aqui são específicas às interações com o Uniswap v4. Sendo assim, neste artigo, examinaremos a lógica segura de interação com hooks sob as seguintes duas perspectivas:

  • Controle de Acesso Falho
  • Validação de Entrada Inadequada

Para cada categoria, começaremos com uma análise da vulnerabilidade e demonstraremos sua possível exploração fornecendo a respectiva Prova de Conceito (PoC). Em seguida, discutiremos possíveis estratégias de mitigação.

Controle de Acesso Falho

De modo geral, as interações relacionadas aos hooks do Uniswap v4 podem ser classificadas com base em se o hook atua como um locker, adquirindo um lock no PoolManager para realizar operações nos pools. Dois cenários principais de interação requerem controles de acesso adequados:

  • Interação Hook-PoolManager: Envolve interações entre as funções de callback oficiais e o PoolManager. As funções de callback incluem oito callbacks de ação de pool (ou seja, initialize, modifyPosition, swap e donate) e o callback de lock (ou seja, lockAcquired).
  • Interação Hook-Interna: Refere-se às interações que ocorrem dentro do contrato hook (atuando como locker).

As interações Hook-PoolManager são relativamente diretas. Aqui, o hook atua puramente como um hook, aceitando os oito callbacks de ação de pool. A lógica no hook não afeta os pools relacionados, ou seja, não há fluxos de fundos entre o hook e os pools. Os parâmetros fornecidos pelas funções de callback são usados para modificar os storages necessários ou como parâmetros importantes de função. A principal consideração é se os parâmetros de callback podem ser manipulados.

As interações Hook-Internas são um pouco mais complexas. Na prática, muitos protótipos de hook fazem mais do que atuar como hooks puros. Alguns desenvolvedores permitem que os hooks forneçam funções de gerenciamento de fundos para seus usuários. Essas funções podem não estar implementadas nos contratos hook, mas ainda podemos considerá-las coletivamente como hooks neste contexto. Nesses casos, um hook aceita fundos de usuários e realiza operações de pool, como gerenciamento de liquidez ou swaps. Isso significa que o contrato deve adquirir um lock do PoolManager, transformando o hook em um locker. A Uniswap Foundation considerou essa situação e integrou uma função em seu template de hook. Especificamente, o template BaseHook fornece a função lockAcquired como o callback de lock, conforme a seguir:

    function lockAcquired(bytes calldata data) external virtual poolManagerOnly
returns (bytes memory) {
        (bool success, bytes memory returnData) = address(this).call(data);
        if (success) return returnData;
        if (returnData.length == 0) revert LockFailure();
        // if the call failed, bubble up the reason
        /// @solidity memory-safe-assembly
        assembly {
            revert(add(returnData, 32), mload(returnData))
        }
    }

Para executar lógica customizada, lockAcquired aceita bytes de data e realiza uma chamada de baixo nível para si mesmo usando esses data. Os data dependem da lógica de negócio do hook e podem ser manipulados pelos usuários, potencialmente levando a problemas de segurança devido às interações Hook-Internas acionadas pelo lockAcquired. Note que o design de hooks é tão flexível que não é possível cobrir todos os cenários possíveis nessa situação. Nosso foco principal aqui é o hook adquirindo um lock e suas subsequentes interações internas. Aprofundar-se em outras lógicas de negócio potenciais tornaria a situação complexa demais para esta discussão.

Em ambos os cenários, a prioridade é tratar quaisquer controles de acesso falhos que possam potencialmente levar a exploração, dado que essas funções possuem entidades de interação bem definidas. Nas subseções seguintes, examinaremos sequencialmente cada cenário e discutiremos os controles de acesso necessários para garantir uma lógica de interação mais segura.

Análise de Vulnerabilidade

Os controles de acesso são soluções de segurança altamente eficientes e diretas para muitos projetos. Se uma função é projetada para ser chamada por entidades específicas, ela deve incorporar controle de acesso. O exemplo mais conhecido de controle de acesso é o contrato Ownable da biblioteca OpenZeppelin, que exige que funções privilegiadas sejam chamadas apenas pelo proprietário do contrato. É evidente que os dois cenários discutidos acima são casos adequados para esse tipo de controle.

Interação Hook-PoolManager: Para interações seguras com o PoolManager, os hooks devem aplicar controle de acesso necessário nessas funções de callback. Especificamente, esses callbacks devem ser chamados exclusivamente pelo PoolManager e não por nenhuma outra conta. A falha em estabelecer tais controles pode deixar essas interfaces sensíveis expostas a possíveis explorações por agentes maliciosos.

Além dos oito callbacks de ação de pool, o callback de lock (ou seja, lockAcquired), que executa lógica customizada após obter o lock do PoolManager, também precisa tratar essa questão.

Interação Hook-Interna: As funções envolvidas nas interações hook-internas também são projetadas para serem invocadas por chamadores específicos. Como mencionamos anteriormente, esse cenário contém duas fases. Primeiro, a função lockAcquired do locker é chamada pelo PoolManager, o que indica que a função deve exigir que o msg.sender seja o PoolManager. Segundo, o hook despacha a chamada de função de acordo. Com base no design do BaseHook, isso é implementado por chamadas de baixo nível ao próprio hook. Isso indica que essas funções devem ser definidas como external e limitar o chamador ao endereço do próprio hook.

Tomemos como exemplo um dos projetos listados no repositório Awesome Uniswap v4 Hooks, ou seja, Stop Loss Order[2]:

Integradas diretamente nos pools do Uniswap V4, as ordens de stop loss são publicadas on-chain e executadas via o hook afterSwap(). Nenhum bot externo ou ator é necessário para garantir a execução.

Vamos examinar sua função de callback afterSwap:

Figura 1: A função afterSwap do Stop Loss Order
Figura 1: A função afterSwap do Stop Loss Order

Claramente, a função acima é projetada para realizar operações sensíveis. No entanto, devido ao controle de acesso falho, ela poderia ser explorada por agentes maliciosos que manipulam os argumentos (por exemplo, key e params), resultando em comportamentos inesperados. Por exemplo, o callback afterSwap pode operar sob a suposição de que o swap já ocorreu no PoolManager. Em seguida, poderia iniciar ações para registrar informações de estado essenciais, como o preço atual ou as taxas de swap coletadas. No entanto, se afterSwap não restringir suas invocações estritamente ao PoolManager, agentes maliciosos poderiam falsificar o parâmetro params, levando a estados registrados distorcidos.

Exploração & PoC

Para simplificar, usaremos um PoC básico para ilustrar esse problema de controle de acesso. Geralmente, o beforeInitialize do hook aceita um parâmetro do tipo PoolKey, que deve conter o endereço deste hook em seu campo hooks (pois o PoolManager usará esse campo para determinar o endereço do hook a ser chamado).

A captura de tela fornece um PoC que demonstra a exploração de um hook com controle de acesso falho, como visto em DiamondHookPoC[3]. Na ausência de restrições de acesso na função de callback beforeInitialize, agentes maliciosos podem fornecer um poolKey arbitrário para essa função. O hook não verifica se o hook deste poolKey corresponde ao endereço do hook atual.

Figura 2: PoolKey.hooks pode ser definido como endereço zero beforeInitialize_poolKey_no_hooks_validation.webp

Embora seja importante notar que a exploração neste cenário pode não causar perdas financeiras ao hook, ela destaca dramaticamente como o estado do hook pode ser manipulado por meio de funções de callback desprotegidas.

Como Mitigar

Para garantir a segurança das interações Hook-PoolManager, tanto os callbacks do hook quanto o callback de lock devem restringir sua acessibilidade exclusivamente ao PoolManager.

Felizmente, o Uniswap v4 fornece boas práticas por meio do BaseHook em seu repositório v4-periphery[4]. O BaseHook fornece o modificador poolManagerOnly para restringir as invocações estritamente ao PoolManager:

    /// @dev Only the pool manager may call this function
    modifier poolManagerOnly() {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        _;
    }

Este modificador pode ser efetivamente empregado para aplicar controle de acesso adequado nos callbacks sensíveis de hook e lock.

Por outro lado, a presença das interações Hook-Internas requer que quaisquer funções significativas de alteração de estado invocadas via callback lockAcquired, conforme especificado pelo BaseHook, não devam ser chamadas arbitrariamente.

Para atender a esse requisito, o BaseHook oferece um modificador selfOnly. Este modificador restringe a acessibilidade da função declarada ao próprio hook, proibindo que contratos externos invoquem diretamente essas funções sensíveis para fins maliciosos.

    /// @dev Only this address may call this function
    modifier selfOnly() {
        if (msg.sender != address(this)) revert NotSelf();
        _;
    }

Em resumo, ao herdar do BaseHook, hooks customizados podem aproveitar esses modificadores e callbacks de controle de acesso integrados para aplicar um controle de acesso adequado.

Validação de Entrada Inadequada

O BaseHook no v4-periphery[4] oferece uma solução para uma lógica de interação mais segura, que os desenvolvedores de hooks podem aproveitar. No entanto, continuamos a observar instâncias de uso inadequado que abrem novas possibilidades para vetores de ataque em hooks existentes.

Por padrão, os hooks permitem que qualquer pool se registre via a função initialize no PoolManager. No entanto, se um hook falha em validar os ativos subjacentes no pool que está sendo registrado, usuários maliciosos poderiam registrar um pool contendo tokens falsificados, permitindo-lhes reentrar no hook via a função transfer dos tokens.

Essa vulnerabilidade é sutil, pois o próprio hook pode não executar lógica maliciosa. No entanto, quando o hook chama o PoolManager, as interações entre o PoolManager e os ativos subjacentes de um pool malicioso poderiam potencialmente transferir o fluxo de controle para um atacante via a função take no PoolManager.

    /// @inheritdoc IPoolManager
    function take(Currency currency, address to, uint256 amount) external override 
noDelegateCall onlyByLocker {
        _accountDelta(currency, amount.toInt128());
        reservesOf[currency] -= amount;
        currency.transfer(to, amount);
    }

Em essência, a vulnerabilidade decorre de validações inadequadas no pool registrado com o qual os usuários do hook planejam interagir. Aprofundaremos essa vulnerabilidade usando um exemplo concreto e discutiremos possíveis estratégias de mitigação.

Análise de Vulnerabilidade

Take Profits Hook[5] é um hook listado pelo Awesome Uniswap v4 Hooks:

Neste exemplo, construímos um hook que permite aos usuários colocar posições de 'take-profit'. Por exemplo, em um pool ETH/DAI onde atualmente 1 ETH = 1500 DAI, você poderia colocar uma ordem de take-profit como "vender todo o meu ETH quando 1 ETH = 2000 DAI", que será executada automaticamente.

Vamos examinar a função _handleSwap neste hook. Esta função realiza um swap para preencher ordens de take-profit após obter um lock.

Figura 3: A função _handleSwap do Take Profits Hook[5]
Figura 3: A função _handleSwap do Take Profits Hook[5]

Você pode notar que esta função não é protegida por nenhum modificador de controle de acesso. No entanto, a linha 250 efetivamente restringe o acesso de modo que esta função só pode ser invocada após um lock ter sido adquirido do PoolManager. Caso contrário, o poolManager.swap falharia, pois o operador não seria o locker mais recente. Em outras palavras, _handleSwap deve ser invocado em uma ordem específica, desde que os pools registrados sejam validados. Infelizmente, o hook não implementa tal validação.

Devido a essa implementação falha, o hook é suscetível a um ataque de reentrância. Essa vulnerabilidade poderia permitir que atacantes forçassem swaps arbitrários usando fundos depositados por usuários.

Exploração & PoC

Especificamente, o ataque pode ser lançado através das seguintes etapas:

  1. O atacante registra um pool malicioso com tokens falsos, especificando o Take Profits Hook como o hook do pool.
  2. O atacante coloca uma ordem de stop-profit no pool malicioso via o hook.
  3. O atacante realiza um swap no pool malicioso, acionando o fillOrder no callback afterSwap para preencher a ordem de stop-profit do atacante.
  4. O hook invoca a função lock do PoolManager para solicitar um lock e chama a função _handleSwap no callback lockAcquired.
  5. Na função _handleSwap, as transferências de tokens acionam lógica maliciosa no contrato de token falso, que reentra na função _handleSwap. Isso é possível porque _handleSwap é uma função externa sem quaisquer restrições de acessibilidade. Como o lock já foi obtido, o atacante pode forçar o hook a executar swaps arbitrários em qualquer pool, desde que o hook possua ativos subjacentes suficientes. O atacante pode então realizar sandwich nos swaps para obter lucros às custas de outros usuários.

O diagrama detalhado a seguir ilustra o fluxo do ataque.

Figura 4: O Fluxo do Ataque
Figura 4: O Fluxo do Ataque

Como mencionado anteriormente, o próprio hook não invoca lógica maliciosa. O único erro é o hook não impedir que pools com tokens não confiáveis se registrem no contrato PoolManager. Indiretamente, a lógica maliciosa no contrato de token falso é invocada via operações de transferência de tokens, o que também é um tipo de chamada externa não confiável.

Como Mitigar

Existem três abordagens viáveis para mitigar possíveis ataques devido à validação de entrada inadequada:

  • Controle de Acesso Adequado. Ao aproveitar os blocos de construção do BaseHook, um hook pode gerenciar estritamente a acessibilidade das funções. Isso impede que contas arbitrárias invoquem funções sensíveis.

  • Lock de Reentrância. No cenário de ataque acima, essa abordagem pode indiscutivelmente impedir que a lógica de token maliciosa reenter as funções sensíveis. No entanto, em alguns casos, o design do hook requer que o próprio hook seja reentrável. Especificamente, quando um hook precisa executar algumas ações de pool, ele deve permitir que o PoolManager reenter seus callbacks para concluir essas ações. Um lock de reentrância pode quebrar essa funcionalidade pretendida.

  • Abordagem de Lista de Permissões. Isso exigiria que um administrador privilegiado colocasse em lista de permissões os pools aprovados nos hooks. O administrador garante que os pools na lista de permissões não introduzam riscos potenciais. No entanto, a limitação é que os usuários do hook só poderiam executar operações em um número limitado de pools aprovados pelo administrador via o hook. Embora a abordagem de lista de permissões melhore a segurança, ela restringe severamente a funcionalidade do hook.

É desafiador encontrar uma solução perfeita que equilibre segurança e usabilidade para hooks. Embora discutamos várias abordagens de mitigação, os desenvolvedores precisarão considerar cuidadosamente as compensações em seu design de hook. O objetivo deve ser mitigar os riscos potenciais o máximo possível, mantendo a funcionalidade pretendida. Além disso, nossa discussão cobre apenas vulnerabilidades que podem estar nas interações especificamente relacionadas às funcionalidades do Uniswap v4. As aplicações práticas serão indiscutivelmente mais abrangentes. Sempre certifique-se de que você entende cada linha de seus contratos e mantenha-se SAFU!

Conclusão

Neste artigo, exploramos as vulnerabilidades que surgem durante a lógica de interação com hooks, concentrando-nos especificamente em dois cenários: controle de acesso falho e validação de entrada inadequada. Apresentamos uma análise detalhada de vulnerabilidades, ilustramos possíveis explorações junto com seus PoCs e discutimos possíveis estratégias de mitigação. Acreditamos que esses insights podem contribuir para o desenvolvimento e uso seguros dos hooks, e orientar esforços futuros na detecção de vulnerabilidades.

Referências

[1] Awesome Uniswap v4 Hooks

[2] Stop Loss Order

[3] DiamondHookPoC

[4] v4-periphery

[5] Take Profits

Leia o Outro Artigo desta Série

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit