Damn Vulnerable DeFi: Truster

0
29

Warning: Solution to the challenge will be discussed here. Also note, I could be totally fucking wrong about everything I’m about to write. This is mostly just me long form “tweeting into the ether” about my smart contract journey. Proceed with caution.

Problem

More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free.
Currently the pool has 1 million DVT tokens in balance. And you have nothing.
But don’t worry, you might be able to steal them all from the pool.

This problem looks like it’s expecting us to drain the lender completely and walk away with a bunch of (or all of) the DVT tokens.

TrusterLenderPool.sol
The image of the TrusterLenderPool contract

This seems like it should’ve been the first problem posted! I could be wrong, but it seems like the issue here almost immediately jumps out target.call(data) , but I know the story of Icarus too well, so we are going to go through it anyways. Per usual, we see that OpenZeppelin at the top of the board, providing us the ERC20 Interface and the ReentrancyGuard which doesn’t really matter here I don’t think because we aren’t going to exploit this contract with reentrancy. We’re going to do it with the ERC20 Interface that’s pointing at the smart contract that manages the DVT tokens 😵.

constructor

When the TrusterLenderPoolcontract is initially deployed we are just going to create a reference to the contract address passed in by the person deploying, in this case, we’re expecting tokenAddress to point to the contract address that holds all of the DVT tokens.

Note: I’m going to go on a small tangent about ERC20 standard and why I think it’s important to this particular exploit.
totalSupply() - how many total tokens will be created and managed within this contract (also can think of this as the sum of all token balances)
balanceOf(address _owner) - how many tokens does the supplied address own
transfer(address _to, uint256 _value) - take the number of tokens specified in _value and record that value as being available for the address _to and it’s very important to remember that these are just state changes, no tokens are actually moving anywhere
transferFrom(address _from, address _to, uint256 _value) - same logic as the transfer method but instead of assuming that the state change is between the smart contract managing the ERC20 token it’s assuming that this is a value transfer happening between two actors where neither is the smart contract managing the ERC20 tokens (this is like sending tokens to your friends or maybe another contract where you manage your holdings across multiple ERC20 contracts)
approve(address _spender, uint256 _value) - in order to actually use the tokens you must call this function with the address of the account we want to authorize to spend from the DVT contract as well as specifying the actual amount we would like to approve for extraction. It’s like going to the bank and telling the teller, “Hey, I know I have $300 in my bank account, but I’d like you to reject any attempt to spend more than $100”. (This is how we exploit the contract, I believe but more on that later).
allowance(address _owner, address _spender) - allowance is the function that tells you just how much a particular account has been approved to pull from a particular users wallet or smart contract. In this case our _owner is the DVT contract and our _spender would be us. It’s also important to note that allowance and balanceOf can be mismatched.
One final thing to always remember about using ERC20 standards (and generally any standard) is that every single function is public and callable by anyone.

flashLoan

I don’t want to be too arrogant with this, but I’m increasingly convinced that the call data is the vulnerability here. The f(x) takes in 4 parameters for extending a flash loan, namely:

uint256 borrowAmount (how much money are we asking for with this loan)
address borrower (who is requesting the loan)
address target (who are we wanting to execute the call on)
bytes calldata data (what information do we want to send to target to perform the function)

This f(x) is again non reentrant (not that it’s important) and it requires an external accessor. Ok, cool. Immediately, the contract is going to check to see if the balance available in the contract is sufficient to satisfy the requested borrowAmount at which point we will automatically call the transfer method which will “send” the requested borrowAmount to our borrower and this sets us up for the trick. In line 27, we see that we attempt to have the target call a function with the data provided when the function is called.

Note: calldata (special data location that contains the function arguments, only available for external function call parameters), is usually used to encode functions for talking directly to the EVM. Which is this case is an indication that we should be sending an encoded function definition for the target to satisfy

So for this piece, we can’t just take the money immediately in the first run because the flashLoan f(x) is checking to make sure that the balance is restored at the end of the the call or all changes will end up being reverted and no one makes any money and now you’ve spent gas for nothing! So, what we can do here is instead of trying to steal the tokens, we can approve our address for the total balance of the DVT contract and circle back after our approval is successfully made and transfer all of the DVT tokens to myself.

Note: our balanceOf will be 0 when the function is done running but our allowance will be the total supply of tokens currently in the pool.

solution

We can use web3 or ethers libraries for creating our encoded f(x) which is simple enough it just allows for telling the EVM the exact bytecode for the function we’re looking for and the arguments we’d like to call it with (if any).

The top level view of the truster.js test file using web3
The top level view of the truster.js test file using ethers
The Exploit function within tester.js file using web3
The exploit function within tester.js file using ethers

The encodeFunctionCallis pretty standard, we are defining the entire function call with parameters and signature. The array at the end supplies the variables we’d like to encode and pass in as our inputs mapping attacker to the spender and TOKENS_IN_POOL to amount and then we call flashLoan supplying nothing for the borrowAmount, so we don’t have to pay anything back to satisfy the balance checks made in the contract, we want to set our attacker as the borrower and our target that we want to make the function call is the address for our DVT smart contract and finally we pass in our data which is our manually constructed f(x) call to have the DVT contract call approve on itself with attacker in place of spender and TOKENS_IN_POOL being the total amount we are approved to transfer out in the next line with transferFrom — and just like that we’ve drained it.

reflection

The main thing here is just being careful with the passing in calldata as well as being careful about specificying who can actually call a function and who they can have call the function. There wasn’t really any reason I can see to even include a target here within the flashLoan piece it wasn’t really necessary to enable the sending of the loans, and even more so it didn’t even really matter who we set as the borrower since attacker is being mapped to spender within data , so in theory we could’ve called flashLoan with any address we wanted as borrower and it would have no bearing on the success of this exploit as long as the proper address for the attacker is encoded in data.

conclusion

This was a bit clearer for me to solve, but I’m not sure if it’s because I’m getting better or if I just happened to already have the requisite knowledge of encoding function calls to see a clear error here. Whenever I see flash loans in the wild, I typically see a calldata piece that allows users to specify function calls but this seems like something you’d want to do from the borrower side and not the DVT smart contract side, so that we aren’t having the DVT contract calling public functions on itself like that, but I’m going to spend a lot more time investigating the positive use cases for a pattern like this and how to better defend against it.

Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing

Also, Read


Damn Vulnerable DeFi: Truster was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.