On March 15, 2026, Venus Protocol on BNB Chain lost $3.7 million to one of the most patient exploits in DeFi history. The attacker spent nine months quietly accumulating THENA tokens, eventually controlling 84% of the Venus supply cap — then bypassed the cap entirely with a single well-known trick: direct token donation.
This wasn't a zero-day. It wasn't a flash loan blitz. It was a slow-motion siege that exploited a vulnerability the Compound codebase has carried since 2020. And Venus had already been hit by the same attack pattern on its zkSync deployment in February 2025.
Here's the full anatomy — and a defense playbook so your Compound fork doesn't become the next victim.
The Setup: 9 Months of Patience
The attacker's wallet, funded through Tornado Cash, began accumulating THE tokens in mid-2025. By March 2026, they held enough to execute the attack:
- Supply cap: 14.5 million THE tokens
- Attacker's position: ~84% of cap already deposited as collateral
- Goal: Bypass the cap and inflate collateral value
The Attack: Supply Cap? What Supply Cap?
Here's where the elegance — and the horror — lies.
Venus, like most Compound V2 forks, calculates exchange rates using the actual token balance of the cToken contract:
// Simplified Compound V2 exchange rate calculation
function exchangeRateStored() public view returns (uint) {
uint totalCash = getCash(); // <- ERC20.balanceOf(address(this))
uint totalBorrows = totalBorrows;
uint totalReserves = totalReserves;
return (totalCash + totalBorrows - totalReserves) / totalSupply;
}
The supply cap check only fires on the mint() function — when users deposit through the normal path. But direct ERC20 transfers (token.transfer(vToken, amount)) bypass mint() entirely while still inflating getCash().
The attacker's playbook:
- Donate 36M+ THE tokens directly to the vTHE contract (bypassing the 14.5M cap)
- Exchange rate inflates by ~3.81x overnight
- Borrow CAKE, BNB, BTCB, and USDC against the inflated collateral
- Swap borrowed assets for more THE on the open market
- Donate again — recursive leverage loop
- THE price pumps from $0.263 → $0.51+ on the open market from buy pressure
Total THE in the contract: 53.23 million — 367% of the supply cap.
The Unwinding: Everyone Loses
When THE's price inevitably collapsed:
- 254 liquidation bots competed across 8,048 transactions
- 42 million THE liquidated as collateral
- $2.15 million in bad debt left on Venus's books
- The attacker themselves lost ~$4.7M net (spent $9.92M accumulating THE)
The attacker's net loss suggests this may have been a market manipulation play gone wrong, or that off-chain profits (short positions on THE elsewhere) were the real target.
Why This Keeps Happening
The donation attack isn't new. Here's the timeline:
| Date | Protocol | Loss | Root Cause |
|---|---|---|---|
| Oct 2023 | Hundred Finance | $7.4M | Empty market donation |
| Apr 2024 | Sonne Finance | $20M | Donation before first deposit |
| Feb 2025 | Venus (zkSync) | ~$500K | Same pattern |
| Mar 2026 | Venus (BNB Chain) | $3.7M | Same. Exact. Pattern. |
The Compound V2 architecture conflates deposited collateral with contract balance. Any token sent to the contract inflates everyone's exchange rate, regardless of the supply cap.
The Defense Playbook: 6 Layers to Donation-Proof Your Fork
1. Internal Accounting (The Nuclear Fix)
Stop using balanceOf(address(this)) as the source of truth:
// BAD: Reads actual balance (includes donations)
function getCash() internal view returns (uint) {
return underlying.balanceOf(address(this));
}
// GOOD: Track deposits explicitly
uint256 internal _totalCashInternal;
function getCash() internal view returns (uint) {
return _totalCashInternal; // Ignores direct transfers
}
function mint(uint amount) external {
// ... supply cap check ...
underlying.transferFrom(msg.sender, address(this), amount);
_totalCashInternal += amount; // Only counts legitimate deposits
}
This is how Aave V3 and Compound V3 (Comet) handle it. If you're still on V2 architecture, this is the migration that matters most.
2. Exchange Rate Caps
Add a ceiling to how fast the exchange rate can grow:
uint256 public constant MAX_EXCHANGE_RATE_GROWTH_PER_BLOCK = 1e15; // 0.1% per block
uint256 public lastExchangeRate;
uint256 public lastExchangeRateBlock;
modifier exchangeRateGuard() {
_;
uint256 currentRate = exchangeRateStored();
uint256 blocksDelta = block.number - lastExchangeRateBlock;
uint256 maxRate = lastExchangeRate *
(1e18 + MAX_EXCHANGE_RATE_GROWTH_PER_BLOCK * blocksDelta) / 1e18;
require(currentRate <= maxRate, "Exchange rate growth too fast");
lastExchangeRate = currentRate;
lastExchangeRateBlock = block.number;
}
3. Donation Detection Circuit Breaker
Monitor for balance-vs-accounting divergence and auto-pause:
function _checkDonationThreshold() internal view {
uint256 actualBalance = underlying.balanceOf(address(this));
uint256 expectedBalance = _totalCashInternal + totalReserves;
uint256 excess = actualBalance > expectedBalance ?
actualBalance - expectedBalance : 0;
// If >5% excess tokens detected, pause borrows
require(
excess * 100 / expectedBalance < 5,
"Suspicious donation detected — borrows paused"
);
}
4. Time-Weighted Borrow Limits
Don't let users borrow against freshly inflated collateral:
mapping(address => uint256) public lastCollateralUpdate;
function borrow(uint amount) external {
require(
block.timestamp - lastCollateralUpdate[msg.sender] > 1 hours,
"Collateral too fresh"
);
// ... borrow logic
}
5. Supply Cap on Contract Balance, Not Just Mints
function accrueInterest() public {
// ... existing logic ...
// Also check actual balance against cap
uint256 actualSupply = underlying.balanceOf(address(this));
if (actualSupply > supplyCap * 120 / 100) { // 20% buffer
_pauseBorrows();
emit SupplyCapBreached(actualSupply, supplyCap);
}
}
6. Low-Liquidity Token Isolation
Venus's mistake was allowing THE — a token the attacker could corner 84% of — as collateral in a shared pool. For low-liquidity tokens:
- Isolated lending pools (attacker can only borrow against their own deposits)
- Dynamic collateral factors that decrease as single-wallet concentration increases
- Oracle circuit breakers that freeze the asset if price moves >30% in 24h
Audit Checklist: 5 Questions for Your Compound Fork
-
Does your exchange rate use
balanceOfor internal accounting? IfbalanceOf, you're vulnerable. -
Does your supply cap check only fire on
mint()? If yes, direct transfers bypass it. - Can a single wallet hold >50% of a market's collateral? If yes, you're exposed to concentration attacks.
- Do you have a circuit breaker for exchange rate spikes? If no, donations go undetected.
- Has the same attack worked on your codebase before? Venus's zkSync deployment was hit in Feb 2025. The BNB Chain deployment used the same code. Twelve months later: same exploit, bigger loss.
The Uncomfortable Truth
Venus had every piece of information needed to prevent this:
- The Compound V2 donation vulnerability was publicly documented in 2022
- Their own zkSync deployment was exploited in February 2025
- The attacker spent 9 months accumulating tokens on-chain — visible to anyone monitoring
This isn't a failure of security research. It's a failure of operational follow-through. The fix (internal accounting) has been known for years. The question for every Compound fork team reading this: have you actually shipped it?
DreamWork Security publishes weekly DeFi exploit analyses and defense playbooks. Follow @ohmygod for the next deep dive.
Top comments (0)