Reproduced Exploit

Cork: emptyReservePartial returns 0 and the last LV redeem locks 10 RA

emptyReservePartial transfers the DS reserve out and then returns dsPos.reserve, which is 0 when the reserve was fully emptied. The last LV redeem had a DS reserve equal to the CT balance, so the correct sell amount is 0. The 0 return makes ctSellAmount the entire CT balance. That transfer reverts…

Aug 2024•Ethereum•logic•3 min read

Loss

10 RA stays in the vault. The holder's redeem reverts

Chain

Ethereum

Category

logic

Date

Aug 2024

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 41476-h-2-flashswaprouteremptyreserve-and-flashswaprouteremptyrese. Standalone Foundry PoC and full write-up: 41476-h-2-flashswaprouteremptyreserve-and-flashswaprouteremptyrese_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/state-update · vuln/dos/frozen-funds

Key info#

FieldValue
Loss10 RA stays in the vault. The holder's redeem reverts
Vulnerable contractFlashSwapRouter.emptyReservePartial
Attacker EOA0x0000000000000000000000000000000000414761 (the LV holder)
Attack contractExploit
Attack txExploit.run()
Chain / block / dateEthereum model · block 0 · 2024-08
Compilersolc 0.8.27, cancun, via IR
Bug classThe emptied amount is reported as the reserve left behind

TL;DR#

emptyReservePartial transfers the DS reserve out and then returns dsPos.reserve, which is 0 when the reserve was fully emptied. The last LV redeem had a DS reserve equal to the CT balance, so the correct sell amount is 0. The 0 return makes ctSellAmount the entire CT balance. That transfer reverts after the CT was already burned for the paired redeem, and the 10 RA deposit stays in the vault. Returning the emptied amount pays the 10 RA back.

Background#

Sherlock's August 2024 Cork Protocol contest, issue 68 (H-2). VaultLib._redeemCtDsAndSellExcessCt uses the return value of emptyReservePartial as the DS that was taken. The protocol later changed the function to return the amount that was emptied.

The vulnerable code#

SOLIDITY
dsPos.ds.transfer(to, amount);
dsPos.reserve -= amount;
// @> VULN: returns the leftover reserve, which is 0 after a full empty
reserve = dsPos.reserve;
lastReturned = reserve;

Via IR keeps the store of that return value on lastReturned. The caller reads 0.

Root cause#

The function's name and the callers both mean "how much was emptied". The implementation returns the balance after the subtraction. A full empty is indistinguishable from "nothing was reserved".

Preconditions#

  • The DS reserve and the vault's CT balance are both 10 ether, the last-redeem case in the report.
  • The holder deposits 10 RA and then calls redeemEarly in the same transaction.

Attack walkthrough#

  1. The holder deposits 10 RA. The vault balance becomes 10 RA.
  2. redeemEarly reads a reserve of 10 and a CT balance of 10, so redeemAmount is 10.
  3. emptyReservePartial moves 10 DS and returns 0.
  4. The vault burns 10 CT for the paired redeem, then computes ctSellAmount as 10 and tries to transfer 10 more CT.
  5. The transfer reverts. The deposit call already committed, so the vault still holds 10 RA and the holder holds 0.
  6. The fixed function returns amount instead of the leftover. ctSellAmount is 0 and the holder receives the 10 RA.

Diagrams#

flowchart TD A["Holder deposits 10 RA"] --> B["DS reserve equals the CT balance"] B --> C["emptyReservePartial transfers 10 DS"] C --> D["VULN: the return value is the leftover 0"] D --> E["The vault tries to sell every CT token"] E --> F["The redeem reverts and 10 RA stays in the vault"]

Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.