3. Simple AMM Vault
Codebase walkthrough
This CTF challenge presents a DeFi system composed of three main contracts:
GREY Token: A standard ERC20 token
SimpleVault: An ERC4626-like vault that allows staking GREY tokens for SV shares
SimpleAMM: An automated market maker that creates a liquidity pool between SV tokens and GREY tokens
The goal is to accumulate at least 3000 GREY tokens starting with only 1000 GREY from the claim function.
Let's examine the initial setup in the Setup.sol
contract:
After setup, the vault state is:
totalAssets = 2000 GREY
totalSupply = 1000 SV
sharePrice = 2000/1000 = 2.0
(2 GREY per SV)
The AMM pool contains:
reserveX = 1000
(stored state variable)reserveY = 2000
(stored state variable)Actual balances: 1000 SV + 2000 GREY
k = 1000 + 2000/2.0 = 2000
(using the vault's share price in calculation)
Bug description
The critical vulnerability lies in how the SimpleAMM calculates its invariant and handles flash loans:
The key issue is that flash loans don't update the stored reserveX
and reserveY
variables, but they allow for atomic replacement of the underlying SV tokens with different economic value.
Step 1: Initial Setup
Player now has: 1000 GREY
Step 2: Flash Loan 1000 SV
Critical insight: During the flash loan:
AMM's actual SV balance: 1000 β 0 SV (tokens sent to borrower)
AMM's stored
reserveX
: remains 1000 (unchanged)AMM's stored
reserveY
: remains 2000 (unchanged)
Why flash loan 1000 SV, not other amount? It is because we want to drain totalSupply to make the vault go back to its βinitial stateβ:
Step 3: SV Token Replacement (in callback)
Before manipulation:
Vault: totalAssets = 2000, totalSupply = 1000, sharePrice = 2.0
Old SV tokens represent: 1000 SV Γ 2.0 = 2000 GREY value
Complete drainage:
Withdraw 1000 SV β get 2000 GREY
Vault state: totalAssets = 0, totalSupply = 0
Share price resets to 1.0 (triggered by totalSupply == 0)
Repopulation with manipulated ratio:
Deposit 1000 GREY β get 1000 SV (at reset 1:1 ratio)
Vault state: totalAssets = 1000, totalSupply = 1000, sharePrice = 1.0
Flash loan repayment:
AMM gets back 1000 SV tokens, but these are new SV tokens with 1:1 GREY backing
AMM's stored reserves remain: reserveX = 1000, reserveY = 2000
Step 4: The Broken Invariant
When the flash loan completes, the invariant modifier executes:
Crucial point: The invariant uses stored reserve values, not actual token balances:
computeK(1000, 2000)
with new share price =1000 + 2000/1.0 = 3000
Original stored
k = 2000
Invariant check:
3000 >= 2000
β (passes)
Step 5: Exploit the Phantom Value
The AMM now has "phantom value" because:
It was initialized with
k = 2000
(when SV had 2:1 backing)But now computes
k = 3000
(with SV having 1:1 backing)The difference of 1000 can be extracted
By swapping 0 SV input for 1000 GREY output:
New stored reserves:
reserveX = 1000
,reserveY = 1000
New computed k =
1000 + 1000/1.0 = 2000
Invariant:
2000 >= 2000
β (still satisfied)
PoC
Last updated