Bao Donation Borrow Exploit
Exploit Transactions
0xdd7dd68cd879d07cfc2cb74606baa2a5bf18df0e3bda9f6b43f904f4f7bbdfc1Victim Addresses
0xb0f8fe96b4880adbdede0ddf446bd1e7ef122c4eEthereum0xe853e5c1edf8c51e81bae81d742dd861df596de7Ethereum0x5ee08f40b637417bcc9d2c51b62f4820ec9cf5d8EthereumLoss Breakdown
Similar Incidents
bZx iYFI Donation Inflation
39%ParaSpace cAPE Donation Repricing
37%Euler DAI Reserve Donation
37%Helio Plugin Donation Inflation
33%Ploutos Market Oracle Feed Misconfiguration Enabled Undercollateralized WETH Borrow
32%Conic crvUSD Oracle Exploit
32%Root Cause Analysis
Bao Donation Borrow Exploit
1. Incident Overview TL;DR
On Ethereum mainnet block 17620871, transaction 0xdd7dd68cd879d07cfc2cb74606baa2a5bf18df0e3bda9f6b43f904f4f7bbdfc1 exploited Bao Finance in a single adversary-crafted transaction. An unprivileged EOA funded a helper contract that flash-borrowed USDC and DAI from Aave V2, minted 34,819,000.000000000000000001 bSTBL, manipulated the Bao bdbSTBL collateral market by directly donating bSTBL into a dust-supply market, borrowed 41.3 baoETH from bdbaoETH, recovered the donated bSTBL through a truncated redeemUnderlying path, and exited with 23.515708843994092512 WETH before gas.
The root cause is a Compound-style accounting flaw in Bao's bdbSTBL market. exchangeRateStoredInternal() trusts raw underlying cash, so direct donations increase the exchange rate without minting new cTokens. redeemUnderlying() then converts underlying back to cTokens with truncating division, so the attacker can withdraw nearly all donated collateral while burning too few cTokens. That combination let the attacker present vastly inflated collateral to the Comptroller, borrow baoETH, and still recover the temporary bSTBL used to create the collateral illusion.
2. Key Background
Bao's bSTBL token at 0x5ee08f40b637417bcc9d2c51b62f4820ec9cf5d8 is a basket token backed by Aave aUSDC and aDAI. The basket uses an ExperiPie-style joinPool(uint256) and exitPool(uint256) flow, so a user can mint bSTBL by depositing the basket constituents and later unwind back into the underlying Aave positions.
Bao's lending markets bdbSTBL at 0xb0f8fe96b4880adbdede0ddf446bd1e7ef122c4e and bdbaoETH at 0xe853e5c1edf8c51e81bae81d742dd861df596de7 reuse Compound-style cToken logic. In that model, collateral value depends on the exchange rate between cTokens and underlying, and redeemUnderlying converts an underlying amount into cTokens by division. If the exchange rate can be manipulated upward and the burn calculation rounds down, a small residual cToken balance can appear to represent a very large amount of collateral.
Immediately before the exploit transaction, independent pre-state checks at block 17620870 showed:
bdbSTBL.totalSupply() = 0
bdbSTBL.getCash() = 0
bdbaoETH.getCash() = 1000000000000000000000
So the exploitable dust state did not already exist on-chain. The attacker created it inside the same transaction by minting with 1 wei of bSTBL and redeeming 3 of the 5 minted cTokens.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is an accounting attack, not a pricing oracle failure. Bao's bdbSTBL market inherited Compound logic that computes the exchange rate from totalCash + totalBorrows - totalReserves divided by total cToken supply. Because totalCash comes from the market's raw token balance, a direct transfer of bSTBL into the market raises the exchange rate without minting additional bdbSTBL. Once the attacker reduced live cToken supply to dust, that donation made each remaining cToken represent an enormous amount of notional collateral. The attacker then borrowed 41.3 baoETH from bdbaoETH against that inflated collateral. Finally, redeemUnderlying() used truncating division and burned only 1 bdbSTBL while returning almost the entire donated 34,819,000 bSTBL, leaving the borrow outstanding and preserving the profit path. The violated invariant is straightforward: direct underlying donations must not increase borrowable collateral unless matching shares are minted, and redeeming underlying must burn enough shares to keep post-redeem collateral accounting correct.
The core victim code path is below:
function exchangeRateStoredInternal() internal view returns (MathError, uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
return (MathError.NO_ERROR, initialExchangeRateMantissa);
} else {
uint totalCash = getCashPrior();
(mathErr, cashPlusBorrowsMinusReserves) = addThenSubUInt(totalCash, totalBorrows, totalReserves);
(mathErr, exchangeRate) = getExp(cashPlusBorrowsMinusReserves, _totalSupply);
return (MathError.NO_ERROR, exchangeRate.mantissa);
}
}
function redeemFresh(address payable redeemer, uint redeemTokensIn, uint redeemAmountIn) internal returns (uint) {
(vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal();
(vars.mathErr, vars.redeemTokens) =
divScalarByExpTruncate(redeemAmountIn, Exp({mantissa: vars.exchangeRateMantissa}));
uint allowed = comptroller.redeemAllowed(address(this), redeemer, vars.redeemTokens);
...
}
This is sufficient to explain the exploit. Donation changes totalCash; the burn path rounds redeemAmount / exchangeRate down; and the Comptroller approves redemption using the truncated burn amount.
4. Detailed Root Cause Analysis
The attacker first sourced temporary capital through Aave V2 at 0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9. The validated operation summary shows two flash-loaned assets: 17,550,000 USDC and 17,510,000 DAI. Those assets were deposited into Aave and turned into aUSDC and aDAI, then fed into bSTBL.joinPool(34819000000000000000000001) to mint 34,819,000.000000000000000001 bSTBL.
The bSTBL basket logic used in the exploit is conventional proportional minting and burning:
function joinPool(uint256 _amount) external override noReentry {
...
uint256 tokenAmount = balance(address(token)).mul(_amount.add(feeAmount)).div(totalSupply);
token.safeTransferFrom(msg.sender, address(this), tokenAmount);
...
LibERC20.mint(msg.sender, _amount);
}
function exitPool(uint256 _amount) external override virtual noReentry {
...
uint256 tokenAmount = tokenBalance.mul(_amount.sub(feeAmount)).div(totalSupply);
token.safeTransfer(msg.sender, tokenAmount);
...
LibERC20.burn(msg.sender, _amount);
}
With bSTBL in hand, the attacker created the dust-collateral state inside bdbSTBL. A mint(1, true) call minted 5 bdbSTBL at the initial exchange rate, then redeem(3) reduced live supply to 2. That left the market in a state where a direct donation could dominate the exchange rate calculation.
The validated exploit-step summary is:
[
{"step":"flashloan","USDC":"17550000000000","DAI":"17510000000000000000000000"},
{"step":"join_pool","minted_bstbl":"34819000000000000000000001"},
{"step":"dust_mint_collateral","underlying_supplied":"1","minted_bdbstbl":"5"},
{"step":"reduce_ctoken_supply","redeemed_bdbstbl":"3"},
{"step":"donate_bstbl_to_market","amount":"34819000000000000000000000"},
{"step":"borrow_baoeth","borrowed_baoeth":"41300000000000000000"},
{"step":"redeem_underlying_after_donation","redeemed_bstbl":"34819000000000000000000000","burned_bdbstbl":"1","remaining_bdbstbl":"1"},
{"step":"profit_transfer","amount":"23515708843994092512"}
]
The donation step is the actual breakpoint. exchangeRateStoredInternal() reads raw getCashPrior(), so transferring 34,819,000 bSTBL directly into bdbSTBL inflated the exchange rate without changing cToken supply. The attacker then borrowed 41.3 baoETH from bdbaoETH, a market that had 1,000 baoETH of available pre-state liquidity.
The exploit completed when the attacker called redeemUnderlying(34,819,000 bSTBL). redeemFresh() computed:
redeemTokens = floor(redeemAmount / inflatedExchangeRate)
Because the denominator had been artificially inflated by the donation, the burn amount rounded down to 1 bdbSTBL. redeemAllowed() only observed that truncated burn amount, approved the redemption, and the market transferred out essentially the full donation. The market was left with 1 cToken total supply and 1 wei of bSTBL cash, which proves the donation was recovered rather than locked as real collateral.
bdbaoETH itself is not a separately verified implementation artifact in the collector seed, but the validation evidence resolves that deterministically. The market address 0xe853e5c1edf8c51e81bae81d742dd861df596de7 is an unverified delegator proxy whose storage slot 19 contains implementation address 0xdb3401bef8f66e7f6cd95984026c26a4f47eee84, the verified CErc20Delegate implementation used elsewhere in this incident analysis. That is why the same code path governs both markets.
5. Adversary Flow Analysis
The adversary cluster consists of EOA 0x00693a01221a5e93fb872637e3a9391ef5f48300 and helper contract 0x3f99d5cd830203a3027eb0ed6548db7f81c3408f. The EOA sent the exploit transaction; the helper contract executed the entire on-chain sequence and later transferred WETH profit back to the EOA.
The lifecycle was:
- Temporary capital acquisition. The helper contract borrowed USDC and DAI from Aave V2 in one flash loan.
- Basket minting. It deposited the flash-loaned assets into Aave, received aUSDC and aDAI, and minted
34,819,000.000000000000000001bSTBL through the basket. - Collateral inflation. It minted
5bdbSTBLwith1wei of bSTBL, redeemed3, then donated34,819,000bSTBL directly intobdbSTBL. - Borrow and collateral recovery. It borrowed
41.3baoETH frombdbaoETH, then redeemed34,819,000bSTBL frombdbSTBLwhile burning only1bdbSTBL. - Unwind and profit realization. It exited the bSTBL basket back to Aave assets, withdrew the Aave positions, swapped the borrowed baoETH to WETH through Balancer, swapped part of the WETH to USDC and DAI through Uniswap V3 to cover flash-loan premiums, repaid Aave, and transferred
23.515708843994092512WETH to the originating EOA.
The balance-diff evidence confirms the economic result. bdbaoETH lost 41.3 baoETH, the attacker contract ended with 1 bdbSTBL, and the EOA's native ETH balance fell by 0.631948505007324021 ETH in gas while receiving the WETH profit transfer.
6. Impact & Losses
The direct measurable loss was borne by the bdbaoETH market, which lost 41.3 baoETH:
{
"token_symbol": "baoETH",
"amount": "41300000000000000000",
"decimal": 18
}
The broader impact was a collapse of collateral integrity in bdbSTBL. A direct donation and floor-truncated redemption allowed the attacker to borrow against collateral value that did not correspond to a durable cToken claim on market assets. The exploit required no privileged keys, no victim-signed transaction, and no private attack-only infrastructure beyond standard public transaction submission, so it qualifies as an ACT opportunity.
7. References
- Seed exploit transaction:
0xdd7dd68cd879d07cfc2cb74606baa2a5bf18df0e3bda9f6b43f904f4f7bbdfc1in Ethereum block17620871. - Adversary EOA:
0x00693a01221a5e93fb872637e3a9391ef5f48300. - Adversary helper contract:
0x3f99d5cd830203a3027eb0ed6548db7f81c3408f. - Victim markets:
bdbSTBL0xb0f8fe96b4880adbdede0ddf446bd1e7ef122c4e,bdbaoETH0xe853e5c1edf8c51e81bae81d742dd861df596de7. - Supporting protocol contracts: Aave V2
0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9, bSTBL0x5ee08f40b637417bcc9d2c51b62f4820ec9cf5d8, baoETH0xf4edfad26ee0d23b69ca93112ecce52704e0006f, Balancer Vault0xba12222222228d8ba445958a75a0704d566bf2c8, Uniswap V3 Router0xe592427a0aece92de3edee1f18e0157c05861564. - Evidence used during validation: seed transaction metadata, trace log, balance diff, selected exploit call-trace summary, block-
17620870pre-state checks, andbdbaoETHproxy-verification evidence resolving implementation address0xdb3401bef8f66e7f6cd95984026c26a4f47eee84.