Truster - Damn Vulnerable DeFi #03
Analysis and PoC of Damn Vulnerable DeFi Level 03 - Truster
Objectives
There's a lending pool with a million DVT tokens. This pool offers a flash loan for free. But as it is with all flash loans, the user must pay back the loan in the same transaction.
Our objective is to drain all the funds from the lending pool and empty the contract. Let's get started.
Smart Contract Analysis
TrusterLenderPool.sol
The only function of interest here is the flashLoan()
. Let's go over the content.
balanceBefore
is used to store the current balance of the pool which is then validated again on Line 39 to make sure that the user returns the loaned token.There's a transfer call on Line 35 in which the loan is offered to the
borrower
.On line 36, there's an interesting external call -
target.functionCall(data);
. ThefunctionCall()
is a function coming from OpenZeppelin's Address.sol. This is calling another internal function called `functionCallWithValue()`.- If you look inside the function definition, you'll notice that it is just a normal
call()
function on the target address, i.e.,target.call{value: value}(data);
. This means that when the user calls theflashLoan()
function, the lender contract makes an external call to thetarget
address with our custom data passed as an argument. This paves the way for exploitation.
- If you look inside the function definition, you'll notice that it is just a normal
The Exploit
Now that we know we can make the lender pool call arbitrary functions on the target
address, imagine what would happen if we were to pass the target
as the address of the lender pool's token itself? We don't see any validation happening in the flashLoan()
function. This would essentially allow us to call any function inside the lender pool and its ERC interfaces in the context of the pool.
Now, to transfer all the tokens from the pool, there's a function called approve(spender, amount)
in the ERC20 standard. This function is used to allow the spender to spend the amount
tokens owned by the caller.
Since we can make the lender pool call any function, we will be using the exploit to make it call the approve()
with our attacker's address and the maximum amount in the lending pool.
Once the tokens are approved by the lender, we can simply withdraw the tokens by calling transferFrom()
, yet another ERC20 function.
borrowAmount
will be 0 since we don't want to pay back anything.borrower
will be our attacker's address.target
will be the address of the damnValuableToken.data
will contain the ABI-encoded data containing the function signature and values to call on thetarget
contract. We will be making use ofabi.encodeWithSignature
for this.
We'll make an attacker contract so that the exploit happens in a single transaction.
This contract code can be placed just below the TrusterLenderPool contract code. In the attacker contract:
The constructor is handling the pool and token address which we'll pass from inside the test file.
The attack function is storing the total pool balance inside
poolBalance
.On line 12,
pool.flashLoan
is called with all the parameters specified above. This will make the lender pool approvepoolBalance
tokens for our attacker contract.On line 22, the approved tokens are transferred back to our contract completing the attack in a single transaction.
Here's how the test case will look:
Lines 4 and 5 are handling our contract deployment and on line 6 we are calling the attack()
function that we created to exploit the lending pool.
Run the script using yarn run truster
and the test case will pass.
Key Takeaways
It's a bad idea to pass user-controlled input directly to function call data without validation.
Do not allow 0 amount in flash loans.
There should have been validations on the
target
address to prevent users from using the token or pool's address.