Raft Finance cbETH Donation / Collateral-Index Manipulation Unbacked-R Mint (2023-11-10)
- Name: Raft Finance cbETH donation / collateral-index manipulation unbacked-R mint
- Date: 2023-11-10
- Protocol: Raft Finance
- Chain: Ethereum
- Category: donation / share-price manipulation / collateral-index manipulation / precision-loss mint / unbacked stablecoin mint
- Loss: about
1,577 ETHdrained from Raft’s R/ETH exit liquidity after roughly6.7M Rwas minted unbacked; most of the attacker-side ETH was then accidentally burned - Summary: The attacker turned Raft’s rebasing collateral token accounting into a printable collateral oracle. By donating roughly
1,061 cbETHdirectly into the Interest Rate Position Manager (IRPM) and then liquidating an undercollateralized cbETH position, they forced Raft to recompute thercbETH-ccollateral index from the contract’s rawcbETHbalance while only a tiny number of raw shares remained outstanding. After that reset, each new1 weicbETH deposit minted1rawrcbETH-cshare becausemint()rounded up withdivUp, butbalanceOf()valued that single raw share at about67.45 cbETHthrough the inflated index. The attacker repeated the1 weideposit loop, minted about6,705,028 Ragainst almost no new value, dumped the unbacked R into market liquidity, and pulled out about1,577 ETHbefore a separate delegatecall mistake in their own cash-out path burned most of the proceeds. - Trigger / entry point:
- Raft represented collateral and debt with
ERC20Indexabletokens whose user-visiblebalanceOf()andtotalSupply()multiplied raw shares by a mutable global index. PositionManager.liquidate()read collateral throughraftCollateralToken.balanceOf(position), then_updateDebtAndCollateralIndex()reset the collateral index fromcollateralToken.balanceOf(address(this))after closing the liquidated position.- Because the backing amount came from the raw cbETH balance sitting in the manager contract, direct cbETH donations could change the next global index update without minting matching raw collateral shares.
ERC20Indexable.mint()used ceiling division (amount.divUp(storedIndex)), so once the index became very large, even a1 weicollateral increase still minted1raw share instead of0.
- Raft represented collateral and debt with
- Root cause:
- Raft let a donated collateral balance become part of the global
rcbETH-cbacking index even though no new raw collateral shares were minted for that donation. - The protocol then combined that donation-sensitive index with a ceil-rounded mint path, so microscopic new deposits still received whole raw shares at the now-inflated index.
- Because downstream collateral checks used
balanceOf()rather than raw shares, the inflated index immediately became borrowable solvency truth inside the same protocol. - In short: Raft treated a mutable rebasing collateral index like safe accounting state, but a direct donation plus round-up minting turned it into an attacker-printable collateral mark.
- Raft let a donated collateral balance become part of the global
- Exploit path:
- In a preparatory transaction (
0xa1378a4d61e81339daaf2c7c8bb669be42002919f10379c616d0aee34047794e), the attacker set up the cbETH-side state used for the later liquidation and donated about1,061 cbETHdirectly into the Raft IRPM contract0x9ab6b21cdf116f611110b048987e58894786c244. - The attacker then liquidated the prepared position so Raft would close it and run
_updateDebtAndCollateralIndex(), which set thercbETH-cindex from the manager contract’s donated cbETH balance divided by the tiny remaining raw share supply. - After that index reset, one raw
rcbETH-cshare represented about67.45 cbETHof apparent collateral value. - In the main exploit transaction (
0xfeedbf51b4e2338e38171f6e19501327294ab1907ab44cfd2d7e7336c975ace7), attacker address0xc1f2b71a502b551a65eee9c96318afdd5fd439farepeatedly calledmanagePosition()to add only1 weiof cbETH collateral at a time. - Because
ERC20Indexable.mint()rounded up, each1 weiaddition still minted1rawrcbETH-cshare. - Because
balanceOf()multiplied raw shares by the already-inflated index, each newly minted raw share looked like tens of cbETH of fresh collateral inside Raft’s solvency checks. - The attacker borrowed and minted about
6.7M Ragainst that printed collateral, then sold the unbacked R into existing liquidity and extracted about1,577 ETH. - The attacker attempted to route profits onward through a second contract, but a delegatecall storage mismatch left the destination address uninitialized and caused roughly
1,570 ETHto be sent to the zero address instead of retained.
- In a preparatory transaction (
- What the attacker needed to be true:
- Direct cbETH transfers into the manager contract had to affect the next collateral-index update without minting offsetting raw collateral shares.
- Liquidation needed to call the shared
_updateDebtAndCollateralIndex()path after closing the prepared position, so the donated balance would be spread across a very small remaining raw share base. ERC20Indexable.mint()had to round small deposits up to one raw share instead of rounding them down to zero once the index exceeded the deposit size.- Raft’s collateral accounting had to trust
balanceOf()on the rebasing collateral token as immediate borrow-power truth. - There needed to be enough R liquidity / exit liquidity on the other side for freshly minted unbacked R to be dumped into ETH before defenses reacted.
- Technical anatomy worth remembering:
- This was a donation / share-price manipulation attack, but not the ordinary ERC-4626
totalAssets / totalSupplyversion. Raft embedded the same economic mistake inside an indexable collateral token whosebalanceOf()presentation layer was itself the solvency rail. - The decisive bug was not just
rounding errorin isolation. The attack needed the combination of donation-sensitive global index resets and ceil-rounded raw-share minting. - The liquidation path mattered because it re-priced the entire collateral token from contract balance after the attacker had already altered that balance off-ledger through direct donation.
- Raft’s collateral was not externally mispriced by an oracle. The protocol printed its own internal collateral reality and then lent against it.
- The attacker still extracted real value despite later burning most of it by mistake; the protocol-side lesson is unchanged because the unbacked R mint and ETH outflow had already happened.
- This was a donation / share-price manipulation attack, but not the ordinary ERC-4626
- Representative transactions and addresses:
- Attacker EOA:
0xc1f2b71a502b551a65eee9c96318afdd5fd439fa - Preparatory tx:
0xa1378a4d61e81339daaf2c7c8bb669be42002919f10379c616d0aee34047794e - Main exploit tx:
0xfeedbf51b4e2338e38171f6e19501327294ab1907ab44cfd2d7e7336c975ace7 - Exploited Raft IRPM contract:
0x9ab6b21cdf116f611110b048987e58894786c244 - Stablecoin contract:
Rat0x183015a9ba6ff60230fdeadc3f43b3d788b13e21
- Attacker EOA:
- Response / mitigation:
- Raft acknowledged the incident the same day and paused further R minting while still allowing existing users to repay debt and withdraw collateral.
- The team later said the protocol had minted about
6.7Munbacked R and that the current version of Raft would be sunsetted. - The exploit became a concrete case study in why rebasing receipt-token presentation math, global index updates, and small-amount rounding need to be modeled together rather than audited as isolated helpers.
Why this incident matters
- Raft is a sharp modern example of donation / share-price manipulation escaping the usual ERC-4626 frame and reappearing as rebasing-collateral
balanceOf()manipulation inside a lending protocol. - More specifically, the exploit path was one joined authority object: liquidation-side
_updateDebtAndCollateralIndex()republished donated rawcbETHbalance as canonical index state,ERC20Indexable.balanceOf()translated that state into apparently huge collateral balances, and Raft’s borrow-side solvency logic treated that presentation-layer result as money-good collateral immediately. - It preserves the important lesson that liquidation code, index-maintenance code, rebasing presentation math, and borrow checks can all be the same oracle surface in practice, even when teams audit them as separate helpers.
Pattern tags
- Donation-to-index publication:
_updateDebtAndCollateralIndex()ultimately calledraftCollateralToken.setIndex(collateralToken.balanceOf(address(this))), so a directcbETHdonation to the manager contract could become canonical collateral-index state without minting offsetting raw shares. - Tiny-denominator index blow-up: the attacker first forced liquidation and position closure while only a very small raw
rcbETH-cshare supply remained, so the next index reset spread donated backing across a near-empty denominator. - Ceiling-mint share resurrection:
ERC20Indexable.mint()usedamount.divUp(storedIndex), which meant even a1 weicollateral increase still minted1raw share once the index dwarfed the deposit size. - Presentation-layer collateral oracle failure: Raft’s solvency checks consumed
raftCollateralToken.balanceOf(position), so an attacker-authored presentation index became immediate borrow power instead of staying a quarantined accounting view. - Design companions: donation-share-price-manipulation, state-publication-authority-failure, sync-harvest-and-cache-write-authority-discipline, and reward-recognition-presentation-and-collateral-separation.
Why the exploit worked
- The manager contract’s raw
cbETHbalance and the rawrcbETH-cshare supply were allowed to drift apart: direct donations increased the former without minting any of the latter. - Liquidation was not only a bad-debt path; it was an index-publication path. Once
_closePosition()and_updateDebtAndCollateralIndex()ran, the protocol republished donated backing across the tiny surviving raw-share base as if it were legitimate collateral truth. - The mint path then failed closed in the wrong direction:
divUpguaranteed that dust-sized collateral additions still resurrected whole raw shares under the attacker-inflated index. - Raft’s borrow checks consumed
balanceOf()rather than a slower, provenance-checked, or raw-share-based collateral object, so the inflated presentation mark immediately became borrowable value inside the same protocol. - The attacker did not need an external oracle mistake after that point. Raft’s own liquidation, indexing, and presentation logic produced the fake collateral and then honored it.
Detection and audit pivots
- Trace every
setIndex,sync,harvest,cache, or liquidation-side recomputation that derives canonical collateral state fromtoken.balanceOf(address(this))or another ambient balance read. Ask which off-ledger transfers can change that value without minting matching shares. - Fuzz the exact sequence
donate underlying -> collapse or concentrate raw share supply -> republish global index -> dust-size mint -> borrow. Donation bugs often only become obvious when denominator collapse and later rounding are exercised together. - Treat ceil rounding on share mint or debt mint as a high-risk solvency surface whenever the denominator or index can be attacker-inflated.
1 weishould never become a whole new share if that share already carries large published value. - Review
balanceOf()on rebasing or indexable tokens as an oracle surface, not a harmless presentation helper, whenever lending, liquidation, or settlement code consumes it directly. - In any protocol that closes positions or redistributes debt during liquidation, check whether the same path also republishes collateral or debt indexes. If so, liquidation is a live valuation authority surface, not just cleanup logic.
Failure-prevention checklist
- Never let global collateral-index publication read straight from
collateralToken.balanceOf(address(this))unless every path that can increase that balance also mints or otherwise accounts for matching raw shares. Direct donations, rescue transfers, and accidental top-ups should be quarantined or explicitly recognized, not silently folded into canonical collateral truth. - Treat liquidation, redemption, and other cleanup flows as valuation-authority surfaces. If those paths close positions and then republish indexes, they must consume one internally accounted backing object rather than ambient contract balances left behind after user-controlled transfers.
- Bound low-denominator states aggressively. If raw collateral-share supply can become tiny, add minimum-share floors, reseeding requirements, or automatic quarantine so one donated numerator cannot be spread across a near-empty denominator and published as a gigantic new index.
- Make dust deposits fail closed once the index becomes large.
divUpor equivalent ceil-rounding on raw-share mint should not let1 weiof new collateral resurrect a whole share that already carries tens of units of published value; use floor rounding, minimum deposit sizes, or exact-share checks instead. - Keep presentation-layer balances separate from borrow authority. Rebasing or indexable
balanceOf()may still be useful UX math, but it should not become immediate solvency truth for minting, borrowing, or liquidation without a stricter provenance-checked backing object or a same-step quarantine. - Review any path that updates debt and collateral indexes together. If one helper can simultaneously republish the collateral numerator and normalize the debt denominator, it is already part of the mint-authority boundary.
- Stress-test
donate underlying -> liquidate or close -> republish index -> dust mint -> borrowas one end-to-end exploit path. Donation, denominator collapse, and rounding rarely look dangerous when audited in isolation. - Keep market-level blast radius bounded for rebasing or indexable collateral: isolated listings, strict debt caps, pause hooks on abnormal index jumps, and monitoring for tiny deposits minting nontrivial borrow power should all be standard.
Audit prompts
- Which exact functions call
setIndex()for collateral and debt, and do any of them derive backing from rawtoken.balanceOf(address(this))rather than from an internal accounted ledger? - Can a user or third party increase the manager contract’s collateral balance without minting matching raw shares, and if so, what stops the next liquidation, redemption, or sync path from canonizing that drift into the global index?
- How small can raw collateral-share supply get after liquidations, redemptions, or position closures, and what guard stops a donated numerator from being spread across that tiny denominator?
- If the published index becomes larger than an incoming deposit, does the mint path still round the user up to one raw share? If yes, how much borrow power does that share carry immediately?
- Where does the protocol consume
balanceOf()on rebasing or indexable tokens for solvency, liquidation, or mint limits, and what stronger object proves that the displayed balance reflects legitimately recognized backing rather than attacker-authored presentation state? - Does liquidation merely settle bad debt, or does it also republish collateral truth for everyone else? If the latter, what independent fact proves the post-liquidation balance sheet still matches the pre-liquidation share ledger?
- If the collateral index jumped 10x to 100x inside one transaction, what exact mechanism would stop the next
managePosition()call from turning that jump into unbacked stablecoin issuance? - Are minimum deposit sizes, minimum raw-share mints, or floor-rounding rules calibrated against worst-case high-index states, or only against normal operating ranges?
- Can one actor intentionally create a thin-share state, donate into the manager, and then be the first actor to mint or borrow against the republished index before anyone else can react?
- What operational alarm or circuit breaker fires when the protocol observes a large index jump unaccompanied by proportionate net-new minted shares or recognized collateral inflow?
Lessons that should already be standard
balanceOf()on a rebasing or indexable collateral token is an oracle surface the moment lending code treats it as solvency truth.- Liquidation code is not just cleanup logic. If it republishes indexes, reallocates numerators, or resets denominators, it is one of the protocol’s live valuation authorities.
- Ceiling rounding is harmless only while the index stays inside ordinary ranges. Under an attacker-inflated index,
divUpturns dust deposits into whole-share mint rights and whole-share mint rights into money-printing. - Ambient contract balances are not the same thing as accounted backing. Donation-sensitive numerators and share-ledger truth must stay separate unless the protocol is willing to treat every raw token transfer as canonical collateral issuance.
- Near-empty denominator states are production hazards, not edge-case trivia. A system can revisit launch-like fragility after liquidations or position closures even if it was safely seeded at deployment.
- Healthy design is stricter than
the collateral index is locally canonical right now. The real requirement isthis published index came from recognized backing, maps cleanly to the surviving share ledger, and is not yet borrowable until that claim survives adversarial timing and rounding tests. - Internal oracle failures can be worse than external ones because teams often stop looking once the price feed appears fine. Raft did not need Chainlink or cbETH market data to fail; it printed its own fake collateral truth and then honored it.
Internal linkages
- Core recurring pattern: donation-share-price-manipulation.
- Publication-authority pattern: state-publication-authority-failure — the direct donation only became borrowable because liquidation-side
_updateDebtAndCollateralIndex()republished raw contract balance as canonical collateral-index truth. - Publication-authority companion: sync-harvest-and-cache-write-authority-discipline — Raft’s index-maintenance path was not neutral upkeep; it was the authority surface that let donated cbETH become immediate borrow-power truth.
- Presentation / collateral timing companion: reward-recognition-presentation-and-collateral-separation — Raft collapsed recognition, presentation, and collateral admission into one rebasing index, so the freshly republished
balanceOf()was both wallet-visible and borrowable in the same breath. - Downstream distrust companion: collateral-admission-lag-and-borrow-side-quarantine — even if Raft wanted the new index to count locally, the healthier boundary was to deny same-step borrow authority to a freshly republished donation-sensitive collateral mark.
- Adjacent pattern: solvency-check-bypass.
Control surface
-
The decisive leverage sat in Raft’s donation-sensitive global collateral-index accounting, the decision to derive that index from raw contract
cbETHbalance after liquidation, the ceil-rounded raw-share mint path, and collateral checks that trusted rebasingbalanceOf()as immediate solvency truth. -
Raft’s publication path and borrow path were effectively the same object: one liquidation republished donated backing into the global index, one
1 weimint claimed a whole raw share under that index, and the next solvency read treated the presentation-layer result as canonical collateral value. -
Raft did not need an external oracle to fail. It let an attacker author the protocol’s own internal collateral mark and then borrow against that fabricated mark before the system could distinguish donated backing from legitimately minted collateral shares.
-
Sources:
- https://rekt.news/raft-rekt
- https://medium.com/neptune-mutual/how-was-raft-protocol-exploited-0a880a608ace
- https://etherscan.io/tx/0xa1378a4d61e81339daaf2c7c8bb669be42002919f10379c616d0aee34047794e
- https://etherscan.io/tx/0xfeedbf51b4e2338e38171f6e19501327294ab1907ab44cfd2d7e7336c975ace7
- https://etherscan.io/address/0x9ab6b21cdf116f611110b048987e58894786c244#code
- https://etherscan.io/address/0x183015a9ba6ff60230fdeadc3f43b3d788b13e21
- https://github.com/raft-fi/contracts/blob/ce916971becccb39da1d60cb9d19d5678e8d8b08/contracts/ERC20Indexable.sol#L42-L67
- https://github.com/raft-fi/contracts/blob/ce916971becccb39da1d60cb9d19d5678e8d8b08/contracts/PositionManager.sol#L169-L210
- https://github.com/raft-fi/contracts/blob/ce916971becccb39da1d60cb9d19d5678e8d8b08/contracts/PositionManager.sol#L589-L599
- https://github.com/raft-fi/contracts/blob/ce916971becccb39da1d60cb9d19d5678e8d8b08/contracts/InterestRates/InterestRatePositionManager.sol#L29-L57
-
Last reviewed: 2026-07-20 UTC