Back to Blog

Mais Uma Tragédia de Perda de Precisão: Uma Análise Aprofundada do Incidente KyberSwap

Code Auditing
December 5, 2023
12 min read

Em 23 de novembro de 2023, observamos uma série de ataques direcionados ao KyberSwap. Esses ataques resultaram em uma perda total superior a $48M. Nossa análise inicial sugeriu que o exploit foi causado por manipulação de tick e contagem dupla de liquidez. No entanto, devido a restrições de espaço, não foi possível aprofundar os detalhes extensos naquela publicação. Apesar das análises perspicazes subsequentes de outros pesquisadores de segurança, a causa raiz do problema — perda de precisão — permaneceu não exposta.

Curiosamente, o enredo se aprofundou alguns dias depois. Em 30 de novembro de 2023, após múltiplas rodadas de discussões com os responsáveis oficiais, o atacante enviou uma mensagem que, para o mundo externo, parecia repleta de provocação, exigindo controle total. Deixando isso de lado, o atacante também revelou uma informação crucial: o problema está de fato relacionado à perda de precisão, como mostrado na figura abaixo. Essa revelação fortalece as evidências para nossa investigação. Portanto, nosso objetivo é apresentar uma análise abrangente neste relatório.

Principais conclusões (TL;DR)

  • Nossa investigação revela que o problema fundamental origina-se da direção incorreta de arredondamento durante o processo de reinvestimento do KyberSwap. Isso subsequentemente leva ao cálculo inadequado de tick e, por fim, à contagem dupla de liquidez.

  • Este incidente destaca a natureza complexa e furtiva dos problemas de perda de precisão nos protocolos DeFi, representando um desafio substancial para toda a comunidade.

  • A frequência desses ataques serve como um alerta contundente sobre a necessidade crítica de medidas proativas de prevenção de ameaças, que poderiam ajudar significativamente a reduzir perdas futuras.

Nas seções seguintes, primeiro ofereceremos algumas informações básicas cruciais sobre o KyberSwap. Em seguida, realizaremos uma análise aprofundada da vulnerabilidade e do ataque associado.

0x1 Contexto

KyberSwap[1] é uma plataforma descentralizada de formador de mercado automatizado (CLAMM). Para atender à demanda do mercado de liquidez concentrada, o KyberSwap Elastic[3] foi lançado com base no Uniswap V3[2], com diversas melhorias incluindo a curva de reinvestimento para habilitar o autocomposição dos rendimentos de provisão de liquidez.

0x1.1 Tick e Preço de Raiz Quadrada

O Tick em CLAMMs semelhantes ao Uniswap V3 é utilizado para marcar o preço de forma discreta, de modo que os LPs possam fornecer liquidez dentro de um intervalo fixo em vez de todo o intervalo (daí o termo "concentrado")[4].

Para permitir que os LPs especifiquem posições de liquidez com intervalos de preço personalizados, o protocolo precisava de uma maneira de rastrear a liquidez agregada em vários pontos de preço. O Uniswap V3 conseguiu isso particionando o espaço de preços possíveis em "ticks" discretos, pelos quais os LPs poderiam contribuir com liquidez entre quaisquer dois ticks.

De acordo com [5], a liquidez pode ser colocada em um intervalo entre quaisquer dois ticks (que não precisam ser adjacentes), ou seja, um par de índices de tick (um tick inferior e um tick superior). Especificamente, o preço de cada tick (em um índice inteiro i) é definido da seguinte forma:

Na prática, o preço de raiz quadrada (denotado como sqrtP ou sqrtPrice) é utilizado:

Também é possível calcular o tick atual com base no preço de raiz quadrada atual:

Usar o preço de raiz quadrada junto com a liquidez L é uma forma prática de evitar mudanças simultâneas. Especificamente, o preço muda ao trocar dentro de um tick; a liquidez muda ao cruzar um tick, ou ao cunhar ou queimar liquidez. Para uma explicação mais detalhada, consulte o whitepaper do Uniswap V3[5].

Obviamente, enquanto apenas um único preço de raiz quadrada é calculado para um determinado tick, múltiplos preços de raiz quadrada podem apontar para o mesmo tick.

0x1.2 Curva de Reinvestimento

