All incidents

Channels Dust-Share Drain

Share
Dec 31, 2023 23:01 UTCAttackLoss: 3,128.84 USDC, 1,283.97 BUSDPending manual check2 exploit txWindow: 2m 6s
Estimated Impact
3,128.84 USDC, 1,283.97 BUSD
Label
Attack
Exploit Tx
2
Addresses
4
Attack Window
2m 6s
Dec 31, 2023 23:01 UTC → Dec 31, 2023 23:03 UTC

Exploit Transactions

TX 1BSC
0x227a731f4fa2bb7dd7bd26cb954451db2700be73632e1d75638a40f865cb1b30
Dec 31, 2023 23:01 UTCExplorer
TX 2BSC
0xcf729a9392b0960cd315d7d49f53640f000ca6b8a0bd91866af5821fdf36afc5
Dec 31, 2023 23:03 UTCExplorer

Victim Addresses

0x93790C641D029D1cBd779D87b88f67704B6A8F4CBSC
0x33e68c922d19D74ce845546a5c12A66ea31385c4BSC
0xca797539f004C0F9c206678338f820AC38466D4bBSC
0xFC518333F4bC56185BDd971a911fcE03dEe4fC8cBSC

Loss Breakdown

3,128.84USDC
1,283.97BUSD

Similar Incidents

Root Cause Analysis

Channels Dust-Share Drain

1. Incident Overview TL;DR

Channels Finance on BNB Chain was exploited through its LP-backed cCLP_BTCB_BUSD market at 0x93790C641D029D1cBd779D87b88f67704B6A8F4C. In transaction 0x227a731f4fa2bb7dd7bd26cb954451db2700be73632e1d75638a40f865cb1b30, the attacker seized the only outstanding raw cCLP share from a public liquidatable borrower and reshaped the market so only 2 raw cTokens existed and both were attacker-controlled. In transaction 0xcf729a9392b0960cd315d7d49f53640f000ca6b8a0bd91866af5821fdf36afc5, the attacker flash-loaned BTCB and BUSD, minted Pancake LP, donated that LP plus 1 CAKE into the market, called accrueInterest(), borrowed all available cash from cUSDC and cBUSD, then redeemed almost all donated LP while burning only 1 raw cCLP unit.

The root cause is a share-accounting failure in the Channels cToken and Comptroller path. redeemUnderlying rounds the raw share burn down, and the controller validates redemption against that truncated redeemTokens value instead of the actual underlying removed. Once the attacker had reduced total supply to dust and inflated exchangeRateStored through direct LP and CAKE donations, one raw cToken unit represented an outsized amount of LP collateral. That let the attacker borrow against inflated collateral and then remove nearly all of it while the controller only approved a one-share redemption.

2. Key Background

Channels Finance used Compound-style cToken markets on BNB Chain. Three markets matter here:

  • cCLP_BTCB_BUSD at 0x93790C641D029D1cBd779D87b88f67704B6A8F4C
  • cUSDC at 0x33e68c922d19D74ce845546a5c12A66ea31385c4
  • cBUSD at 0xca797539f004C0F9c206678338f820AC38466D4b

Live RPC identity checks show these markets belong to Channels Finance and all point at the same Comptroller 0xFC518333F4bC56185BDd971a911fcE03dEe4fC8c:

"Channels USDC"
"Channels BUSD"
"Channels CLP_BTCB_BUSD"
0xFC518333F4bC56185BDd971a911fcE03dEe4fC8c
0xFC518333F4bC56185BDd971a911fcE03dEe4fC8c
0xFC518333F4bC56185BDd971a911fcE03dEe4fC8c

cCLP_BTCB_BUSD used the Pancake V2 BTCB/BUSD LP token 0xF45cd219aEF8618A92BAa7aD848364a158a24F33 as its underlying. This market also staked LP in Pancake MasterChef and compounded CAKE rewards during accrueInterest(). In a Compound-style market, user ownership is tracked in raw cToken units, while exchangeRateStored converts those raw units into underlying value. When totalSupply is extremely small, each raw cToken becomes disproportionately valuable.

That dust-supply condition existed here. Pre-state checks at block 34847553 show:

block 34847553
cCLP_BTCB_BUSD totalSupply = 1
cCLP_BTCB_BUSD balanceOf(0x07e536F23a197F6FB76F42aD01ac2Bcdc3BF738E) = 1
cCLP_BTCB_BUSD exchangeRateStored = 1000000000000000000

After the priming transaction, block 34847595 already had the attacker contract holding the full market supply:

