Hacking Ethereum Damn Vulnerable DeFi (Part I)

The purpose of this post is to explain how to learn offensive security of DeFi smart contracts in Ethereum blockchain. First challenge was about stop service, named «unstoppable» contract. There’s a lending pool with a million DVT tokens in balance, offering flash loans for free. If only there was a way to attack and stop the pool from offering flash loans. Probably we should either stop the system from working. We start with 100 DVT tokens as attacker. Last post, I was performed a source code audit finding critical/high issues without explain much details about how deploy smart contract or explain Solidity source code and how use Brownie and Ganache commands/GUI.

Smart Contract’s Technical Review

Understand smart contract code is important to find critical/high issues. First of all, contract has to be deployed using requirements from challenge description. Requirements are described into web3.js script code in github repository. Directly I translate from javascript code and deploy manually using Brownie. Therefore, mint ERC20 token’s is needed. Constructor isn’t pass any parameters, so using localhost testnet with Brownie and Ganache will be our environment. When smart contract is deployed, minter is msg.sender and mint uint256 type unit maximum value.

Next, we deployed UnstoppableLender smart contract. Constructor needs one parameter, just address of ERC20 token. It’s needed approve of tokens in pool contract and transfer token from sender. Sender must have first approved them. Finally a transfer to attacker account is performed according initial requirements. In depositTokens function the possible values of poolBalance variable ranges from 0 to 2 ^ 256. So, it means that if you are around the max value and increment your variable, it will go back to 0. The same happens if your variable is at 0, and you subtract one, instead of overflow it is called underflow. The SafeMath library also protects contracts for possible integer overflows or underflows.

We now demonstrate whether any user take out a flash loan using receiver contract.

Brownie commands (Deploy and exploit): 
minter = accounts[0]
DamnValuableToken.deploy({'from': minter})
token = DamnValuableToken[-1]
token.balanceOf(minter) 
UnstoppableLender.deploy(token, {'from' : minter})
pool = UnstoppableLender[-1]
token.approve(pool, 1000000, {'from' : minter})
pool.depositTokens(1000000, {'from' : minter})
attacker = accounts[1]
token.transfer(attacker, 100, {'from' :minter})
token.balanceOf(pool)
token.balanceOf(attacker)
user = accounts[2]
ReceiverUnstoppable.deploy(pool, {'from' : user})
receiverContract = ReceiverUnstoppable[-1]
receiverContract.executeFlashLoan(10, {'from' : user})
token.transfer(pool, 1, {'from': attacker})
receiverContract.executeFlashLoan(10, {'from' : user}) //DoS

Receiver contract is provided by developers (for all user that wants flash loan). This contract we can perform a flash loan using flashLoan function from UnstoppableLender contract. Using receiveTokens function pool will call this during flash loan process and return all tokens to the pool with transfer() function.

Before explain following function, depositTokens() was used to transfer tokens to pool and poolBalance stores total value. But there is a way performing token transfer if we use token.transfer in Brownie.

When flashLoan is executed first check if borrowamount variable is higher than 0, so must deposit at least one token.
Next get balance of pool account (smart contract) using balancebefore variable and check if balance is higher than borrowamount. Asserting poolBalance variable is equal to balancebefore. There is a problem with assert() due to someuser can abuse token.transfer increasing balance and take control of balancebefore variable. Therefore depositTokens function didn’t use, poolBalance didn’t increase however balancebefore was. Asserting differents values lead an error and denial of service can occur. Critical issue was discover because assert() function when false, uses up all the remaining gas and reverts all the changes made. Therefore always will be false and reverts transaction.

There isn’t a way to solve the problem because poolBalance variable is used through depositTokens function call and balance values of both variables (balancebefore and poolBalance) will always be different.

Next post will be resolution of Naive Receiver challenge, stay tuned! Regards.