WEB3DEV

Cover image for Desafios Damn Vulnerable DeFi: #1 Unstoppable
Panegali
Panegali

Posted on

Desafios Damn Vulnerable DeFi: #1 Unstoppable

Damn Vulnerable DeFi (Maldita DeFi Vulnerável) é uma série de jogos CTF (Capture the flag ou Capturar o sinalizador) onde o jogador deve encontrar uma vulnerabilidade para quebrar ou roubar um protocolo.
Esses jogos educativos são muito interessantes no sentido de que eles imitam aplicativos reais (flashloans, pool, yield, …), permitindo-nos aprender não apenas sobre segurança na web3, mas também o que é DeFi.
Link aqui: https://www.damnvulnerabledefi.xyz/index.html
Nesta série de artigos, apresentarei os desafios e suas soluções.

#1 Unstoppable (Imparável)

Há um pool de empréstimo com um milhão de tokens DVT de saldo, oferecendo empréstimos relâmpagos gratuitamente.
Se ao menos houvesse uma maneira de atacar e impedir que o pool oferecesse empréstimos relâmpagos...
Você começa com 100 tokens DVT de saldo.
Impede o pool de oferecer empréstimos relâmpagos.

Descrição do protocolo

Em primeiro lugar, precisamos entender como funciona o protocolo.

O protocolo é composto por 3 contratos principais:

  • DamnValuableToken.sol \ Este é o Token ERC-20 da série de desafios: DVT. Este é um ERC-20 muito simples, sem recursos adicionais. Na construção, o max(uint256) é cunhado para o msg.sender.
  • UnstoppableLender.sol \ Este contrato oferece flashloans (empréstimos relâmpagos) para outros contratos que obedecem a uma interface específica, chamando esses contratos por meio de um método receiveToken.
  • ReceiverUnstoppable.sol \ Este é um contrato pronto para uso capaz de interagir com o UnstoppableLender que já implementa o método receiveToken.

Claramente, o que precisamos verificar aqui é o pool de empréstimos, pois é esse que o desafio nos pede para quebrar:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

interface IReceiver {
    function receiveTokens(address tokenAddress, uint256 amount) external;
}

/**
 * @title UnstoppableLender
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract UnstoppableLender is ReentrancyGuard {

    IERC20 public immutable damnValuableToken;
    uint256 public poolBalance;

    constructor(address tokenAddress) {
        require(tokenAddress != address(0), "O endereço do Token não pode ser zero");
        damnValuableToken = IERC20(tokenAddress);
    }

    function depositTokens(uint256 amount) external nonReentrant {
        require(amount > 0, "Deve depositar pelo menos um token");
        // Token de transferência do remetente. O remetente deve tê-los aprovado primeiro.
        damnValuableToken.transferFrom(msg.sender, address(this), amount);
        poolBalance = poolBalance + amount;
    }

    function flashLoan(uint256 borrowAmount) external nonReentrant {
        require(borrowAmount > 0, "Deve tomar emprestado pelo menos um token");

        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Não há tokens suficientes no pool");

        // Garantido pelo protocolo através da função `depositTokens`
        assert(poolBalance == balanceBefore);

        damnValuableToken.transfer(msg.sender, borrowAmount);

        IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);

        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "O empréstimo relâmpago ainda não foi pago");
    }
}
Enter fullscreen mode Exit fullscreen mode

Notas rápidas após a primeira leitura:

  1. O contrato usa solidity >0.8.0, onde overflow/underflow agora são verificados durante a compilação.
  2. Ele usa a biblioteca OpenZeppelin ReentrancyGuard e a usa em todas as funções, nenhum ataque de reentrância deve ser possível.
  3. Existe uma função de depósito que permite que qualquer pessoa envie DVT para o pool para aumentar seu saldo.
  4. Múltiplas verificações são feitas durante a execução do método flashLoan.

Se pensarmos no desafio, somos solicitados a impedir que o pool de credores ofereça empréstimos relâmpagos. Isso pode ser feito drenando todos os seus fundos ou quebrando sua lógica de verificação.

Solução

Se quebrarmos a lógica da função, temos isso:

  1. require(borrowAmount>0) \ Não podemos pedir um empréstimo relâmpago de 0.
  2. balanceBefore = DVT.balanceOf(address(this)) \ A função armazena o saldo, conforme contabilizado pelo contrato de DVT em uma variável chamada balanceBefore.
  3. require(balanceBefore >= borrowAmount) \ Não podemos emprestar mais do que o pool realmente possui, a transferência falharia de qualquer maneira na linha 42.
  4. assert(poolBalance == balanceBefore) \ Assert é um tipo especial de verificação, mas no final se comporta como um request e reverte toda a execução (mais detalhes aqui). \ E aqui está nossa vulnerabilidade.

O que acontece se eu transferir alguns tokens DVT para o pool sem usar sua função deposit?

Sim, enquanto seu saldo no contrato DVT aumentará, sua própria variável interna poolBalance, que deve ser sempre igual a damnValuableToken.balanceOf(address(this))será distorcida pelo valor transferido, e a assertiva sempre falhará.

Para fazer isso, tudo o que precisamos fazer é abrir o arquivo challenge.js correspondente e adicioná-lo dentro da seção Exploit:

 it('Exploit', async function () {
        /** CODIFIQUE SEU EXPLOIT AQUI */
        await this.token.connect(attacker).transfer(this.pool.address, 10);
    });
Enter fullscreen mode Exit fullscreen mode

Se você executar o teste usando:

>npx hardhat test .\test\unstoppable.challenge.js
Enter fullscreen mode Exit fullscreen mode

O teste deve passar, o que significa que você resolveu o desafio.

Etapas de mitigação recomendadas

Uma maneira fácil de corrigir essa vulnerabilidade seria remover a variável poolBalancee a asserção desnecessária. O contrato DVT já mantém o registro de cada saldo.

Espero que tenham gostado deste artigo e nos vemos na próxima vez para um novo desafio!


Artigo escrito por Salah I e traduzido por Marcelo Panegali.

Oldest comments (0)