All incidents

Cork Recursive DS Drain

Share
May 28, 2025 11:39 UTCAttackLoss: 3,760.88 wstETHPending manual check1 exploit txWindow: Atomic
Estimated Impact
3,760.88 wstETH
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
May 28, 2025 11:39 UTC → May 28, 2025 11:39 UTC

Exploit Transactions

TX 1Ethereum
0xfd89cdd0be468a564dd525b222b728386d7c6780cf7b2f90d2b54493be09f64d
May 28, 2025 11:39 UTCExplorer

Victim Addresses

0xccd90f6435dd78c4ecced1fa4db0d7242548a2a9Ethereum
0xf0da8927df8d759d5ba6d3d714b1452135d99cfcEthereum
0x55b90b37416dc0bd936045a8110d1af3b6bf0fc3Ethereum

Loss Breakdown

3,760.88wstETH

Similar Incidents

Root Cause Analysis

Cork Recursive DS Drain

1. Incident Overview TL;DR

On Ethereum mainnet block 22581020, attacker EOA 0xea6f30e360192bae715599e15e2f765b49e4da98 used helper contract 0x9af3dce0813fd7428c47f57a39da2f6dd7c9bb09 to create a new Cork market whose pegged asset was wstETH and whose redemption asset was an already-issued Cork DS token from the live market 0x6b1d373ba0974d7e308529a62e41cec8bac6d71a57a1ba1b5c5bf82f6a9ea07a. That recursive market accepted old-market DS as if it were valid external collateral, minted a fresh CT/DS pair, and then returned old-market DS that the attacker combined with old-market CT to redeem wstETH from the live market. The transaction finished with the attacker cluster gaining 3760881365943909071528 raw wstETH units after starting from less than one wstETH.

The root cause is a protocol bug in Cork’s market-creation path. CorkConfig.sol exposed initializeModuleCore(...) and issueNewDs(...) to arbitrary callers, and ModuleCore.sol trusted that config input without validating that the supplied RA and PA were canonical external assets. Because Pair.sol only rejects zero addresses and pa == ra, the attacker could set RA = old DS token, making Cork’s own derivative inventory recursively redeemable for real wstETH.

2. Key Background

Cork markets are keyed by (pa, ra, initialArp, expiryInterval, exchangeRateProvider). ModuleCore.initializeModuleCore(...) stores that tuple and deploys the associated liquidity-vault asset. Each issuance then creates a fresh CT/DS pair for that market through issueNewDs(...).

Within a market, Cork’s PSM logic treats the redemption asset as collateral. Depositing RA mints matching CT and DS, while burning equal CT and DS through returnRaWithCtDs(...) releases the same amount of RA back to the caller. That mechanism is safe only if RA is a real external backing asset for the market rather than a Cork-issued derivative from another market.

The live market relevant to this incident already existed before the exploit. Its market id was 0x6b1d373ba0974d7e308529a62e41cec8bac6d71a57a1ba1b5c5bf82f6a9ea07a, its RA was wstETH at 0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0, its current CT was 0xcd25aa56aad1bcc1bb4b6b6b08bda53007ec81ce, and its current DS was 0x7ea0614072e2107c834365bea14f9b6386fb84a5. The attacker targeted that DS token and reintroduced it as collateral in a second market.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an access-control and asset-validation failure. CorkConfig.sol allows any caller to forward arbitrary (pa, ra, initialArp, expiryInterval, exchangeRateProvider) into ModuleCore, and CorkConfig.sol likewise leaves issueNewDs(...) public. Inside ModuleCore, ModuleCore.sol checks only that the caller is config and the market is not already initialized, while Pair.sol limits validation to nonzero addresses and pa != ra.

That means Cork never enforced the invariant that a new market’s PA and RA must be valid external assets rather than already-issued Cork assets. Once the attacker chose RA = old-market DS, PSM minting and redemption logic in PsmLib.sol and PsmLib.sol treated the protocol’s own DS as backing collateral. The attacker then converted protocol-internal derivative balances into redeemable live-market CT+DS and finally into real wstETH. The broken invariant is: Cork-issued CT/DS/LV assets from one market must never be accepted as first-class collateral in a second market.

4. Detailed Root Cause Analysis

The key breakpoint is the public market-initialization path. CorkConfig exposes:

function initializeModuleCore(
    address pa,
    address ra,
    uint256 initialArp,
    uint256 expiryInterval,
    address exchangeRateProvider
) external {
    moduleCore.initializeModuleCore(pa, ra, initialArp, expiryInterval, exchangeRateProvider);
}

