Channels Dust-Share Drain
Exploit Transactions
Victim Addresses
0x93790C641D029D1cBd779D87b88f67704B6A8F4CBSC0x33e68c922d19D74ce845546a5c12A66ea31385c4BSC0xca797539f004C0F9c206678338f820AC38466D4bBSC0xFC518333F4bC56185BDd971a911fcE03dEe4fC8cBSCLoss Breakdown
Similar Incidents
Helio Plugin Donation Inflation
38%Bearn Dust Sweep Exploit
37%Sheep Burn Reserve Drain
34%Transit Router V5 Drain
33%Cellframe Migration Drain
33%LAYER3 Oracle-Mint Drain
33%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_BUSDat0x93790C641D029D1cBd779D87b88f67704B6A8F4CcUSDCat0x33e68c922d19D74ce845546a5c12A66ea31385c4cBUSDat0xca797539f004C0F9c206678338f820AC38466D4b
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:
- The attacker flash-loaned
1 BTCBand42218672818223010583114 BUSDfrom Pancake V3 liquidity. - The attacker minted
174494827409609936690LP_BTCB_BUSD tokens. - The attacker transferred all of that LP plus
1 CAKEdirectly intocCLP_BTCB_BUSD. - The attacker called
accrueInterest(), causing the dust-share position to absorb the donated value. - The attacker entered the
cCLP_BTCB_BUSD,cUSDC, andcBUSDmarkets in the Comptroller. - The attacker borrowed the full on-hand cash from
cUSDCandcBUSD. - The attacker called
redeemUnderlying(174494827409609936689), which only burned one raw cCLP share. - 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:
cUSDClost3128837445560031450147raw USDC units, and the attacker EOA received the same amount.cBUSDlost1283970316009743676673raw 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