O CLAMM baseado no Uniswap V3 sofre com a utilização do pool para taxas de LP e taxas de gás significativas necessárias para reinvestimento. Portanto, o KyberSwap adotou a curva de reinvestimento[6] para resolver o problema:

A curva de reinvestimento foi projetada com o único propósito de reinvestir nativamente as taxas de LP que de outra forma não seriam utilizadas no modelo de liquidez concentrada. Isso significava que as taxas de LP para posições de liquidez concentrada eram automaticamente compostas sem a sobrecarga de gás ou de gerenciamento manual. Além disso, os LPs ainda têm a opção de coletar seus ganhos de taxas autocompostas separadamente a qualquer momento.

A chave para a curva de reinvestimento é que as taxas coletadas em cada swap são acumuladas como liquidez adicional no pool como a liquidez de reinvestimento dentro de um intervalo infinito. Os tokens de reinvestimento são cunhados para os LPs e a liquidez de reinvestimento acumulada é alocada para os LPs de acordo. Além disso, a liquidez de reinvestimento também participa do processo de swap e cálculo de preço.

Para ser preciso, em vez da fórmula do produto constante:

as taxas são acumuladas em ΔL em cada swap:

O cálculo de ΔL pode ser simplificado para (sob a suposição de que o desvio de preço é menor que um limite):

Então, o valor do swap e o preço final podem ser derivados da fórmula do produto constante modificada:

O código correspondente aos cálculos introduzidos acima é mostrado na função computeSwapStep no seguinte trecho de código do pool correspondente.

Deve-se observar que, devido à liquidez de reinvestimento, a liquidity nesta função é uma soma de dois componentes: baseL para a liquidez base e reinvestL para a liquidez acumulada para o reinvestimento.

0x1.3 Swap no KyberSwap

O fluxo de controle de um swap no Uniswap V3 pode ser descrito da seguinte forma[5]:

Consequentemente, a implementação da função swap do pool do KyberSwap discutido anteriormente pode ser abstraída no diagrama abaixo:

A lógica crucial pertencente ao cálculo de tick reside dentro do loop while de troca, conforme destacado pelo retângulo azul. Especificamente, a lógica principal envolve a função computeSwapStep e a função _updateLiquidityAndCrossTick. A primeira calcula estados-chave, como valores de entrada e saída para o swap fornecido e nextSqrtP, enquanto a segunda lida com casos quando ocorre um cruzamento de tick.

Tradicionalmente, quando o preço aumenta, nos referimos a isso como deslocar o tick para a direita/para cima; caso contrário, dizemos que o tick se move para a esquerda/para baixo.

Para melhor compreender a vulnerabilidade que será discutida posteriormente, é essencial explorarmos a lógica de código relevante da função computeSwapStep, conforme ilustrado na figura a seguir:

Primeiramente, das linhas 50 a 57, a função calcReachAmount é invocada para calcular a quantidade de token de entrada necessária para atingir o targetSqrtP (próximo tick ou preço-alvo especificado pelo usuário).

Em seguida, entre as linhas 59 e 62, um teste é realizado para determinar se o tick deve ser cruzado ou não.

Especificamente, se o valor utilizado (usedAmount) for maior do que o valor especificado pelo usuário (specifiedAmount) no swap de entrada exata (o caso utilizado no ataque), significa que o tick não deve ser cruzado e o nextSqrtP precisa ser derivado da liquidez incremental (deltaL, ou seja, a liquidez delta).

  • Subsequentemente, entre as linhas 70 e 79, o ΔL (deltaL) é derivado do valor de entrada, liquidez atual e preço usando a função estimateIncrementalLiquidity. Por fim, o preço final após o swap nextSqrtP é calculado com base no deltaL, valor de entrada, preço atual e liquidez, usando a função calcFinalPrice.

Por outro lado, se o valor necessário for menor do que o valor especificado pelo usuário (o que significa que nextSqrtP > 0), o deltaL é calculado usando o sqrtP atual e o alvo, e o nextSqrtP é o sqrtP no próximo tick. Os detalhes são omitidos porque este ramo não é utilizado no ataque.

As etapas descritas acima deixam claro que se o tick não for cruzado, o nextSqrtP retornado pelo computeSwapStep não deve ser maior que o sqrtP do próximo tick. No entanto, devido à dependência do preço na liquidez (liquidez base e liquidez delta) e à perda de precisão, o atacante é capaz de manipular o nextSqrtP para ser maior enquanto o tick não é cruzado.