block 34847595
cCLP_BTCB_BUSD totalSupply = 2
cCLP_BTCB_BUSD balanceOf(0xa47b9f87173eda364c821234158dda47b03ac217) = 2
cCLP_BTCB_BUSD exchangeRateStored = 1000000500000000000000000

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an accounting bug, not an oracle spoof or privileged admin compromise. The attacker first compressed the LP-backed cToken market to a 2-share dust state under attacker control. The attacker then inflated the exchange rate by donating fresh BTCB/BUSD LP tokens and 1 CAKE directly into cCLP_BTCB_BUSD before calling accrueInterest(), which turned those 2 raw cTokens into massively overvalued collateral. The critical failure is that redeemUnderlying computes redeemTokens with floor division, so a large underlying redemption can map to only 1 raw cToken when the exchange rate is extremely high. The Comptroller then checks redeemAllowed against that truncated share count instead of the actual LP removed. As a result, the attacker could borrow against the donation-inflated collateral, redeem almost all of that collateral back out, repay the flash loan, and keep the drained stablecoins.

The invariant that fails is straightforward: a redemption path must conservatively account for the exact collateral removed from the redeemer. In this incident, that invariant broke at the interface between the cToken implementation and Comptroller redemption hooks.

4. Detailed Root Cause Analysis

The updated evidence package includes decompiled victim bytecode for both the cCLP implementation and the Comptroller implementation. The cCLP implementation exposes redeemUnderlying(uint256) and clearly embeds calls to Comptroller selectors 0xeabe7d91 and 0x51dff989, which correspond to redeemAllowed and redeemVerify:

dispatch_852a12e3 -> redeemUnderlying(uint256)

348E    PUSH4 0xeabe7d91
34AF    ADDRESS
34B1    DUP12
34B7    PUSH2 0x534a

366E    PUSH4 0x51dff989
368E    ADDRESS
3690    DUP13
3697    PUSH2 0x5365

The Comptroller implementation decompilation shows what those hooks actually validate. redeemAllowed parses exactly three arguments: (cToken, redeemer, redeemTokens). redeemVerify parses four arguments: (cToken, redeemer, redeemAmount, redeemTokens), and only reverts if redeemAmount > 0 while redeemTokens == 0:

dispatch_eabe7d91 -> redeemAllowed(address,address,uint256)

3554 arg0 = msg.data[arg0:arg0 + 0x20]
3555 arg1 = msg.data[arg1:arg1 + 0x20]
3556 var0 = msg.data[temp1 + 0x40:temp1 + 0x40 + 0x20]

1785 function redeemVerify(...)
1790 var0 = msg.data[temp1 + 0x40:temp1 + 0x40 + 0x20]
1791 var1 = msg.data[temp1 + 0x60:temp1 + 0x60 + 0x20]
1808 else if (var0 <= 0x00) { return; }
1809 else { revert("redeemTokens zero"); }

This matches the exploit trace. In the exploit transaction, the attacker contract donates 174494827409609936690 LP units plus 1000000000000000000 CAKE, calls accrueInterest(), and then borrows all cash from cUSDC and cBUSD. The on-chain trace records the borrow amounts and the later under-burned redemption:

0x33e68c922d19D74ce845546a5c12A66ea31385c4::borrow(3150153795938974454242)
0xca797539f004C0F9c206678338f820AC38466D4b::borrow(1304921512019249746678)

0x93790C641D029D1cBd779D87b88f67704B6A8F4C::redeemUnderlying(174494827409609936689)
0xFC518333F4bC56185BDd971a911fcE03dEe4fC8c::redeemAllowed(0x93790..., 0xA47b..., 1)
0xFC518333F4bC56185BDd971a911fcE03dEe4fC8c::redeemVerify(0x93790..., 0xA47b..., 174494827409609936689, 1)

That is the breakpoint. The actual underlying removed was 174494827409609936689 LP units, but the controller only validated redeemTokens = 1. The exploit trace also shows getAccountSnapshot(attacker) returning cTokenBalance=2 with an exchange rate of 87251007677747747001000000000000000000, which means those two raw cTokens represented roughly 174.502015355495494002 LP units after the donation and compounding step. Once that inflated collateral was accepted, the attacker could borrow all available stablecoins and then redeem away nearly all backing while leaving only one raw cToken behind.

5. Adversary Flow Analysis

The adversary used two transactions.

First, tx 0x227a731f4fa2bb7dd7bd26cb954451db2700be73632e1d75638a40f865cb1b30 deployed and funded attacker contract 0xa47b9f87173eda364c821234158dda47b03ac217. The contract repaid 500 raw BUSD to liquidate public borrower 0x07e536F23a197F6FB76F42aD01ac2Bcdc3BF738E, seized the lone raw cCLP unit, redeemed it, minted fresh dust LP into the now-empty market, and redeemed all but 2 raw cCLP units. The result was total market capture: the attacker owned both remaining raw shares.

