Voltar aos artigos
Intermediate

Desvio de Invariante em AMM: Como Acúmulo de Taxas e Ataques de Doação Quebram Suposições de Produto Constante

O invariante de produto constante — `x * y = k` — é a espinha dorsal matemática de todo AMM estilo Uniswap V2. É elegante, determinístico e, em termos matemáticos puros, inquebrantável.

@0xrafasecFebruary 18, 2026decentralized_systems_security

Available in English

Share:

Desvio de Invariante em AMM: Como Acúmulo de Taxas e Ataques de Doação Quebram Suposições de Produto Constante

Legal & Ethical Disclaimer

This content is provided for EDUCATIONAL and AUTHORIZED SECURITY TESTING purposes only.

DO
  • Use these techniques on systems you own or have explicit written permission to test
  • Practice in authorized lab environments (VulnHub, HackTheBox, DVWA, etc.)
  • Follow responsible disclosure practices when finding vulnerabilities
  • Use knowledge for defensive security and authorized penetration testing
DO NOT
  • Access systems without explicit authorization
  • Use these techniques for malicious purposes
  • Deploy exploits against production systems you don't own
  • Share working exploits for unpatched vulnerabilities

Legal warning

Unauthorized access to computer systems is illegal in most jurisdictions (e.g. CFAA in the US, Computer Misuse Act in the UK). Violators may face criminal prosecution and civil liability. The author and publisher assume no liability for misuse of this information. By continuing, you agree to use this knowledge ethically and legally.

Hook & Contexto

O invariante de produto constante — x * y = k — é a espinha dorsal matemática de todo AMM estilo Uniswap V2. É elegante, determinístico e, em termos matemáticos puros, inquebrantável. O problema é que contratos reais de AMM não vivem em termos matemáticos puros. Eles vivem na EVM, onde divisão inteira trunca, onde taxas de protocolo acumulam como incrementos discretos de token, e onde qualquer pessoa pode chamar transfer() para inserir tokens no saldo de um pool sem passar pelo roteador de swap. Cada uma dessas forças causa divergências nas reservas reais — às vezes por alguns wei, às vezes o suficiente para redirecionar um caminho de arbitragem — do k teórico que o contrato acredita estar fazendo cumprir.

Esse desvio não é apenas uma nota de arredondamento. O desvio de invariante cria uma lacuna persistente e explorável entre o que o AMM acredita sobre seu próprio estado e o que é realmente verdade. Atacantes sofisticados — e em alguns casos apenas usuários descuidados — podem ampliar essa lacuna deliberadamente e, em seguida, extrair essa discrepância como lucro. As vítimas são sempre os provedores de liquidez, que descobrem que suas posições valem menos do que as garantias de invariante deveriam permitir. Os atacantes geralmente são invisíveis: operam inteiramente dentro do gráfico de chamadas esperado do protocolo, nunca tocando em uma função controlada por acesso.

Para auditores, essa é uma das classes de bug mais difíceis de detectar manualmente. Nenhuma linha de código é obviamente errada. A vulnerabilidade é emergente — ela vive na interação entre contabilidade de taxas, rastreamento de reservas e a verificação de invariante que deveria proteger LPs. Os testes de invariante baseados em fuzz com Foundry são o que a indústria tem de mais próximo a um detector sistemático. Este artigo te ensina o modelo de ameaça, a anatomia do ataque e como escrever harnesses que encontrem desvios antes do deployment.


TL;DR

ConceitoUma frase
Desvio de invariantex * y real diverge de k armazenado devido a taxas, doações e arredondamento
Ataque de doaçãotransfer() direto para o pool infla reservas sem atualizar k corretamente
Acúmulo de taxasTaxas de protocolo/LP alteram saldos sem ajuste proporcional de k
Erros de arredondamentoDivisão inteira da EVM cria pequenas assimetrias que se acumulam em muitos swaps
Técnica de auditoriaEscrever harnesses de invariante Foundry que afirmem k monotonicamente não-decrescente
Vítimas primáriasProvedores de liquidez via valor de posição diluído

Fundações & Teoria

A fórmula de produto constante pura diz: após cada swap, x' * y' = x * y. Nenhum valor entra ou sai; a curva é conservada. No momento em que você adiciona uma taxa — mesmo uma bem pequena — isso se quebra por design. No Uniswap V2, a taxa de swap de 0,3% significa que (1 - 0,003) da quantidade de entrada move a curva, enquanto 0,003 fica no pool como compensação para LP. Isso intencionalmente causa k crescer com cada swap. O invariante não é k = constante; é k_after >= k_before. Uniswap V2 faz cumprir isso calculando um "saldo ajustado" (balance * 1000 - fee * 3) e verificando que o produto de saldos ajustados é pelo menos k_before * 1000000. Este é o invariante de verificação _update + swap.

