Truster - Damn Vulnerable DeFi #03

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);. The functionCall() 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 the flashLoan() function, the lender contract makes an external call to the target address with our custom data passed as an argument. This paves the way for exploitation.

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 the target contract. We will be making use of abi.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 approve poolBalance 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.