Second, tx 0xcf729a9392b0960cd315d7d49f53640f000ca6b8a0bd91866af5821fdf36afc5 executed the drain:

  1. The attacker flash-loaned 1 BTCB and 42218672818223010583114 BUSD from Pancake V3 liquidity.
  2. The attacker minted 174494827409609936690 LP_BTCB_BUSD tokens.
  3. The attacker transferred all of that LP plus 1 CAKE directly into cCLP_BTCB_BUSD.
  4. The attacker called accrueInterest(), causing the dust-share position to absorb the donated value.
  5. The attacker entered the cCLP_BTCB_BUSD, cUSDC, and cBUSD markets in the Comptroller.
  6. The attacker borrowed the full on-hand cash from cUSDC and cBUSD.
  7. The attacker called redeemUnderlying(174494827409609936689), which only burned one raw cCLP share.
  8. The attacker unwound LP back into BTCB and BUSD, repaid the flash loan, and transferred the remaining stablecoins to the EOA 0xd227dc77561b58c5a2d2644ac0173152a1a5dc3d.

This sequence is ACT-complete. It used only public undercollateralized debt, public AMM liquidity, public flash liquidity, and public lending-market entry and borrow functions.

6. Impact & Losses

The exploit fully drained the immediately borrowable cash from the Channels cUSDC and cBUSD markets. The seed balance-diff artifact shows the exact losses:

  • cUSDC lost 3128837445560031450147 raw USDC units, and the attacker EOA received the same amount.
  • cBUSD lost 1283970316009743676673 raw BUSD units, and the attacker EOA received the same amount.

The stablecoin outflows are visible directly in the balance diff:

{
  "token": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
  "holder": "0x33e68c922d19d74ce845546a5c12a66ea31385c4",
  "delta": "-3150153795938974454242"
}
{
  "token": "0xe9e7cea3dedca5984780bafc599bd69add087d56",
  "holder": "0xca797539f004c0f9c206678338f820ac38466d4b",
  "delta": "-1304921512019249746678"
}
{
  "token": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
  "holder": "0xd227dc77561b58c5a2d2644ac0173152a1a5dc3d",
  "delta": "3128837445560031450147"
}
{
  "token": "0xe9e7cea3dedca5984780bafc599bd69add087d56",
  "holder": "0xd227dc77561b58c5a2d2644ac0173152a1a5dc3d",
  "delta": "1283970316009743676673"
}

After the exploit, the protocol was left with stablecoin bad debt while the remaining cCLP collateral was only a single raw share backed by dust LP. The attacker also paid 5528874000000000 wei in BNB gas for the exploit transaction, which does not change the exploit classification because the stablecoin profit materially exceeded gas cost.

7. References

  • Deployment / priming transaction: 0x227a731f4fa2bb7dd7bd26cb954451db2700be73632e1d75638a40f865cb1b30
  • Exploit transaction: 0xcf729a9392b0960cd315d7d49f53640f000ca6b8a0bd91866af5821fdf36afc5
  • Channels Comptroller: 0xFC518333F4bC56185BDd971a911fcE03dEe4fC8c
  • Channels cCLP_BTCB_BUSD: 0x93790C641D029D1cBd779D87b88f67704B6A8F4C
  • Channels cUSDC: 0x33e68c922d19D74ce845546a5c12A66ea31385c4
  • Channels cBUSD: 0xca797539f004C0F9c206678338f820AC38466D4b
  • Pancake LP underlying: 0xF45cd219aEF8618A92BAa7aD848364a158a24F33
  • Pre-state checks: artifacts/auditor/iter_0/prestate_notes.txt
  • Deployment trace excerpt: artifacts/auditor/iter_0/deployment_trace_excerpt.txt
  • Exploit trace excerpt: artifacts/auditor/iter_0/exploit_trace_excerpt.txt
  • Full exploit trace: artifacts/collector/seed/56/0xcf729a9392b0960cd315d7d49f53640f000ca6b8a0bd91866af5821fdf36afc5/trace.cast.log
  • Balance diff: artifacts/collector/seed/56/0xcf729a9392b0960cd315d7d49f53640f000ca6b8a0bd91866af5821fdf36afc5/balance_diff.json
  • Channels identity checks: artifacts/auditor/iter_1/channels_identity.txt
  • cCLP implementation decompilation: artifacts/auditor/iter_1/cclp_btcb_busd_impl_decompiled.html
  • cCLP implementation excerpt: artifacts/auditor/iter_1/cclp_btcb_busd_impl_excerpt.txt
  • Comptroller decompilation: artifacts/auditor/iter_1/comptroller_impl_decompiled.html
  • Comptroller excerpt: artifacts/auditor/iter_1/comptroller_impl_excerpt.txt