O design funciona corretamente quando a única forma de alterar reservas é através dos pontos de entrada oficiais swap, mint e burn. Mas a EVM não força isso. Qualquer endereço pode chamar token.transfer(pool_address, amount) e aumentar o token.balanceOf(pool) do pool sem chamar nenhuma função do pool. O pool só aprende sobre isso quando alguém chama sync() ou skim(), ou quando o próximo swap lê balanceOf e descobre uma discrepância versus as variáveis reserve armazenadas.

Esta é a raiz do desvio de invariante: existem múltiplas fontes de verdade para os saldos de token de um pool, e eles podem discordar.


Onde Encaixa no Workflow

Loading diagram…

A análise de desvio de invariante deve começar durante a fase de revisão de arquitetura, não após deployment. Uma vez que você entenda o modelo de taxa e padrão de atualização de reserva, pode sinalizar imediatamente se doações de transferência direta são tratadas com segurança. Harnesses de fuzz escritos durante a auditoria funcionam como testes de regressão que a equipe de protocolo pode levar adiante.


Conceitos-chave em Profundidade

1. Acúmulo de Taxas como Desvio Controlado

No Uniswap V2, taxas de LP crescem k monotonicamente. Este é o comportamento esperado — LPs ganham valor. Mas forks às vezes introduzem taxas de protocolo (uma fração da taxa de LP redirecionada para um tesouro) que não são extraídas imediatamente. Em vez disso, elas ficam no pool como saldos de token, incluídas em reservas, até que uma chamada collectProtocolFee() privilegiada as extraia. Isso cria uma classe de bugs onde a verificação de invariante passa (porque os tokens de taxa fazem k parecer maior), mas o preço implícito está errado porque o saldo de taxa não deveria ser negociável como liquidez.

Pior ainda: se a função de coleta de taxa tem um erro de arredondamento e extrai ligeiramente mais do que deveria, pode fazer k cair abaixo do seu valor pré-swap. Qualquer caminho de código que diminua k é um vetor de drenagem potencial de LP.

2. Ataques de Doação: Inflação Sem Atualização de Invariante

Um ataque de doação é simples em conceito. Um atacante envia tokens diretamente para o endereço do pool, contornando o roteador. O balanceOf do pool aumenta, mas sua variável reserve armazenada não. A próxima chamada a sync() atualiza a reserva para corresponder a balanceOf, e o pool recalcula k. De repente, k saltou — mas nenhuma cota de LP foi cunhada para contabilizar o valor injetado. O atacante doou valor aos LPs existentes.

Por que alguém faria isso intencionalmente? Porque pode engenheirar um cenário onde são o único LP no momento da doação, então retiram sua posição e capturam o valor doado. Este é o padrão de ataque clássico de "inflação de cota de LP", relacionado ao ataque de inflação de vault ERC-4626. Os passos são:

Loading diagram…

A base de código Uniswap V2 trata disso parcialmente com um bloqueio MINIMUM_LIQUIDITY de 1000 cotas queimadas na gênese — tornando a precondição "único LP" mais difícil de alcançar — mas forks que removem ou reduzem esse mínimo são imediatamente vulneráveis.

3. Acúmulo de Erros de Arredondamento

A aritmética da EVM é apenas inteira. Cada divisão trunca em direção a zero. Em um pool de alta frequência, isso significa cada swap pode deixar um resíduo de 1 wei que não é corretamente atribuído ao LP nem corretamente excluído de k. Ao longo de milhões de swaps, esses erros se acumulam.

Mais criticamente, arredondamento na fórmula de cunhagem de liquidez pode causar cotas a serem emitidas que valem infinitesimalmente mais do que os tokens depositados, ou infinitesimalmente menos. Bugs de arredondamento relevantes para auditoria geralmente aparecem em um dos três lugares:

  • liquidity = min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1) — o min garante a contagem de cota mais conservadora, mas o valor descartado fica no pool, enriquecendo ligeiramente os LPs existentes.
  • A verificação de invariante ajustada por taxa: balance0Adjusted * balance1Adjusted >= kLast * 1e6 — se kLast não for atualizado atomicamente com o swap, uma reentrância ou um desvio multi-bloco pode fazer a verificação passar quando não deveria.
  • Assimetria skim() vs sync(): skim() transfere o excesso para fora, enquanto sync() escreve o saldo superior para dentro. Um pool que permite ambos, chamados em sequência por um atacante, pode ser usado para manipular qual "verdade" o pool aceita.

4. Exploração Multi-Passo: Encadeando Desvio em Extração de Valor

Desvio isolado é geralmente apenas alguns wei. Exploits reais encadeiam múltiplas condições de desvio em blocos ou dentro de uma única transação para amplificar a lacuna. Um cenário multi-passo típico:

  1. Setup: Atacante pega emprestado uma grande posição de token via flashloan.
  2. Inflar: Atacante chama sync() para escrever uma doação em reservas, aumentando k.
  3. Swap: Atacante executa um swap no preço agora incorreto, que a verificação de invariante passa porque o k inflado cria mais liquidez aparente.
  4. Desinflar: Atacante chama skim() ou queima cotas de LP para recuperar tokens doados.
  5. Repagar: Flashloan reembolsado; lucro líquido da discrepância de preço.

