Damn Vulnerable DeFi V4: Truster
Overview
Hello everyone! Hope you are doing great!
I’m back again with a new Blockchain/Web3 by tackling the third challenge from the Damn Vulnerable DeFi series created by The Red Guild where I will be explaining the challenge in depth, my approach to solving it and my solutions.
Hope you enjoy it and learn something new!
Check the previous Damn Vulnerable DeFi challenge called Naive Receiver from this blog post. Enjoy!
‘Truster’ challenge

Challenge Description
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.
Link to the original challenge
Github repo link that contains challenge code and solver
Understanding the contract
Overview
We have 2 main contracts:
DamnValuableToken.sol
This is the definition contract of the Damn Valuable Token (DVT): an ERC20() token that will be the currency (token) in this project.
We can see that this token has 18 decimals
1 | contract DamnValuableToken is ERC20 { |
TrusterLenderPool.sol
This is a smart contract that offers Flash Loans through the TrusterLenderPool::flashLoan() method.
- First we fix the contract balance prior to supplying the flashloan to the borrower:
1 | uint256 balanceBefore = token.balanceOf(address(this)); |
- Next we will transfer the tokens to the borrower wallet:
1 | token.transfer(borrower, amount); |
- Now the contract allows the borrower to execute a function call through the OpenZeppelin
Address::functionCall():
1 | target.functionCall(data); |
We can see below the definition of the method:
1 | function functionCall(address target, bytes memory data) internal returns (bytes memory) { |
What this basically does is that it will take a target contract address and some calldata, then it will execute the bytes function call from within the calldata as a low level call. Providing direct arbitrary method execution.
- Finally we will make a check that the borrower actually returned the loaned amount to the contract:
1 | if (token.balanceOf(address(this)) < balanceBefore) { |
Point of Failure
What directly stands out from the contract is the usage of functionCall() applied on a user-supplied target contract. This represents a very serious issue as the user can invoke whatever method they want (by encoding the method ABI and executing it on the target as a low level call.). This will allow the borrower to approve token spending from the contract and then steal all the tokens after the flash loan transaction finishes.
Exploitation
Our main goal from the challenge is to rescue all the tokens and send them to the recovery account wallet.
The steps to do this are clear:
- Create calldata that will be used to approve coins spending BY the contract itself, because it is the owner of the contract and the only one that can allow spending:
1 | bytes memory data=abi.encodeWithSignature("approve(address,uint256)", address(this),p_token.balanceOf(address(p_pool))); |
This will encode the approve method which approves the spending of the entire contract balance: p_token.balanceOf(address(p_pool)) to the malicious contract: address(this)
- Next, we will request a flash loan with 0 DVT, this is to pass the
if (token.balanceOf(address(this)) < balanceBefore)check. We will pass the current exploit contract as the borrower and theTrustercontract as the target contract (so the approval happens through it and gets validated.):
1 | p_pool.flashLoan(0,address(this),address(p_token),data); //this doesn't take any money from the contract |
- Finally, after the flashloan transaction finishes, we will find ourselves with the spending of the entire contract balance approved. So we can just send all the tokens to the recovery challenge and successfully rescue them:
1 | p_token.transferFrom(address(p_pool),p_recovery,p_token.balanceOf(address(p_pool))); |
Exploit Test Case
Below you can find the full test case.
First, the exploitation contract (because we have to call a contract inside the functionCall method):
1 | contract TrusterExploit { |
And the call from inside the test case:
1 | function test_truster() public checkSolvedByPlayer { |
This takes the checkSolvedByPlayer modifier behavior which performs these checks:
1 | assertEq(vm.getNonce(player), 1, "Player executed more than one tx"); |
The output is as follows:
1 | $ forge test -vv |
And Tadaaa! Solved!
Conclusion
That was it for the Truster challenge from Damn Vulnerable DeFi series.
You can find through this github link the repository that contains my solver and all the future Damn Vulnerable DeFi solutions Inshallah!
See you next time~
- Title: Damn Vulnerable DeFi V4: Truster
- Author: Foued SAIDI
- Created at : 2025-12-31 16:41:32
- Updated at : 2025-12-31 16:54:26
- Link: https://kujen5.github.io/2025/12/31/Damn-Vulnerable-DeFi-V4-Truster/
- License: This work is licensed under CC BY-NC-SA 4.0.