function issueNewDs(Id id, uint256 ammLiquidationDeadline) external whenNotPaused {
    moduleCore.issueNewDs(id, defaultDecayDiscountRateInDays, rolloverPeriodInBlocks, ammLiquidationDeadline);
}

The receiving ModuleCore path only verifies config provenance and tuple freshness:

function initializeModuleCore(
    address pa,
    address ra,
    uint256 initialArp,
    uint256 expiryInterval,
    address exchangeRateProvider
) external override {
    onlyConfig();
    configNotPaused();
    if (expiryInterval == 0) revert InvalidExpiry();
    Pair memory key = PairLibrary.initalize(pa, ra, initialArp, expiryInterval, exchangeRateProvider);
    ...
}

The tuple validation itself is only:

if (pa == address(0) || ra == address(0)) revert ZeroAddress();
if (pa == ra) revert InvalidAddress();
key = Pair(pa, ra, initialArp, expiry, exchangeRateProvider);

Nothing in those checks rejects a previously issued Cork DS token as the new market’s RA. The exploit trace confirms that the attacker exercised exactly that path. In trace.cast.log, the public config contract calls:

0xF0DA...::initializeModuleCore(
    WstETH: [0x7f39...2ca0],
    Asset: [0x7ea0...84A5],
    1,
    100,
    0x9Af3...bb09
)

The same trace then shows issueNewDs(...) for the attacker-created market id 0xc67cae5b35ca2fdf6564b38dc5332c88ad608d1c5b3595dd9ad781f5a340cb9d. Later in the same transaction, the recursive market burns its newly minted CT/DS and returns old-market DS through returnRaWithCtDs(...), and the attacker immediately uses live-market CT+DS to redeem wstETH from the original market through a second returnRaWithCtDs(...).

The PSM logic is consistent with that outcome. PsmLib.sol converts the requested fixed-point amount into RA, unlocks it to the caller, and burns the current market CT and DS. Separately, the expiry redemption path transfers both PA and RA to the caller. Those mechanics are safe only if RA is external collateral. Here, RA was itself a Cork DS token, so the attacker could recursively mint new derivatives against old derivatives and then unwrap the old derivatives into real wstETH.

5. Adversary Flow Analysis

The adversary cluster consisted of EOA 0xea6f30e360192bae715599e15e2f765b49e4da98 and helper contract 0x9af3dce0813fd7428c47f57a39da2f6dd7c9bb09. The helper’s address history in normal_transactions.json shows that the EOA deployed it before the exploit and then invoked it as the outer target of the exploit transaction.

The exploit proceeded in three stages. First, the attacker deployed and approved the helper in earlier setup transactions. Second, in seed transaction 0xfd89cdd0be468a564dd525b222b728386d7c6780cf7b2f90d2b54493be09f64d, the helper created the recursive market with PA = wstETH, RA = old-market DS, and attacker-controlled rate provider 0x9af3...bb09. Third, the recursive market accepted 3761257491693078379366 old-market DS from vault 0x55b90b37416dc0bd936045a8110d1af3b6bf0fc3, minted recursive CT/DS, returned 3761257269717268176446 old-market DS to the attacker path, and enabled redemption of 3760885365943909071528 wstETH from the live market.

The trace line items show the relevant state transitions directly: market initialization, new DS issuance, recursive burn back into old DS, and final live-market redemption. The balance diff corroborates the economic result. balance_diff.json records +3761877955369549831945 wstETH for helper 0x9af3...bb09 and -3760881365943909071528 wstETH for ModuleCore 0xccd90f6435dd78c4ecced1fa4db0d7242548a2a9, with the EOA paying 41062287111204062 wei in gas.

6. Impact & Losses

The directly measured protocol loss is 3760881365943909071528 raw wstETH units, equal to 3760.881365943909071528 wstETH, drained from Cork ModuleCore proxy 0xccd90f6435dd78c4ecced1fa4db0d7242548a2a9. The attacker cluster ended the exploit transaction with 3761877955369549831945 raw wstETH units after beginning the transaction with only 996592406032878584 raw wstETH units across the identified cluster.

This was not a paper accounting discrepancy. The exploit converted Cork’s internal live-market CT/DS inventory into externally valuable wstETH, proving that any user could weaponize the public market-creation path against already-live Cork assets so long as enough redeemable RA liquidity remained in an existing market.

7. References

  1. Exploit transaction metadata: metadata.json
  2. Exploit transaction trace: trace.cast.log
  3. Exploit balance diff: balance_diff.json
  4. Public config entrypoints: CorkConfig.sol
  5. ModuleCore market initialization and issuance: ModuleCore.sol
  6. Pair validation logic: Pair.sol
  7. PSM redemption logic: PsmLib.sol
  8. Helper deployment and history: normal_transactions.json