A chave de insight para auditores: cada passo é individualmente válido. Nenhuma chamada única viola controle de acesso. O exploit é a sequência. É por isso que revisão de código manual sozinha é insuficiente — você precisa testar sequências, não funções individuais.

5. Escrevendo Harnesses de Invariante Foundry

O framework invariant de Foundry chama repetidamente sequências arbitrárias de suas funções handler, depois afirma propriedades globais. Para desvio de AMM, o invariante central é:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/UniV2Pair.sol";

contract InvariantAMMDrift is Test {
    UniV2Pair public pair;
    MockERC20 public token0;
    MockERC20 public token1;
    uint256 public kLast;

    function setUp() public {
        token0 = new MockERC20("T0", "T0", 18);
        token1 = new MockERC20("T1", "T1", 18);
        pair = new UniV2Pair(address(token0), address(token1));
        // Seed initial liquidity
        token0.mint(address(pair), 1e18);
        token1.mint(address(pair), 1e18);
        pair.mint(address(this));
        (uint112 r0, uint112 r1,) = pair.getReserves();
        kLast = uint256(r0) * uint256(r1);
    }

    // Handler: simulate direct donation
    function donateToken0(uint96 amount) public {
        token0.mint(address(pair), amount);
        // Do NOT call sync() — leave as latent drift
    }

    // Handler: execute a swap
    function swap(bool zeroForOne, uint96 amountIn) public {
        amountIn = uint96(bound(amountIn, 1, 1e17));
        (uint112 r0, uint112 r1,) = pair.getReserves();
        if (zeroForOne) {
            uint256 amountOut = (uint256(amountIn) * 997 * r1)
                / (uint256(r0) * 1000 + uint256(amountIn) * 997);
            token0.mint(address(pair), amountIn);
            pair.swap(0, amountOut, address(this), "");
        }
        // mirror for oneForZero...
    }

    // THE INVARIANT: k must never decrease
    function invariant_kNeverDecreases() public {
        (uint112 r0, uint112 r1,) = pair.getReserves();
        uint256 kNow = uint256(r0) * uint256(r1);
        assertGe(kNow, kLast, "k decreased: invariant drift detected");
        kLast = kNow;
    }

    // SECONDARY: reserves must never exceed balanceOf
    function invariant_reservesSolvent() public {
        (uint112 r0, uint112 r1,) = pair.getReserves();
        assertLe(r0, token0.balanceOf(address(pair)), "reserve0 exceeds balance");
        assertLe(r1, token1.balanceOf(address(pair)), "reserve1 exceeds balance");
    }
}

Executar com: forge test --match-contract InvariantAMMDrift --runs 10000 --depth 50

A flag --depth 50 diz a Foundry para gerar sequências de até 50 chamadas de handler antes de verificar invariantes. É aqui que cenários de desvio encadeado aparecem. Se o invariante falhar, o algoritmo de encolhimento de Foundry produzirá a sequência mínima que reproduz a falha — que é frequentemente exatamente o PoC que você precisa para seu relatório.

⚠️ Nota crítica de auditoria: também afirme que balanceOf >= reserve (não apenas igualdade), e teste separadamente sync() + skim() sequenciando como funções handler explícitas. Muitos forks têm a lacuna apenas em ordenações específicas de chamadas.


Alternativas & Comparação

AbordagemPontos FortesFraquezasMelhor Para
Fuzzing de invariante FoundryGeração automática de sequências, encolhimento, iteração rápidaRequer escrita de harness, espaço de busca limitadoAuditorias pré-deployment, regressão CI
EchidnaBaseado em propriedades, corpus-guided, integração SlitherSetup mais lento, curva de aprendizado mais íngremeCampanhas de auditoria de longa duração
Revisão manual de matemáticaDetecta erros estruturais de contabilidade de taxa rapidamenteNão consegue encontrar sequências multi-passo emergentesRevisão de arquitetura de primeira passagem
Verificação formal (Certora)Provadamente exaustivo para propriedades especificadasCaro, escrita de especificação requer expertiseDeployments de protocolo de alto valor
Fork + simulação mainnet (Hardhat/Anvil)Contratos de token reais, feeds de preço reaisLento, não-determinístico, difícil afirmar propriedadesValidando sequências de PoC conhecidas

Para a maioria das auditorias de AMM, a resposta certa é revisão manual para identificar candidatos de vetor de desvio, seguido de testes de invariante Foundry para confirmar e caracterizar deles. Verificação formal vale o investimento para protocolos gerenciando >$100M TVL.


Leitura Adicional & Referências

Achou este artigo interessante? Siga-me no X e no LinkedIn.