Armadilha de pool tóxica na Uniswap cobra 131.888 dólares em 29 horas
BlockbeatsTítulo original: "Cotação de 0%, execução de 12,8%: análise da armadilha de taxa dinâmica da pool tóxica da Uniswap v4"
Autor original: Wang Zihao, BitsLab
Entre 22 e 23 de agosto de 2026, uma pool de taxa dinâmica USDC/WBNB da Uniswap v4 na BNB Chain exibiu uma taxa de 0% nas cotações dos agregadores, mas os utilizadores foram efetivamente cobrados com uma taxa de provedor de liquidez (LP fee) de 12,8% nas transações reais. Em cerca de 29 horas, esta pool processou 21 086 transações, acumulando 131 888 dólares em comissões, antes de ser desativada. Todas as transações afetadas foram bem-sucedidas: o contrato não foi comprometido, o output ficou acima do limite mínimo definido pelo utilizador e não houve qualquer sinal anómalo — a diferença foi silenciosamente convertida em comissões.
Este artigo parte de uma transação de amostra:
(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0: o utilizador investiu 5 926,90 USDT e recebeu menos 757,43 USDT do que o esperado), para dissecar como esta pool conseguiu "cotar 0% e executar 12,8%": a decisão em três camadas da função de taxa, a proteção contra slippage (derrapagem) neutralizada e a whitelist que mantém outros provedores de liquidez afastados. Todas as conclusões baseiam-se em dados on-chain e em verificação controlada local.
01 O caminho da cotação à execução
Depois de o utilizador iniciar a troca na interface do agregador, todo o caminho é o seguinte:

Existem dois factos contextuais neste caminho, sobre os quais se baseiam todas as decisões subsequentes.
Primeiro, o lado v4: permite que o Hook substitua a taxa em cada transação. PoolKey.fee = 0x800000 declara uma pool de taxa dinâmica; o Hook devolve 0x400000 | fee no terceiro valor de retorno de beforeSwap. O código no v4-core que recebe este valor de retorno (Pool.sol:303-305):

isOverride() verifica a flag de substituição, removeOverrideFlagAndValidate() remove o 0x400000 e valida que não excede MAX_LP_FEE = 1 000 000 (100%), depois executa a troca com essa taxa e regista o evento Swap. Ou seja, o utilizador paga exatamente o que o Hook devolver — desde que o output continue acima de amountOutMin. Esta é uma funcionalidade formal; ajustar taxas com base na volatilidade ou no inventário são usos legítimos.
Depois, o próprio Hook: declara apenas duas permissões. A v4 codifica as permissões nos 14 bits inferiores do endereço do contrato (Hooks.sol:29-47), validadas na implementação e imutáveis posteriormente. As definições das flags no lado do v4-core e a descodificação do endereço:

A leitura on-chain de getHookPermissions() devolve um bitmap consistente: apenas estes dois bits são verdadeiros.
O Hook não tem código-fonte. Este artigo descompilou a função de taxa completa a partir do bytecode — segue-se a análise comportamental desta lógica que já correu on-chain:

Vamos analisar camada por camada. As duas primeiras respondem à pergunta "isto é uma simulação?", a terceira decide quanto paga a transação real.
02 A decisão em três camadas da função de taxa
Decisão 1: distinguir o chamador
Quando a função de taxa é chamada, primeiro compara tx.origin: se for igual a qualquer um dos três endereços constantes, devolve diretamente o valor de fallback. O fallback está atualmente configurado para 0%.
Os três endereços não foram escolhidos ao acaso. Quando eth_call não especifica from, o origin da transação recai no endereço zero — as consultas em lote dos agregadores normalmente não especificam. Verificação direta num fork local:

A primeira camada reconhece apenas estes três endereços. A camada seguinte mostra como o Hook consegue distinguir depois de especificar um endereço normal.
Decisão 2: verificar o gas
Ao substituir tx.origin por um endereço normal e tentar novamente, o retorno continua a ser 0% — a primeira camada foi contornada, mas a taxa não mudou. Isto mostra que a primeira camada não é a única decisão; localizámos a segunda camada: gasleft() * 10100 / 10000 >= gate.
Os agregadores costumam dar gas muito alto nas cotações, para evitar que rotas candidatas falhem na simulação por falta de gas; na transação real, antes de entrar no Hook, o gas já foi consumido pelo Router, autorizações e saltos anteriores. O mesmo código, três níveis de gas:

