DEV Community

Matthew
Matthew

Posted on

Forensic Walkthrough: Decoding the ResupplyFi Attack Contract

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
  • Deposit worthless shares
08ad │ PUSH4  # 0xa0712d68 → mint(uint256)
0a27 │ PUSH4  # 0x60c52d05 → addCollateralVault(uint256,address)
0ab1 │ PUSH4  # 0x13966db5 → mintFee()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)