DEV Community

ohmygod
ohmygod

Posted on

The Venus Protocol Donation Attack: How a 9-Month Ambush Turned a $14.5M Supply Cap Into a $53M Trojan Horse — And How to Donation-Proof Your Lending Fork

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Donate 36M+ THE tokens directly to the vTHE contract (bypassing the 14.5M cap)
  2. Exchange rate inflates by ~3.81x overnight
  3. Borrow CAKE, BNB, BTCB, and USDC against the inflated collateral
  4. Swap borrowed assets for more THE on the open market
  5. Donate again — recursive leverage loop
  6. 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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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"
    );
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Does your exchange rate use balanceOf or internal accounting? If balanceOf, you're vulnerable.
  2. Does your supply cap check only fire on mint()? If yes, direct transfers bypass it.
  3. Can a single wallet hold >50% of a market's collateral? If yes, you're exposed to concentration attacks.
  4. Do you have a circuit breaker for exchange rate spikes? If no, donations go undetected.
  5. 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)