A linha divisória medida situa-se entre 16,85M e 16,9M de gas. A diferença entre 30M e 2,59M é a diferença entre "cotação" e "execução".
Decisão 3: pseudoaleatório por transação
As transações reais que passam as duas primeiras camadas entram na terceira: fazem hash da impressão digital do ambiente do bloco, calculam o módulo 10 000 e devolvem a taxa correspondente ao intervalo do resultado. A configuração atual tem três escalões: 8% / 10% / 10%; se não cair em nenhum, devolve 0%.
Esta camada tem dois detalhes de design dignos de nota. A impressão digital mistura três campos do calldata — o calldata da cotação do agregador e o calldata da rota real são naturalmente diferentes, logo as impressões digitais também diferem. Além disso, o pseudoaleatório por transação distribui as taxas, tornando difícil detetar um padrão com poucas transações; uma enumeração simples de regras não consegue bloquear.
Nos 21 086 eventos on-chain aparecem 19 taxas históricas (6,8%–28%), mostrando que os escalões de taxa foram sempre reconfigurados por uma função de administrador conforme necessário; na transação de amostra, a taxa em vigor era 12,8%.
O quadro combinado das três camadas

Chegados aqui, podemos definir uma "pool tóxica" — uma pool v4 que satisfaz simultaneamente três condições: PoolKey.fee carrega a flag de taxa dinâmica; a decisão da taxa lê sinais do ambiente de execução (gas, origin, ambiente do bloco) em vez de ler o estado público do mercado; a taxa da simulação de cotação e a da execução on-chain divergem sistematicamente, beneficiando o implementador da pool tóxica. A fronteira está no que a taxa lê: devolver a mesma taxa a qualquer chamador é design de mercado programável; devolver taxas diferentes consoante "quem está a perguntar" é enganar o sistema de roteamento.
Até aqui, o mecanismo da divergência de taxas está completo. Mas depois de a taxa ser cobrada, porque é que a transação ainda é bem-sucedida? Essa é a próxima questão.
03 Porque é que a proteção contra slippage não reverteu
Primeiro, o caminho completo e os montantes desta transação:

O evento Swap deste salto regista a taxa em vigor no campo fee: 128000. Na v4, a unidade de taxa é 1 000 000 para 100%, logo 128000 é 12,8%. A perda desta transação pode ser medida de duas perspetivas: do lado da pool, a taxa incide sobre o input em WBNB; do lado da rota completa, a diferença entre stablecoins de entrada e saída é de 757,43 USDT, uma perda de 12,7794%, que inclui ainda comissões de saltos anteriores e desvio de paridade. Os dois números diferem apenas 0,02 pontos percentuais, mostrando que a principal fonte da perda é a taxa de LP desta pool.
amountOutMin apenas valida o limite mínimo do output final, sem verificar quanto cada salto intermédio cobrou:

A taxa de 12,8% cabe inteiramente na margem de 20,32%, o output continua acima do mínimo — a transação é bem-sucedida, sem reversão. O padrão habitual em rotas de stablecoins é 0,1%–1%; esta rota deu mais de 20 vezes esse espaço. O utilizador pensa que uma margem larga é "mais segura", mas na prática entrega todos os contratos do caminho à boa-fé da contraparte. Esta conta em gráfico:

04 As operações do implementador da pool tóxica
Whitelist: manter outros provedores de liquidez afastados
A lógica da whitelist está em beforeAddLiquidity, com o seguinte resultado da descompilação:

Adicionar liquidez exige passar três portas: primeiro a verificação de chamada do PoolManager, comum a qualquer Hook v4; depois tem de passar pelo PositionManager oficial; por fim, verifica a whitelist do detentor da posição. Endereços fora da lista são revertidos pelo contrato aqui; outros provedores de liquidez não conseguem entrar, as receitas de comissões não são diluídas e vão todas para o implementador. A lista em si é mantida por uma função de administrador (seletor 0xc4452e52) por endereço.
Escalão de 0% e wash trading (negociação fictícia)
Olhando para as 21 086 transações em conjunto, a distribuição mostra um padrão claro: escalão de 0% com 6 946 transações, volume de 12 947 751 dólares, média de 1 864 dólares por transação; escalões com taxa com 14 140 transações, volume de 1 120 106 dólares, média de 79 dólares por transação. Os grandes volumes concentram-se no escalão de 0%, as comissões concentram-se nas transações pequenas. A explicação mais razoável para os grandes volumes a 0% é o próprio implementador fazer self-trading com gas alto: o gas alto coincide com o mesmo critério da cotação do agregador, e o custo de comissão do self-trading é próximo de zero. O volume fabricado empurra a pool para os rankings dos sites de mercado (o snapshot mostra volume de 24h de cerca de 10,82 milhões de dólares, 16 671 transações); aos olhos do agregador, esta é uma pool com boa profundidade e taxa baixa.
Reconfiguração dos escalões de taxa conforme necessário
A decisão 3 mencionou 19 taxas históricas; elas vêm da função de configuração de taxas descompilada (seletor 0x4d909a45, assinatura pública não registada):