0x2 Análise da Vulnerabilidade

A causa raiz reside no cálculo falho de tick causado pela direção incorreta de arredondamento dentro do cálculo de liquidez delta (ou seja, a função estimateIncrementalLiquidity) do contrato SwapMath (que é invocado pela função computeSwapStep). Isso, por sua vez, afeta inadequadamente o cálculo de tick posteriormente.

Curiosamente, ao examinar o comentário na linha 188 (destacado pelo retângulo azul), descobrimos que deltaL pretende ser arredondado para cima a fim de arredondar para baixo o nextSqrtP. No entanto, deltaL é erroneamente arredondado para baixo devido ao uso da função mulDivFloor na linha 189. Consequentemente, nextSqrtP é arredondado incorretamente para cima.

0x3 Análise do Ataque

Os atacantes iniciaram múltiplas transações de ataque, com cada transação drenando múltiplos pools. Por simplicidade, a discussão a seguir é baseada no primeiro ataque dentro da transação de ataque.

A lógica central do ataque consiste nas seguintes seis etapas:

  1. Empréstimo de 2.000 WETH via flash loan da AAVE.

  2. Troca de 6,850 WETH por 6,371 frxETH no pool vítima 0xfd7b. Esta etapa é usada para empurrar o tick atual e currentSqrtP para uma localização onde atualmente não há liquidez presente.

  • currentSqrtP parece ter sido escolhido aleatoriamente pelo atacante, e o swap para precisamente neste preço.
  • A liquidez base (baseL) é zero após esta etapa, mas a liquidez de reinvestimento (reinvestL) é diferente de zero.
  1. Adição de liquidez ao pool e posterior remoção de parte da liquidez. Esta etapa é usada para controlar o intervalo e a liquidez total para um valor desejado.
  • O intervalo de tick é escolhido com base no currentSqrtP.
  • A liquidez desejada para o ataque pode ser derivada do intervalo de tick, embora a lógica de cálculo correspondente requeira exploração adicional.
  1. Troca de 387,170 WETH por 0,06 frxETH no pool. Esta etapa é usada para manipular o tick atual de forma que nextTick == currentTick.
  • O valor de entrada é escolhido com base na liquidez e no currentSqrtP.
  1. Troca de 0,06 frxETH por 396,244 WETH no pool. Observe que a direção da troca é oposta em comparação com a etapa anterior. Nesta etapa, a liquidez é contada em dobro para tornar o swap lucrativo e, consequentemente, drenar o pool.

  2. Reembolso do flash loan e coleta de 6,364 WETH e 1,117 frxETH.

Obviamente, as duas últimas trocas (etapas 4 e 5) são as etapas-chave do ataque para manipular o cálculo de tick e tornar o swap lucrativo para drenar o pool. Aprofundaremos os detalhes nas subseções a seguir.

É importante notar que a etapa 3 é crucial para manipular a liquidez. Devido à necessidade de manipulação precisa de tick por meio da operação de arredondamento, alcançar o objetivo adicionando liquidez diretamente é inviável. A remoção de liquidez é para controlar precisamente a liquidez no intervalo conforme desejado pelo atacante.

0x3.1 Etapa 4: manipular o tick atual e currentSqrtP

Após as etapas anteriores (etapas 1 e 2), o atacante preparou o intervalo de tick e a liquidez para manipulação. Especificamente:

  • currentSqrtP está em uma localização desejada
  • tick atual = 110.909 e próximo tick = 111.310, circundando o currentSqrtP

Esta etapa troca WETH por frxETH. Na função computeSwapStep, temos o seguinte rastreamento de execução:

Conforme mostrado na figura acima, o valor para atingir o alvo (ou seja, o próximo tick) será calculado invocando a função calcReachAmount:

  • usedAmount = calcReachAmount(liquidity, currentSqrtP, targetSqrtP)

Observe que este cálculo pode ser derivado antes do swap. Ao escolher cuidadosamente o specifiedAmount (usedAmount = specifiedAmount + 1), o atacante controlou o swap de forma que o alvo (ou seja, o próximo tick 111.310) não fosse atingido, resultando em nextSqrtP = 0.

