Skip to content

Commit 81cff9e

Browse files
committedJul 4, 2023
feat: add Oracle Manipulation Attack
1 parent 53b6d48 commit 81cff9e

File tree

10 files changed

+631
-1
lines changed

10 files changed

+631
-1
lines changed
 

‎README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# 分散型アプリケーション脆弱性解析ゼミ
22

3-
IPAが主催する[セキュリティ・キャンプ全国大会2023](https://www.ipa.go.jp/jinzai/security-camp/2023/zenkoku/index.html)の『分散型アプリケーション脆弱性解析ゼミ』の紹介です。
3+
[セキュリティ・キャンプ全国大会2023](https://www.ipa.go.jp/jinzai/security-camp/2023/zenkoku/index.html)の『分散型アプリケーション脆弱性解析ゼミ』の紹介と講義資料を置くリポジトリです。
4+
5+
講義資料はこちら: [course](course)
6+
7+
以下、応募開始時点で公開したゼミの紹介です(エントリー期間は終了しました)。
8+
9+
---
410

511
## 概要
612

‎course/README.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# seccamp2023 L4 講義資料
2+
3+
随時公開します。
4+
5+
## 一覧
6+
1. [Foundryの紹介](foundry)
7+
2. [Ethernautの紹介](ethernaut)
8+
3. [Reentrancy Attack](reentrancy)
9+
4. [Oracle Manipulation Attack & Flash Loan](oracle-manipulation)
10+
5. ...

‎course/oracle-manipulation/README.md

+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# Oracle Manipulation Attack & Flash Loan
2+
3+
**目次**
4+
- [オラクルとは](#オラクルとは)
5+
- [Oracle Manipulation Attackとは](#oracle-manipulation-attackとは)
6+
- [Oracle Manipulation Attackの具体例](#oracle-manipulation-attackの具体例)
7+
- [演習](#演習)
8+
- [Oracle Manipulation Attackの対策](#oracle-manipulation-attackの対策)
9+
- [Time Weighted Average Pricing (TWAP)](#time-weighted-average-pricing-twap)
10+
- [非中央集権型オラクル](#非中央集権型オラクル)
11+
- [Flash Loanとは](#flash-loanとは)
12+
- [演習](#演習-1)
13+
- [Flash Loanを利用したOracle Manipulation Attack](#flash-loanを利用したoracle-manipulation-attack)
14+
- [演習](#演習-2)
15+
16+
## オラクルとは
17+
18+
Oracle Manipulation Attackの説明をする前に、簡単にオラクル(oracle)について説明します。
19+
20+
ブロックチェーンにおけるオラクルとは、狭義には、オンチェーン(= チェーン内)のスマートコントラクトに、オフチェーン(= チェーン外)の情報を提供するプロトコルのことです。
21+
広義には、オンチェーンだけで収集できる情報を加工して提供する関数やコントラクトもオラクルと呼びます。
22+
23+
ブロックチェーンの仕組み上、コントラクトはブロックチェーンの外で起こっている情報を得るために、オフチェーンの主体に依存する必要があります。
24+
例えば、現実世界の天気や株式の価格などは、ブロックチェーン上に存在しないため、誰かが提供する必要があります。
25+
これをオラクル問題と呼びます。
26+
27+
オラクル問題を非中央集権的に解決するために、オフチェーンの情報をオンチェーンに非中央集権的にコミットするプロトコルがあり、非中央集権型オラクル(Decentralized Oracle)と呼ばれます。
28+
代表的な非中央集権型オラクルのプロトコルに[Chainlink](https://chain.link/)があります。
29+
非中央集権型オラクルについては、後で詳しく説明します。
30+
31+
## Oracle Manipulation Attackとは
32+
33+
Oracle Manipulation Attackとは、オラクルを故意に操作することでオラクルを利用するプロトコルからトークンの奪取などを行う攻撃の総称です。
34+
特に、オラクルの広義の意味に含まれる「オンチェーンだけで収集できる情報を加工して提供する関数やコントラクト」は適切な利用を行わないと、この種の攻撃に脆弱になりやすいです。
35+
36+
## Oracle Manipulation Attackの具体例
37+
38+
Oracle Manipulation Attackの単純な例を説明します。
39+
40+
まず、ユーザー、レンディングプロトコル、オラクルの3つのパーティーがいるとしましょう。
41+
ユーザーは、レンディングプロトコルにいくらかのETHをデポジットすれば、ある閾値までのUSDCを借りることができます。
42+
レンディングプロトコルは、USDC/ETHの価格を提供するオラクルを利用して、その閾値を決定します。
43+
オラクルは具体的には、UniswapなどのAMMを想定してもらって構いません。
44+
45+
悪意のない一般ユーザーがレンディングプロトコルから資産を借りる流れは以下の図のようになります。
46+
(通常の矢印がアクションで、点線の矢印が単なる返り値を表しています。)
47+
48+
```mermaid
49+
sequenceDiagram
50+
participant User
51+
participant Lending
52+
participant Oracle
53+
54+
User ->> Lending: 10 ETHデポジット
55+
User ->>+ Lending: 15,000 USDC貸して
56+
Lending ->>+ Oracle: USDC/ETHの値は?
57+
Oracle -->>- Lending: 2,000 USDC/ETH
58+
Note over Lending: Collateral Factor (75%)による条件チェック<br>2,000 * 10 * 0.75 = 15,000 USDC
59+
Lending ->>- User: 15,000 USDC送金
60+
```
61+
62+
さて、ここでオラクルを操作することで何か攻撃を行うことはできないでしょうか?
63+
64+
もしオラクルが提供する価格を操作できるとしましょう。
65+
そうすると、本来借りれるはずの額よりも大きな額のUSDCを借りれてしまいます。
66+
例えば、USDC/ETHの価格が現在の倍の4,000 USDC/ETHに出来たら、30,000 USDC借りることが出来ます。
67+
攻撃者はそのUSDCを返さなければ、結果として、30,000 USDC - 2,000 USDC/ETH * 10 ETH = 10,000 USDC を利益にできてしまいます。
68+
69+
図に表すと以下の流れになります。
70+
71+
```mermaid
72+
sequenceDiagram
73+
participant Attacker
74+
participant Lending
75+
participant Oracle
76+
77+
Attacker ->> Oracle: (何かしらの操作)
78+
Attacker ->> Lending: 10 ETHデポジット
79+
Attacker ->>+ Lending: 30,000 USDC貸して
80+
Lending ->>+ Oracle: USDC/ETHの値は?
81+
Oracle -->>- Lending: 4,000 USDC/ETH
82+
Note over Lending: Collateral Factor (75%)による条件チェック<br>4,000 * 10 * 0.75 = 30,000 USDC
83+
Lending ->>- Attacker: 30,000 USDC送金
84+
```
85+
86+
それでは、オラクルへの具体的な操作は何が考えられるでしょうか?
87+
88+
オラクルの実態は取引所なので、ETHを大量に買い上げればUSDC/ETHの値が上がるでしょう。
89+
90+
その取引所の価格決定アルゴリズムが $xy = k$ 型で、USDC-ETHプールにあるUSDCとETHの総量がそれぞれ、20,000,000 USDCと10,000 ETHだとします。
91+
もし2,000 USDC/ETHから4,000 USDC/ETHになったときのETHの量を $x$ とすれば、 $4000 x^2 = 20000000 \times 10000$ を満たします。
92+
$x$ を求めると $7071$ 程度になります。
93+
一方でUSDCの量は、 $4000x = 28284000$ 程度です。
94+
つまり、8,284,000 USDCを所持していれば価格を4,000 USDC/ETHに釣り上げることが可能です。
95+
96+
USDC-ETHプールの交換手数料を0.3 %とすると、ざっくりと8,284,000 USDC * 0.3 % * 2回 = 49,704 USDCの手数料がかかります。
97+
攻撃者の利益はレンディングプロトコルが持つUSDCの総量の1/3であるため、その総量が約49,704 * 3 = 149,112 USDC以上であれば、攻撃者は利益を得られることになります。
98+
99+
最終的に残ったETHをUSDCに戻せば攻撃完了です。
100+
101+
この攻撃の流れを図に表すと次のようになります。
102+
8,284,000や7,071のような数値は $x,y$ などの記号にしています。
103+
104+
105+
```mermaid
106+
sequenceDiagram
107+
participant Attacker
108+
participant Lending
109+
participant AMM
110+
111+
Attacker ->>+ AMM: x USDCをETHに交換して
112+
AMM ->>- Attacker: y ETH送金
113+
Attacker ->> Lending: 100 ETHデポジット
114+
Attacker ->>+ Lending: 300,000 USDC貸して
115+
Lending ->>+ AMM: USDC/ETHの値は?
116+
AMM -->>- Lending: 4,000 USDC/ETH
117+
Note over Lending: Collateral Factor (75%)による条件チェック<br>4,000 * 100 * 0.75 = 300,000 USDC
118+
Lending ->>- Attacker: 300,000 USDC送金
119+
Attacker ->>+ AMM: y' ETHをUSDCに交換して
120+
AMM ->>- Attacker: x' USDC送金
121+
```
122+
123+
この攻撃では、一つ攻撃者にとって問題点があります。
124+
それは、攻撃者が8,284,000 USDCを用意しなくてはならない点です。
125+
しかし、後述するFlash Loanと組み合わせることで、攻撃者は無一文でも攻撃を行うことができます(厳密にはトランザクション手数料分のETHを保持している必要があります)。
126+
127+
### 演習
128+
129+
問題ディレクトリ: [challenge-oracle-manipulation](challenge-oracle-manipulation)
130+
131+
プレイヤーは`A`トークンを初期状態で9,000,000持っています。
132+
`Challenge.sol``LendingPool`コントラクトに対してOracle Manipulation Attackを行い、`A`トークンを全て排出して、プレイヤーのトークン`A`の総量を9,100,000以上にしてください。
133+
134+
以下のコマンドを実行して、テストがパスしたら成功です。
135+
136+
```
137+
forge test -vvv --match-path course/oracle-manipulation/challenge-oracle-manipulation/Challenge.t.sol
138+
```
139+
140+
## Oracle Manipulation Attackの対策
141+
142+
Oracle Manipulation Attackの対策として、「Time Weighted Average Pricing (TWAP)」と「非中央集権型オラクル」の2つの技術を紹介します。
143+
144+
### Time Weighted Average Pricing (TWAP)
145+
146+
TWAPは、複数のブロックの価格の平均を取ることで価格を決定するアルゴリズムです。
147+
TWAPはUniswapなどのプロトコルで提供されています。
148+
149+
例えば、ある時点 $i$ での価格を $P_i$ とすると、 $[a,a+1,\ldots,b-1]$ のTWAPは次のように表せます。
150+
151+
$$\mathrm{TWAP} = \frac{P_{a}+P_{a+1}+\cdots + P_{b-1}}{b-a}$$
152+
153+
このTWAPを導入することで、先程紹介したようなOracle Manipulation Attackを防ぐことができます。
154+
155+
まず、TWAPを導入すれば、現在のトランザクション(あるいはブロック)の時点での価格だけ操作しても、価格を適正価格から大きく乖離できずに攻撃が失敗します。
156+
具体的には、ある時点での価格操作の影響が $\frac{1}{b-a}$ になってしまいます。
157+
158+
次に、そもそも現在のブロックの価格がTWAPの計算に含まれないようなTWAPの場合は、そもそも価格への影響はありません。
159+
160+
さらに、攻撃を2つ以上のトランザクションに分けてTWAPを操作しようとしても、すぐに他のユーザーによるアービトラージが行われ、適正価格に修正されてしまいます。
161+
適正価格に修正されると、何度も不利な価格でスワップすることになります。
162+
結局、そのような攻撃は非常に高いコストがかかるため、攻撃しても損するだけでインセンティブがありません。
163+
164+
特に、Oracle Manipulation Attackは後述するFlash Loanと組み合わせられることがほとんどであり、Flash Loanを利用すると1トランザクションで攻撃を完結させないといけないため、TWAPは強力な対策の一つです。
165+
166+
また、TWAPは価格決定アルゴリズムの中では非常にシンプルであるため、オンチェーンで実装する上で相性が良いというメリットもあります。
167+
168+
ただし、急激な価格変化にすぐに追いつけないという性質はあります。
169+
170+
### 非中央集権型オラクル
171+
172+
Chainlinkなどは、複数のある程度信頼できるパーティーからの価格データを収集し、そのデータをオンチェーンにコミットするプロトコルを非中央集権的に運用しています。
173+
この非中央集権型オラクルを利用することでも、先程紹介したようなOracle Manipulation Attackを防ぐことがきます。
174+
175+
Chainlinkのノードがオンチェーンに価格データをコミットしなくてはならないため、チェーンが混雑しているときは、価格更新トランザクションがすぐに実行されない可能性があります。
176+
177+
## Flash Loanとは
178+
179+
Flash Loanとは、トランザクションの終了までに借りた資産が返却される限り、無担保で資産を借入できるローンのことです。
180+
借り手は、そのトランザクション内で借りた資産をどのように扱っても良いです。
181+
手数料は発生しますが、借りた期間に基づく利子はありません。
182+
183+
Flash LoanはUniswapを始めとする様々な取引所で提供されています。
184+
185+
例えば、10,000 WETHを借りるFlash Loanは次のようなイメージです。
186+
187+
```mermaid
188+
sequenceDiagram
189+
participant User
190+
participant FlashLoanProvider
191+
192+
User ->>+ FlashLoanProvider: 10,000 WETH借して
193+
FlashLoanProvider ->>+ User: 10,000 WETH
194+
Note over User: 任意の処理
195+
User ->>- FlashLoanProvider: (10,000 + fee) WETH
196+
FlashLoanProvider -->>- User: (Flash Loan終了)
197+
```
198+
199+
### 演習
200+
201+
問題ディレクトリ: [challenge-flash-loan](challenge-flash-loan)
202+
203+
Uniswap V2のフラッシュローン(Flash Swapと呼びます)を使って、`Flag`コントラクトの`solved`フラグを立ててください。
204+
Flash Swapの使い方は、[Uniswapのドキュメント](https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/using-flash-swaps)を参照してください。
205+
206+
今までの演習問題と異なり、メインチェーンをフォークしていることに注意してください。
207+
208+
Flash Loanを行う際に使えるアドレスを参考までに載せておきます(これらアドレスを使わなくても構いません)。
209+
- WETHのアドレス: `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2`
210+
- USDC-WETH Pairのアドレス: `0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc`
211+
212+
以下のコマンドを実行して、テストがパスしたら成功です。
213+
214+
```
215+
forge test -vvv --match-path course/oracle-manipulation/challenge-flash-loan/Challenge.t.sol
216+
```
217+
218+
## Flash Loanを利用したOracle Manipulation Attack
219+
220+
最初の紹介したOracle Manipulation Attackでは、攻撃者が初期状態で大量のUSDCを持っている必要がありました。
221+
しかし、今紹介したFlash Loanを利用することで、攻撃に必要な資産を準備する必要はもうありません。
222+
223+
Flash Loanと組み合わせたときのOracle Manipulation Attackは次のようなイメージになります。
224+
225+
226+
```mermaid
227+
sequenceDiagram
228+
participant FlashLoanProvider
229+
participant Attacker
230+
participant Lending
231+
participant AMM
232+
233+
Attacker ->>+ FlashLoanProvider: x USDC貸して
234+
FlashLoanProvider ->>+ Attacker: x USDC送金
235+
Attacker ->>+ AMM: x USDCをETHに交換して
236+
AMM ->>- Attacker: y ETH送金
237+
Attacker ->> Lending: 100 ETHデポジット
238+
Attacker ->>+ Lending: 300,000 USDC貸して
239+
Lending ->>+ AMM: USDC/ETHの値は?
240+
AMM -->>- Lending: 4,000 USDC/ETH
241+
Note over Lending: Collateral Factor (75%)による条件チェック<br>4,000 * 100 * 0.75 = 300,000 USDC
242+
Lending ->>- Attacker: 300,000 USDC送金
243+
Attacker ->>+ AMM: y' ETHをUSDCに交換して
244+
AMM ->>- Attacker: x' USDC送金
245+
Attacker ->>- FlashLoanProvider: (x + fee) USDC送金
246+
FlashLoanProvider -->>- Attacker: (Flash Loan終了)
247+
```
248+
249+
### 演習
250+
251+
問題ディレクトリ: [challenge-oracle-manipulation-with-flash-loan](challenge-oracle-manipulation-with-flash-loan)
252+
253+
最初の演習と異なり、プレイヤーは`A`トークンを初期状態で所持していません。
254+
今回の問題でもメインネットをフォークしています。
255+
それに加えて、`A`トークンに`USDC`が、`B`トークンに`WETH`が割り当てられています。
256+
257+
`Challenge.sol``LendingPool`コントラクトに対して、Flash Loanを用いたOracle Manipulation Attackを行い`USDC`を全て排出して、プレイヤーの`USDC`の総量を100,000以上にしてください。
258+
259+
以下のコマンドを実行して、テストがパスしたら成功です。
260+
261+
```
262+
forge test -vvv --match-path course/oracle-manipulation/challenge-oracle-manipulation-with-flash-loan/Challenge.t.sol
263+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+
contract Flag {
7+
ERC20 immutable weth;
8+
bool public solved = false;
9+
10+
constructor(ERC20 weth_) {
11+
weth = weth_;
12+
}
13+
14+
function solve() external {
15+
require(weth.balanceOf(msg.sender) >= 1_000 ether, "not enough WETH");
16+
solved = true;
17+
}
18+
}
19+
20+
contract Setup {
21+
ERC20 public weth = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
22+
bool claimed = false;
23+
Flag public flag;
24+
25+
constructor() {
26+
flag = new Flag(weth);
27+
}
28+
29+
function isSolved() public view returns (bool) {
30+
return flag.solved();
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "forge-std/Test.sol";
5+
import "./Challenge.sol";
6+
7+
contract ChallengeTest is Test {
8+
Setup setup;
9+
address public playerAddress;
10+
11+
function setUp() public {
12+
vm.createSelectFork("mainnet", 17600000);
13+
14+
playerAddress = makeAddr("player");
15+
vm.deal(playerAddress, 4 ether);
16+
setup = new Setup();
17+
}
18+
19+
function testExploit() public {
20+
vm.startPrank(playerAddress, playerAddress);
21+
22+
////////// YOUR CODE GOES HERE //////////
23+
24+
////////// YOUR CODE END //////////
25+
26+
assertTrue(setup.isSolved(), "challenge not solved");
27+
vm.stopPrank();
28+
}
29+
}
30+
31+
////////// YOUR CODE GOES HERE //////////
32+
33+
////////// YOUR CODE END //////////
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import "forge-std/console.sol";
6+
7+
contract AMM {
8+
ERC20 public immutable tokenA;
9+
ERC20 public immutable tokenB;
10+
11+
constructor(address tokenAAddress, address tokenBAddress) {
12+
tokenA = ERC20(tokenAAddress);
13+
tokenB = ERC20(tokenBAddress);
14+
}
15+
16+
function swap(address tokenInAddress, address tokenOutAddress, uint256 amountIn) external {
17+
ERC20 tokenIn = ERC20(tokenInAddress);
18+
ERC20 tokenOut = ERC20(tokenOutAddress);
19+
require(tokenIn == tokenA || tokenIn == tokenB, "invalid tokenIn");
20+
require(tokenOut == tokenA || tokenOut == tokenB, "invalid tokenOut");
21+
require(tokenIn != tokenOut, "tokenIn == tokenOut");
22+
uint256 balanceIn = tokenIn.balanceOf(address(this));
23+
uint256 balanceOut = tokenOut.balanceOf(address(this));
24+
uint256 amountOut = balanceOut - balanceIn * balanceOut * 1000 / (balanceIn + amountIn) / 997; // 0.3% fee
25+
tokenIn.transferFrom(msg.sender, address(this), amountIn);
26+
tokenOut.transfer(msg.sender, amountOut);
27+
}
28+
}
29+
30+
contract LendingPool {
31+
ERC20 public immutable tokenA;
32+
ERC20 public immutable tokenB;
33+
AMM public immutable amm;
34+
mapping(address => mapping(address => int256)) public deposits;
35+
36+
constructor(address tokenAAddress, address tokenBAddress, address ammAddress) {
37+
tokenA = ERC20(tokenAAddress);
38+
tokenB = ERC20(tokenBAddress);
39+
amm = AMM(ammAddress);
40+
}
41+
42+
function supply(address asset, uint256 amount) external {
43+
ERC20 token = ERC20(asset);
44+
require(token == tokenA || token == tokenB, "invalid asset");
45+
deposits[asset][msg.sender] += int256(amount);
46+
token.transferFrom(msg.sender, address(this), amount);
47+
}
48+
49+
function withdraw(address asset, uint256 amount) external {
50+
// NOTE:
51+
// - require: depositsIn[msg.sender] * price * 75% >= amount + -depositsOut[msg.sender]
52+
// - price = tokenOut.balanceOf(address(amm)) / tokenIn.balanceOf(address(amm))
53+
ERC20 token = ERC20(asset);
54+
require(token == tokenA || token == tokenB, "invalid asset");
55+
if (token == tokenA) {
56+
require(
57+
deposits[address(tokenB)][msg.sender] * int256(tokenA.balanceOf(address(amm))) * 3
58+
>= (int256(amount) + -deposits[asset][msg.sender]) * int256(tokenB.balanceOf(address(amm))) * 4,
59+
"insufficient deposit"
60+
);
61+
} else {
62+
require(
63+
deposits[address(tokenA)][msg.sender] * int256(tokenB.balanceOf(address(amm))) * 3
64+
>= (int256(amount) + -deposits[asset][msg.sender]) * int256(tokenA.balanceOf(address(amm))) * 4,
65+
"insufficient deposit"
66+
);
67+
}
68+
deposits[asset][msg.sender] -= int256(amount);
69+
token.transfer(msg.sender, amount);
70+
}
71+
72+
/* A liquidation function is omitted. */
73+
}
74+
75+
contract Setup {
76+
ERC20 public tokenA;
77+
ERC20 public tokenB;
78+
AMM public amm;
79+
LendingPool public lendingPool;
80+
uint256 immutable tokenAUnit;
81+
uint256 immutable tokenBUnit;
82+
83+
constructor(address tokenAAddress, address tokenBAddress) {
84+
tokenA = ERC20(tokenAAddress);
85+
tokenB = ERC20(tokenBAddress);
86+
tokenAUnit = 10 ** tokenA.decimals();
87+
tokenBUnit = 10 ** tokenB.decimals();
88+
}
89+
90+
function init() external {
91+
amm = new AMM(address(tokenA), address(tokenB));
92+
tokenA.transfer(address(amm), 20_000_000 * tokenAUnit);
93+
tokenB.transfer(address(amm), 10_000 * tokenBUnit);
94+
lendingPool = new LendingPool(address(tokenA), address(tokenB), address(amm));
95+
tokenA.transfer(address(lendingPool), 900_000 * tokenAUnit);
96+
}
97+
98+
function isSolved() public view returns (bool) {
99+
return tokenA.balanceOf(msg.sender) >= (100_000 * tokenAUnit) && tokenA.balanceOf(address(lendingPool)) == 0;
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "forge-std/Test.sol";
5+
import "forge-std/console.sol";
6+
import "./Challenge.sol";
7+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
8+
9+
contract ChallengeTest is Test {
10+
Setup setup;
11+
address public playerAddress;
12+
13+
function setUp() public {
14+
vm.createSelectFork("mainnet", 17600000);
15+
16+
playerAddress = makeAddr("player");
17+
ERC20 usdc = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
18+
ERC20 weth = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
19+
setup = new Setup(address(usdc), address(weth));
20+
deal(address(usdc), address(setup), 20_900_000 * (10 ** usdc.decimals()));
21+
deal(address(weth), address(setup), 10_000 ether);
22+
setup.init();
23+
}
24+
25+
function testExploit() public {
26+
vm.startPrank(playerAddress, playerAddress);
27+
28+
////////// YOUR CODE GOES HERE //////////
29+
30+
////////// YOUR CODE END //////////
31+
32+
emit log_named_decimal_uint("user tokenA", setup.tokenA().balanceOf(playerAddress), setup.tokenA().decimals());
33+
emit log_named_decimal_uint("user tokenB", setup.tokenB().balanceOf(playerAddress), setup.tokenB().decimals());
34+
assertTrue(setup.isSolved(), "challenge not solved");
35+
vm.stopPrank();
36+
}
37+
}
38+
39+
////////// YOUR CODE GOES HERE //////////
40+
41+
////////// YOUR CODE END //////////
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6+
7+
contract MintableERC20 is ERC20, Ownable {
8+
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}
9+
10+
function mint(address account, uint256 amount) external onlyOwner {
11+
_mint(account, amount);
12+
}
13+
}
14+
15+
contract AMM {
16+
ERC20 public immutable tokenA;
17+
ERC20 public immutable tokenB;
18+
19+
constructor(address tokenAAddress, address tokenBAddress) {
20+
tokenA = ERC20(tokenAAddress);
21+
tokenB = ERC20(tokenBAddress);
22+
}
23+
24+
function swap(address tokenInAddress, address tokenOutAddress, uint256 amountIn) external {
25+
ERC20 tokenIn = ERC20(tokenInAddress);
26+
ERC20 tokenOut = ERC20(tokenOutAddress);
27+
require(tokenIn == tokenA || tokenIn == tokenB, "invalid tokenIn");
28+
require(tokenOut == tokenA || tokenOut == tokenB, "invalid tokenOut");
29+
require(tokenIn != tokenOut, "tokenIn == tokenOut");
30+
uint256 balanceIn = tokenIn.balanceOf(address(this));
31+
uint256 balanceOut = tokenOut.balanceOf(address(this));
32+
uint256 amountOut = balanceOut - balanceIn * balanceOut * 1000 / (balanceIn + amountIn) / 997; // 0.3% fee
33+
tokenIn.transferFrom(msg.sender, address(this), amountIn);
34+
tokenOut.transfer(msg.sender, amountOut);
35+
}
36+
}
37+
38+
contract LendingPool {
39+
ERC20 public immutable tokenA;
40+
ERC20 public immutable tokenB;
41+
AMM public immutable amm;
42+
mapping(address => mapping(address => int256)) public deposits;
43+
44+
constructor(address tokenAAddress, address tokenBAddress, address ammAddress) {
45+
tokenA = ERC20(tokenAAddress);
46+
tokenB = ERC20(tokenBAddress);
47+
amm = AMM(ammAddress);
48+
}
49+
50+
function supply(address asset, uint256 amount) external {
51+
ERC20 token = ERC20(asset);
52+
require(token == tokenA || token == tokenB, "invalid asset");
53+
deposits[asset][msg.sender] += int256(amount);
54+
token.transferFrom(msg.sender, address(this), amount);
55+
}
56+
57+
function withdraw(address asset, uint256 amount) external {
58+
// NOTE:
59+
// - require: depositsIn[msg.sender] * price * 75% >= amount + -depositsOut[msg.sender]
60+
// - price = tokenOut.balanceOf(address(amm)) / tokenIn.balanceOf(address(amm))
61+
ERC20 token = ERC20(asset);
62+
require(token == tokenA || token == tokenB, "invalid asset");
63+
if (token == tokenA) {
64+
require(
65+
deposits[address(tokenB)][msg.sender] * int256(tokenA.balanceOf(address(amm))) * 3
66+
>= (int256(amount) + -deposits[asset][msg.sender]) * int256(tokenB.balanceOf(address(amm))) * 4,
67+
"insufficient deposit"
68+
);
69+
} else {
70+
require(
71+
deposits[address(tokenA)][msg.sender] * int256(tokenB.balanceOf(address(amm))) * 3
72+
>= (int256(amount) + -deposits[asset][msg.sender]) * int256(tokenA.balanceOf(address(amm))) * 4,
73+
"insufficient deposit"
74+
);
75+
}
76+
deposits[asset][msg.sender] -= int256(amount);
77+
token.transfer(msg.sender, amount);
78+
}
79+
80+
/* A liquidation function is omitted. */
81+
}
82+
83+
contract Setup {
84+
MintableERC20 public tokenA;
85+
MintableERC20 public tokenB;
86+
AMM public amm;
87+
LendingPool public lendingPool;
88+
bool claimed = false;
89+
90+
constructor() {
91+
tokenA = new MintableERC20("A", "A");
92+
tokenB = new MintableERC20("B", "B");
93+
amm = new AMM(address(tokenA), address(tokenB));
94+
tokenA.mint(address(amm), 20_000_000 ether);
95+
tokenB.mint(address(amm), 10_000 ether);
96+
lendingPool = new LendingPool(address(tokenA), address(tokenB), address(amm));
97+
tokenA.mint(address(lendingPool), 900_000 ether);
98+
}
99+
100+
function claim() external {
101+
require(!claimed);
102+
claimed = true;
103+
tokenA.mint(msg.sender, 9_000_000 ether);
104+
}
105+
106+
function isSolved() public view returns (bool) {
107+
return tokenA.balanceOf(msg.sender) >= (9_100_000 ether) && tokenA.balanceOf(address(lendingPool)) == 0;
108+
}
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "forge-std/Test.sol";
5+
import "./Challenge.sol";
6+
7+
contract ChallengeTest is Test {
8+
Setup setup;
9+
address public playerAddress;
10+
11+
function setUp() public {
12+
playerAddress = makeAddr("player");
13+
setup = new Setup();
14+
}
15+
16+
function testExploit() public {
17+
vm.startPrank(playerAddress, playerAddress);
18+
19+
////////// YOUR CODE GOES HERE //////////
20+
21+
////////// YOUR CODE END //////////
22+
23+
emit log_named_decimal_uint("user tokenA", setup.tokenA().balanceOf(playerAddress), setup.tokenA().decimals());
24+
emit log_named_decimal_uint("user tokenB", setup.tokenB().balanceOf(playerAddress), setup.tokenB().decimals());
25+
assertTrue(setup.isSolved(), "challenge not solved");
26+
vm.stopPrank();
27+
}
28+
}
29+
30+
////////// YOUR CODE GOES HERE //////////
31+
32+
////////// YOUR CODE END //////////

‎foundry.toml

+3
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ out = "out"
44
libs = ["lib"]
55
match_path = "*/*.t.sol"
66

7+
[rpc_endpoints]
8+
mainnet = "https://rpc.ankr.com/eth"
9+
710
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

0 commit comments

Comments
 (0)
Please sign in to comment.