On March 15, 2026, a methodical attacker drained approximately $3.7 million from Venus Protocol on BNB Chain — not through a flash loan, not through a reentrancy bug, but through a vulnerability class that has plagued Compound-forked lending protocols since their inception: the donation attack.
The attacker spent nine months preparing. They accumulated 84% of Venus's THE (Thena) supply cap — roughly 14.5 million tokens — starting in June 2025. Then they bypassed Venus's deposit mechanisms entirely, directly transferring THE tokens to the vTHE contract to manipulate the exchange rate. The result: $2.15 million in bad debt for Venus and $3.7 million in extracted assets.
This is not a novel attack vector. Donation attacks against Compound-forked protocols have been documented since 2022. Yet in 2026, one of the largest lending protocols on BNB Chain fell victim to the exact same pattern. Let's understand why — and how to prevent it.
The Mechanics: How Donation Attacks Work
Every Compound-fork lending protocol uses a cToken model (or equivalent — vTokens in Venus's case). When you deposit an asset, you receive a proportional share of the pool represented by cTokens. The exchange rate between cTokens and the underlying asset determines how much you can borrow against your position.
The exchange rate formula:
exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
Where:
-
totalCash= underlying tokens held by the cToken contract -
totalBorrows= outstanding loans from the pool -
totalReserves= protocol-reserved funds -
totalSupply= total cTokens in circulation
Here's the vulnerability: totalCash includes any tokens sent directly to the contract, not just those deposited through the proper mint() function.
// Simplified Compound cToken — the critical flaw
function getCashPrior() internal view returns (uint) {
// This counts ALL tokens the contract holds
// Including tokens sent via direct transfer (donation)
return IERC20(underlying).balanceOf(address(this));
}
function exchangeRateStored() public view returns (uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
return initialExchangeRateMantissa;
}
uint totalCash = getCashPrior();
uint cashPlusBorrows = totalCash + totalBorrows;
uint exchangeRate = (cashPlusBorrows - totalReserves) * 1e18 / _totalSupply;
return exchangeRate;
}
By sending tokens directly to the vTHE contract (a "donation"), the attacker inflates totalCash without increasing totalSupply. This skews the exchange rate upward, making each vTHE token worth far more than it should be.
The Venus Attack: Step by Step
Phase 1: Position Building (June 2025 - March 2026)
Over nine months, the attacker accumulated ~14.5 million THE tokens, representing roughly 84% of Venus's supply cap for this asset. This wasn't cheap — on-chain analysis suggests they spent approximately $9.16 million acquiring THE and vTHE positions, funded through 7,400 ETH from Tornado Cash.
Why nine months? Because accumulating this much of a single token quickly would have triggered price alerts and community attention. Slow accumulation under the radar is a hallmark of sophisticated attacks.
Phase 2: Exchange Rate Manipulation
The attacker directly transferred THE tokens to the vTHE contract, bypassing the standard mint() flow. This inflated the exchange rate dramatically:
Before donation:
totalCash = 17.3M THE
totalSupply = 16.8M vTHE
exchangeRate ≈ 1.03
After donation:
totalCash = 53.2M THE (3.7x the supply cap!)
totalSupply = 16.8M vTHE (unchanged)
exchangeRate ≈ 3.17
The attacker's vTHE holdings were now "worth" 3x what they should have been according to the protocol's accounting.
Phase 3: Borrow and Extract
With massively inflated collateral value, the attacker borrowed assets against their position:
- ~20 BTC
- ~1.5 million CAKE
- ~200 BNB
- Additional USDC
The attacker also used borrowed funds to buy more THE on the open market, driving the spot price from ~$0.27 to nearly $5 — further inflating the oracle-reported collateral value in a feedback loop.
Phase 4: Exit
The borrowed assets were extracted from the protocol, leaving Venus holding overvalued THE collateral that could never be liquidated at the inflated price.
Why Supply Caps Didn't Help
Venus had supply caps configured for THE. But the donation bypass circumvented them entirely:
// Venus's supply cap check occurs in mint()
function mintInternal(uint mintAmount) internal {
// This check exists...
require(
totalSupply + mintTokens <= supplyCap,
"Supply cap reached"
);
// ...but it's never reached when tokens
// are sent directly to the contract
}
// Direct transfer: token.transfer(vTHE_address, amount)
// No mint() called = no supply cap check
This is the fundamental design flaw: supply caps gate the mint() function, but the exchange rate calculation reads raw balanceOf(). Any discrepancy between these two pathways is exploitable.
The Compound-Fork Donation Attack History
This vulnerability class has a long history:
| Date | Protocol | Loss | Variant |
|---|---|---|---|
| Oct 2022 | Hundred Finance | $7.4M | Empty market + donation |
| Apr 2023 | Euler Finance | $197M | Donation + self-liquidation |
| Jul 2023 | Compound V2 (cUSDCv3) | $0 (caught) | Supply cap bypass |
| Nov 2023 | Sonne Finance | $20M | Empty market + frontrun |
| Mar 2026 | Venus Protocol | $3.7M | Supply cap bypass + price manipulation |
The pattern is consistent: every Compound fork that uses raw balanceOf() in its exchange rate calculation inherits this vulnerability. The specific exploitation technique varies, but the root cause is always the same.
Defense Pattern 1: Internal Accounting
The most robust defense: never use balanceOf() for exchange rate calculations. Track deposits and withdrawals internally.
// VULNERABLE: Uses balanceOf()
function getCashPrior() internal view returns (uint) {
return IERC20(underlying).balanceOf(address(this));
}
// SECURE: Uses internal accounting
uint256 internal _totalCash;
function getCashPrior() internal view returns (uint) {
return _totalCash;
}
function mintInternal(uint mintAmount) internal {
// ... validation ...
_totalCash += mintAmount;
// ... mint cTokens ...
}
function redeemInternal(uint redeemTokens) internal {
// ... validation ...
_totalCash -= redeemAmount;
// ... burn cTokens ...
}
Trade-off: Internal accounting requires careful bookkeeping across all code paths (mint, redeem, borrow, repay, liquidate, seize, reserves). A missed update creates its own bugs. But this eliminates the donation attack vector entirely.
Aave V3 uses this approach — its aToken accounting tracks scaled balances internally rather than relying on raw token balances.
Defense Pattern 2: Donation Detection and Neutralization
If switching to internal accounting is too disruptive (common for existing deployments), detect and neutralize donations:
uint256 internal _lastKnownBalance;
function _syncBalance() internal {
uint256 currentBalance = IERC20(underlying).balanceOf(address(this));
if (currentBalance > _lastKnownBalance) {
uint256 donation = currentBalance - _lastKnownBalance;
// Route donations to reserves instead of inflating exchange rate
totalReserves += donation;
emit DonationDetected(msg.sender, donation);
}
_lastKnownBalance = currentBalance;
}
// Call _syncBalance() before any exchange rate calculation
function exchangeRateStored() public view returns (uint) {
_syncBalance();
// ... standard calculation, now donation-proof ...
}
By routing unexpected balance increases to totalReserves, the exchange rate remains unaffected by direct transfers. The donated tokens aren't lost — they become protocol revenue — but they can't be used for manipulation.
Defense Pattern 3: Supply Cap Enforcement at the Balance Level
If you must use balanceOf(), enforce supply caps at the balance level, not just the mint level:
function exchangeRateStored() public view returns (uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
return initialExchangeRateMantissa;
}
uint totalCash = getCashPrior();
// Cap effective cash to prevent donation inflation
uint effectiveCash = Math.min(
totalCash,
_totalSupply * maxExchangeRate / 1e18
);
uint cashPlusBorrows = effectiveCash + totalBorrows;
uint exchangeRate = (cashPlusBorrows - totalReserves) * 1e18 / _totalSupply;
return exchangeRate;
}
This caps the exchange rate regardless of how many tokens are in the contract. A sudden 3x inflation in contract balance would be clamped to the maximum acceptable exchange rate.
Defense Pattern 4: Collateral Factor Scaling for Thin Markets
The Venus attack was amplified by the low liquidity of THE. A lending protocol should dynamically reduce collateral factors for assets with thin markets:
struct AssetConfig {
uint256 baseCollateralFactor; // e.g., 65%
uint256 minLiquidityUSD; // e.g., $5M
uint256 liquidityScaleFactor; // reduction per $1M below minimum
}
function getEffectiveCollateralFactor(
address asset
) public view returns (uint256) {
AssetConfig memory config = assetConfigs[asset];
uint256 liquidity = getMarketLiquidity(asset);
if (liquidity >= config.minLiquidityUSD) {
return config.baseCollateralFactor;
}
// Reduce collateral factor proportionally to liquidity deficit
uint256 deficit = config.minLiquidityUSD - liquidity;
uint256 reduction = deficit * config.liquidityScaleFactor / 1e18;
if (reduction >= config.baseCollateralFactor) {
return 0; // Effectively delist as collateral
}
return config.baseCollateralFactor - reduction;
}
If THE's collateral factor had been dynamically reduced as its on-chain liquidity thinned, the attacker's inflated position would have had far less borrowing power.
Defense Pattern 5: Position Concentration Limits
No single address should control 84% of a market's supply. Period.
uint256 public constant MAX_POSITION_SHARE = 2000; // 20% in basis points
function mintInternal(uint mintAmount) internal {
// ... existing checks ...
// Position concentration check
uint256 newBalance = balanceOf(msg.sender) + mintTokens;
uint256 newShare = newBalance * 10000 / (totalSupply + mintTokens);
require(
newShare <= MAX_POSITION_SHARE,
"Position exceeds concentration limit"
);
}
This wouldn't have prevented the direct transfer bypass, but it would have limited the attacker's legitimate vTHE accumulation during the nine-month preparation phase.
The Oracle Feedback Loop
A critical amplifier in the Venus attack was the oracle feedback loop:
- Attacker inflates vTHE exchange rate via donation
- Attacker borrows against inflated collateral
- Attacker uses borrowed funds to buy more THE on the open market
- THE spot price increases → oracle reports higher price
- Attacker's remaining THE collateral is now worth even more
- Repeat steps 2-5
This feedback loop is only possible when:
- The lending protocol's oracle sources from the same thin market the attacker is manipulating
- There are no circuit breakers on collateral value changes
- Borrowing is not rate-limited per block or per position
Breaking any one of these conditions breaks the loop.
Audit Checklist: Is Your Compound Fork Vulnerable?
If you're running or auditing a Compound-forked lending protocol, check these:
- [ ] Exchange rate calculation: Does it use
balanceOf()or internal accounting? - [ ] Supply cap enforcement: Is it checked only in
mint(), or also against actual contract balance? - [ ] Donation handling: Are unexpected balance increases routed to reserves?
- [ ] Position concentration: Can a single address accumulate >20% of a market?
- [ ] Collateral factor: Is it static or dynamically scaled by liquidity?
- [ ] Oracle independence: Does the price feed source from different markets than the one being collateralized?
- [ ] Borrow velocity limits: Can an attacker open massive borrows in a single block?
- [ ] Empty market initialization: Can the first depositor manipulate the exchange rate? (the Sonne Finance variant)
If any of these boxes are unchecked, you have a donation attack surface.
The Systemic Issue: Compound's Legacy
Compound V2's architecture was groundbreaking in 2020. But its design choices — particularly using balanceOf() for accounting — created a vulnerability template that has been inherited by hundreds of forks across every EVM chain.
Compound V3 (Comet) fixed this by moving to internal accounting. Aave V3 never had the issue. But the long tail of Compound V2 forks — Venus, Benqi, Moonwell, Iron Bank, Cream (RIP), and dozens of smaller protocols — still carry the original DNA.
The Venus exploit cost $3.7 million. Across all Compound-fork donation attacks historically, losses exceed $230 million. And every unpatched fork is another potential victim, waiting for an attacker patient enough to spend nine months preparing.
For protocol teams running Compound forks: Implement internal accounting or donation detection. Today. The attack playbook is public and the cost of preparation is dropping as attackers get more sophisticated tooling.
For auditors: Add donation attack testing to your standard Compound-fork audit checklist. It's a 30-minute check that catches a multi-million dollar vulnerability class.
For users: Check your exposure on Compound-forked protocols with thin-liquidity collateral assets. If the protocol hasn't addressed donation attacks, your deposits are at risk by proxy.
This analysis is part of the DeFi Security Field Notes series. Previously: Oracle Security Design Patterns | Simulation-Execution Divergence
Disclaimer: This article is for educational and defensive security research purposes. Always practice responsible disclosure.
Top comments (0)