Nesta situação, como o tick não é cruzado, o nextSqrtP (ou seja, o preço final) precisa ser derivado da liquidez delta (acumulada como taxas de swap).

Primeiro, a liquidez incremental deltaL das taxas é calculada por:

  • deltaL = estimateIncrementalLiquidity(absDelta, currentSqrtP)

Então o preço final nextSqrtP:

  • nextSqrtP = calcFinalPrice(absDelta, liquidity, deltaL, currentSqrtP)

Revisitando o erro de direção de arredondamento discutido na seção anterior, aqui deltaL é erroneamente arredondado para baixo, levando ao arredondamento para cima de nextSqrtP. Especificamente, neste caso, com base no mesmo absDelta (387.170.294.533.119.999.999), os resultados do cálculo diferem devido às diferentes direções de arredondamento:

Portanto, após a manipulação de tick na etapa 4, os estados atuais são resumidos da seguinte forma:

  • currentSqrtP é 20.693.058.119.558.072.255.665.971.001.964, ligeiramente maior que o sqrtP no tick 111.310 (sqrtP em 111.310 = 20.693.058.119.558.072.255.662.180.724.088).
  • tick atual = 111.310 e próximo tick = 111.310

Conforme ilustrado na figura acima, o swap na etapa 4 engana astuciosamente o pool ao fazê-lo acreditar que o tick 111.310 não foi cruzado. No entanto, na realidade, o currentSqrtP é de fato maior que o sqrtP do tick 111.310.

0x3.2 Etapa 5: contagem dupla de liquidez

Com base na manipulação da etapa 4, a lógica do ataque na etapa 5 é razoavelmente direta. Neste ponto, o atacante orquestrou um swap reverso de frxETH para WETH, que deslocaria o tick e o currentSqrtP para a esquerda. Especificamente, a função computeSwapStep é invocada duas vezes dentro do loop, o que em última instância aciona a contagem dupla de liquidez[7] de maneira imprevista e consequentemente gera lucros adicionais.

Conforme mostrado no rastreamento acima:

  • Na primeira invocação da função computeSwapStep, o currentSqrtP foi deslocado para o sqrtP do tick 111.310. Esta é uma troca minúscula que usa apenas 3 wei de frxETH para efetivamente atingir o tick 111.310. Subsequentemente, dentro da função _updateLiquidityAndCrossTick, o tick atual deve cruzar o tick 111.310 (movendo-se para a esquerda/para baixo), mesmo que não tenha verdadeiramente atravessado o tick 111.310 na direção direita/para cima na etapa 4. Isso resulta na liquidez no tick 111.310 sendo contada duas vezes.

  • Na segunda invocação da função computeSwapStep, a contagem dupla anterior de liquidez pode levar ao potencial de lucros adicionais. Especificamente, ao aproveitar esta contagem dupla de liquidez, o preço do swap na etapa final é distorcido, levando a uma quantidade maior de WETH sendo trocada, gerando assim um lucro.

0x4 Resumo dos Ataques e Lucros

Até o momento desta escrita, observamos vários ataques em diferentes cadeias (incluindo Ethereum, Optimism, Polygon, Arbitrum, Avalanche e Base) em produção, causando perdas superiores a $48M. Esses ataques foram lançados por diferentes atacantes, conforme segue:

Uma lista completa dessas transações de ataque foi coletada em um documento que preparamos. Consulte-o para obter informações mais detalhadas.

0x5 Conclusão

Em conclusão, esta é uma vulnerabilidade sutil originada de lógica de arredondamento inadequada. O exploit é incrivelmente sofisticado. De fato, este ano observamos uma série de incidentes de segurança relacionados a problemas de perda de precisão, apresentando desafios significativos para a comunidade.

Mais uma vez, esses ataques contínuos demonstram a importância da prevenção proativa de ameaças, uma estratégia que poderia efetivamente ajudar a mitigar perdas potenciais.

Referência

[1] https://docs.kyberswap.com/

[2] https://blog.uniswap.org/uniswap-v3

[3] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic

[4] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/tick-range-mechanism

[5] https://uniswap.org/whitepaper-v3.pdf

[6] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/reinvestment-curve

[7] https://100proof.org/kyberswap-post-mortem.html

Best Security Auditor for Web3

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

BlockSec Audit