Os quatro escalões de taxa e os limiares são empacotados em conjunto e escritos no slot de armazenamento keccak(poolId, 2). A leitura on-chain desse slot é 0x0138800186a00186a0, correspondendo segmento a segmento ao formato de empacotamento da função. Os escalões de taxa são aqui parâmetros ajustáveis a qualquer momento, não fixados na implementação.
Ciclo de vida
Esta pool foi criada a 22/08 às 06:57, a primeira troca ocorreu às 07:13, e às 21:39 ocorreu a transação de amostra deste artigo; a última transação foi a 23/08 às 12:25, seguida de desativação. Todo o período ativo durou cerca de 29 horas, com volume total de 14 067 857 dólares e receitas de comissões de 131 888 dólares (sem deduzir os custos do implementador). Na verificação, a flag de registo voltou a estar ativa — a pool ainda existe e pode ser reativada a qualquer momento.
05 Caminho completo
Ligando toda a cadeia de ataque:

Toda a causalidade da cadeia está na imagem acima; as três condições são indispensáveis: a simulação acerta nas duas primeiras camadas, a transação real cai na terceira camada e a margem é maior do que a taxa. Se qualquer uma falhar, este esquema desmorona.
06 Correções e recomendações
O primeiro ponto está no método de cotação dos agregadores. A causa raiz é a simulação e a execução não seguirem o mesmo caminho: a cotação usa consultas idealizadas ao nível da pool, com endereço zero e gas alto; a transação real traz identidade real e gas já consumido. O Hook consegue ler exatamente estas diferenças, e a taxa diverge. A solução é aproximar a simulação da execução — simular com o calldata real do Router a enviar, from/to reais e gas próximo da transação on-chain; após a execução, descodificar o evento Swap para verificar a fee e, se não corresponder à cotação, reduzir o peso ou remover a rota.
O segundo ponto está na configuração de slippage do utilizador. A causa raiz é o min_out demasiado folgado: a margem de 20,32% engole toda a taxa de 12,8%, e a transação prossegue normalmente. Em rotas de stablecoins, 0,1%–1% é suficiente para uso diário; verifique este número antes de iniciar a transação; carteiras e interfaces (frontends/backends) que apertem os valores por defeito podem bloquear a maior parte do risco para o utilizador.
O terceiro ponto está na admissão de rotas. A causa raiz é qualquer pool de taxa dinâmica poder participar diretamente na competição de cotações: Hooks sem código aberto e sem registo de auditoria conseguem entrar nas rotas recomendadas graças ao volume fabricado. Para pools de taxa dinâmica sem perfil fiável, não entrar nas rotas por defeito ou reduzir significativamente o peso é o tratamento mais seguro.
Dados e notas
· Transação de amostra:
https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 (bloco 117497524)
· Transação de inicialização da pool:
https://bscscan.com/tx/0x8fa72ef72d77b61f715dc9ce90548249ab045c6d78716078cd5ab425bbe69a05
· PoolManager:
0x28e2ea090877bf75740558f6bfb36a5ffee9e9df
· Pool ID:
0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea
· Hook:
0xd111b3ddd92e627f1864520c770e913ec04e0880 (sem código-fonte, pseudocódigo descompilado independentemente para este artigo; administrador 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)
· Volume e comissões: acumulados transação a transação a partir de todos os 21 086 eventos Swap da pool (eth_getLogs), WBNB convertido ao preço de execução da mesma transação; receita bruta, sem deduzir custos do implementador
Este conteúdo é apenas para fins informativos e educacionais e não constitui aconselhamento de investimento relacionado à BTCC. A BTCC envida todos os esforços, mas não pode garantir a veracidade, a precisão ou a originalidade do conteúdo acima.