Unpack every step of the $9.8 M ResupplyFi heist by turning raw EVM selectors into human-readable calls.
Background
On June 27, 2025, ResupplyFi’s freshly-deployed ERC-4626 vault was drained for $9.8 M in under two hours via a “donation” exchange-rate collapse attack. The attacker donated a microscopic amount of worthless shares to crash the vault’s price, then looped through collateral deposits and drains until the vault was empty.
I built a tool evm-lens that allows anyone to inspect the opcodes given arbitrary bytecode. evm-lens --abi mode decodes every PUSH4 into its function signature, so you see exactly what the attacker was calling.
Step 0: Install evm-lens
Make sure you have evm-lens installed with cargo cargo install evm-lens
Step 1: Annotate the Disassembly
First, insert the attacker contract address with abi selector annotations:
evm-lens \
--address 0x151aA63dbb7C605E7b0a173Ab7375e1450E79238 \
--abi \
> attacker_disasm.txt
You’ll get lines like:
0013 │ PUSH4 # 0x31f57072 → onMorphoFlashLoan(uint256,bytes)
01dc │ PUSH4 # 0xe0232b42 → flashLoan(address,uint256,bytes)
02e7 │ PUSH4 # 0xa9059cbb → transfer(address,uint256)
08ad │ PUSH4 # 0xa0712d68 → mint(uint256)
0a27 │ PUSH4 # 0x60c52d05 → addCollateralVault(uint256,address)
0bf9 │ PUSH4 # 0xd6bda0c0 → borrow(uint256,uint256,address)
0da9 │ PUSH4 # 0xba087652 → redeem(uint256,address,address)
0f64 │ PUSH4 # 0x128acb08 → swap(address,bool,int256,uint160,bytes)
13ff │ PUSH4 # 0x3df02124 → exchange(int128,int128,uint256,uint256)
150a │ PUSH4 # 0xa9059cbb → transfer(address,uint256)
# …plus standard ERC-20 calls like approve(), balanceOf(), etc.
Step 2: Extract Every Resolved Selector
Pull every line with a decoded arrow:
grep -E '→' attacker_disasm.txt | grep -v '0xffffffff' | sort -u
0013 │ PUSH4 # 0x31f57072 → onMorphoFlashLoan(uint256,bytes)
0029 │ PUSH4 # 0xfa461e33 → TestBrMSja(address,address,bytes)
00fe │ PUSH4 # 0x095ea7b3 → approve(address,uint256)
01dc │ PUSH4 # 0xe0232b42 → flashLoan(address,uint256,bytes)
027b │ PUSH4 # 0x0dfe1681 → amarettoIdealist()
02e7 │ PUSH4 # 0xa9059cbb → transfer(address,uint256)
036c │ PUSH4 # 0xd21220a7 → detectabilityAntiauthoritarianism()
03d8 │ PUSH4 # 0xa9059cbb → transfer(address,uint256)
047d │ PUSH4 # 0x70a08231 → balanceOf(address)
05f6 │ PUSH4 # 0x3df02124 → exchange(int128,int128,uint256,uint256)
0699 │ PUSH4 # 0xf77c4791 → controller()
0729 │ PUSH4 # 0xa9059cbb → transfer(address,uint256)
08ad │ PUSH4 # 0xa0712d68 → mint(uint256)
0a27 │ PUSH4 # 0x60c52d05 → addCollateralVault(uint256,address)
0ab1 │ PUSH4 # 0x13966db5 → mintFee()
0b41 │ PUSH4 # 0x93ae0df9 → totalDebtAvailable()
0bf9 │ PUSH4 # 0xd6bda0c0 → borrow(uint256,uint256,address)
0d0e │ PUSH4 # 0x70a08231 → balanceOf(address)
0da9 │ PUSH4 # 0xba087652 → redeem(uint256,address,address)
0ebf │ PUSH4 # 0x70a08231 → balanceOf(address)
0f64 │ PUSH4 # 0x128acb08 → swap(address,bool,int256,uint160,bytes)
10ad │ PUSH4 # 0x70a08231 → balanceOf(address)
1150 │ PUSH4 # 0x2e1a7d4d → OwnerTransferV7b711143(uint256)
12e9 │ PUSH4 # 0x70a08231 → balanceOf(address)
1363 │ PUSH4 # 0x095ea7b3 → approve(address,uint256)
13ff │ PUSH4 # 0x3df02124 → exchange(int128,int128,uint256,uint256)
1488 │ PUSH4 # 0x70a08231 → balanceOf(address)
150a │ PUSH4 # 0xa9059cbb → transfer(address,uint256)
That single command reveals every selector + signature the attacker invoked; no manual whitelisting needed
Step 3: Walk Through the Exploit Flow
- Flash-loan & callback
0013 │ PUSH4 # 0x31f57072 → onMorphoFlashLoan(uint256,bytes)
01dc │ PUSH4 # 0xe0232b42 → flashLoan(address,uint256,bytes)
# Attacker triggers a Morpho flash-loan, then enters the onMorphoFlashLoan callback.
- Deposit worthless shares
08ad │ PUSH4 # 0xa0712d68 → mint(uint256)
0a27 │ PUSH4 # 0x60c52d05 → addCollateralVault(uint256,address)
0ab1 │ PUSH4 # 0x13966db5 → mintFee()
They mint a tiny amount of vault shares, crashing the exchange rate, then collateralize.
- Drain & swap assets
02e7 │ PUSH4 # 0xa9059cbb → transfer(address,uint256)
0da9 │ PUSH4 # 0xba087652 → redeem(uint256,address,address)
0f64 │ PUSH4 # 0x128acb08 → swap(address,bool,int256,uint160,bytes)
13ff │ PUSH4 # 0x3df02124 → exchange(int128,int128,uint256,uint256)
They transfer out tokens, redeem more shares, then swap/exchange leftovers; looping until the vault is empty.
Why This Matters
One tool, one command: full disassembly + selector decoding in a single pass.
Immediate clarity: decoded calls (flashLoan, addCollateralVault, redeem, etc.) highlight dangerous operations instantly.
Reproducibility: anyone can rerun this on any attacker contract and see exactly what happened.
What is next?
storage-diff (v0.3) is coming: automatically derive storage layouts, detect slot collisions, and generate auditor friendly HTML reports to pair with selector decoding—catch both function and storage-level risks in one tool.
With evm-lens v0.2, raw bytecode is no longer a black box—and v0.3 will make it even safer. 🚀
Suggestions
If you have any suggestions feel free to comment down below, I promise to read every single comment :)
Top comments (0)