In March 2026 alone, donation attacks drained over $6 million across two separate DeFi protocols. First, the sDOLA LlamaLend market lost $239K when an attacker manipulated vault share pricing through a direct token transfer. Days later, Venus Protocol on BNB Chain suffered $3.7 million in losses from the same fundamental pattern — despite a prior audit flagging the exact vector.
These aren't isolated incidents. They're symptoms of a structural vulnerability in how DeFi protocols calculate asset-to-share exchange rates. If your protocol holds tokens in a contract and uses balanceOf() to determine value, you're probably vulnerable.
Let me show you exactly how this attack works, why it keeps slipping past audits, and the four-layer defense architecture that actually stops it.
The Anatomy of a Donation Attack
A donation attack exploits a fundamental mismatch: the difference between tracked deposits (what the protocol knows about) and actual balances (what the contract holds). When a protocol uses the contract's token balance to determine exchange rates, an attacker can manipulate those rates by sending tokens directly to the contract — no function call required.
The Core Mechanic
// Vulnerable exchange rate calculation
function exchangeRate() public view returns (uint256) {
return totalAssets() / totalShares;
}
function totalAssets() public view returns (uint256) {
// This is the problem — balanceOf includes donations
return underlying.balanceOf(address(this));
}
The attacker's playbook:
- Deposit a small amount to receive shares
-
Donate tokens directly to the contract via
transfer() - The exchange rate inflates because
totalAssets()increased buttotalSharesdidn't - Exploit the inflated rate — either by withdrawing more than deposited, or by triggering liquidations on other users whose collateral ratios shifted
Why transfer() Breaks Everything
ERC-20's transfer() function is permissionless. Anyone can send tokens to any address without the recipient's contract logic executing. This means:
- No deposit hooks fire
- No internal accounting updates
- No access control checks
- The contract's
balanceOf()changes silently
// Attacker donates 1000 USDC directly
usdc.transfer(address(vault), 1000e6);
// Vault's exchange rate just changed without any function being called
// Previous: 1 share = 1 USDC
// After donation: 1 share = 1001 USDC (if only 1 share exists)
Case Study 1: sDOLA LlamaLend — Oracle Manipulation via Donation
Date: March 2, 2026
Loss: ~$239,000
Chain: Ethereum
The sDOLA LlamaLend market used vault share pricing to determine collateral values. The attacker:
- Took a flash loan
- Donated tokens to inflate the sDOLA exchange rate from ~1.188 to ~1.358 sDOLA per DOLA — a 14% shift
- This created a divergence between the oracle price and the actual market price
- Borrowers who appeared healthy before the donation were suddenly undercollateralized
- The attacker liquidated these positions at a discount
The particularly insidious aspect: lenders were unaffected. Only borrowers using sDOLA as collateral got liquidated. The attacker essentially weaponized the price oracle against specific positions.
Curve Finance has since confirmed they're redesigning LlamaLend V2's oracle architecture to resist donation-based price manipulation for all vault collateral markets.
Case Study 2: Venus Protocol — 9 Months of Patience
Date: March 16, 2026
Loss: ~$3.7 million ($2.15M in bad debt)
Chain: BNB Chain
The Venus attack was more sophisticated — the attacker spent nine months accumulating THE tokens (Thena's governance token) before executing:
- Accumulated a large THE position over 9 months
- Donated THE tokens directly to the vTHE contract
- This bypassed supply cap controls — the caps checked deposits, not balances
- The inflated vTHE exchange rate made the attacker's collateral appear worth far more
- Borrowed against the inflated collateral value
- Walked away with $3.7M; protocol absorbed $2.15M in bad debt
The worst part: A previous audit had flagged this exact donation vector. The Venus team dismissed it as "harmless." The auditor's finding, word for word, described what happened nine months later.
// What the audit flagged (simplified):
// "Direct token transfers to the vToken contract
// will inflate the exchange rate without updating
// supply caps, allowing excess borrowing."
//
// Venus team response: "Won't fix — considered acceptable risk"
Why Donation Attacks Keep Slipping Past Audits
1. The "Obvious But Overlooked" Problem
Every auditor knows about first-depositor inflation attacks on ERC-4626 vaults. But donation attacks are the generalized version — they affect any contract that uses balanceOf() for pricing. The audit industry has a blind spot for generalizing known vulnerability patterns to new contexts.
2. Supply Caps Don't Help
Venus had supply caps. They protected against deposits exceeding thresholds. But donations bypass the deposit path entirely:
function deposit(uint256 amount) external {
require(totalSupply + amount <= supplyCap, "Cap exceeded"); // ✅ Checked
// ...
}
// But transfer() never hits this function
// underlying.transfer(address(this), amount) → no cap check
3. Time-Delayed Attacks Defeat Monitoring
The Venus attacker accumulated tokens over 9 months. Most monitoring systems look for flash loan attacks within single transactions. A patient attacker who builds a position slowly is invisible to traditional alerting.
4. Cross-Contract Composition
The vulnerability often exists not in a single contract, but in the interaction between contracts. LlamaLend's lending logic was fine. sDOLA's vault logic was fine. The vulnerability existed in how LlamaLend used sDOLA's exchange rate as a price input.
The 4-Layer Defense Architecture
Here's the defense stack that actually works against donation attacks:
Layer 1: Internal Accounting (Mandatory)
Never use balanceOf() for pricing or exchange rate calculations. Track deposits and withdrawals explicitly:
contract SecureVault {
uint256 private _totalManagedAssets; // Internal tracker
function totalAssets() public view returns (uint256) {
// Use internal accounting, not balanceOf
return _totalManagedAssets;
}
function deposit(uint256 assets) external {
_totalManagedAssets += assets;
// ... mint shares
}
function withdraw(uint256 assets) external {
_totalManagedAssets -= assets;
// ... burn shares
}
// Donated tokens are invisible to the exchange rate
// They can be swept by governance later
}
This is the single most important defense. If you do nothing else, do this.
Layer 2: Virtual Shares and Assets (ERC-4626 Specific)
For tokenized vaults, add virtual offsets to prevent exchange rate manipulation at low total supply:
function _convertToShares(uint256 assets) internal view returns (uint256) {
uint256 supply = totalSupply();
return assets.mulDiv(
supply + 10 ** _decimalsOffset(), // Virtual shares
totalAssets() + 1, // Virtual asset
Math.Rounding.Down
);
}
OpenZeppelin's ERC-4626 implementation includes this since v4.9. If you're not using it, you should be.
Layer 3: Oracle Hardening (For Lending Protocols)
When using vault shares as collateral, never trust the vault's self-reported exchange rate alone:
contract HardenedOracle {
uint256 public constant MAX_RATE_CHANGE_PER_BLOCK = 10; // 0.1% max
uint256 private _lastRate;
uint256 private _lastBlock;
function getPrice() external returns (uint256) {
uint256 currentRate = vault.exchangeRate();
uint256 maxRate = _lastRate * (10000 + MAX_RATE_CHANGE_PER_BLOCK) / 10000;
if (currentRate > maxRate && block.number == _lastBlock + 1) {
// Rate spiked suspiciously — use capped rate
return maxRate;
}
_lastRate = currentRate;
_lastBlock = block.number;
return currentRate;
}
}
Aave's CAPO (Correlated Assets Price Oracle) module implements this pattern. Even when it had a misconfiguration in March 2026 that caused $27M in liquidations, the concept is sound — the fix was a parameter adjustment, not an architecture change.
Layer 4: Balance Invariant Checks
Add assertions that catch unexpected balance changes:
modifier balanceInvariant() {
uint256 balanceBefore = underlying.balanceOf(address(this));
_;
uint256 balanceAfter = underlying.balanceOf(address(this));
uint256 expectedChange = _expectedBalanceChange;
require(
balanceAfter == balanceBefore + expectedChange ||
balanceAfter == balanceBefore - expectedChange,
"Unexpected balance change detected"
);
_expectedBalanceChange = 0;
}
This won't prevent donations (you can't stop transfer()), but it will prevent your contract from acting on manipulated balances during critical operations.
The Audit Checklist
For auditors and developers reviewing lending protocols, here's the donation attack checklist:
- [ ] Does any function use
balanceOf(address(this))for pricing? → Flag it - [ ] Are supply caps enforced at the deposit level only? → Caps bypass via donation
- [ ] Does the protocol accept vault tokens as collateral? → Check exchange rate oracle
- [ ] Is there a maximum rate-of-change limit on exchange rates? → Missing = vulnerable
- [ ] Can the exchange rate be manipulated within a single transaction? → Flash loan risk
- [ ] Is there a minimum deposit/share threshold? → Prevents micro-position inflation
- [ ] Does governance have a sweep function for excess balances? → Good hygiene
The Bigger Picture: Implicit Trust in Token Balances
The donation attack pattern reveals a deeper philosophical issue in smart contract design: the ERC-20 standard allows anyone to modify your contract's state without calling your contract's functions.
Every time you write token.balanceOf(address(this)), you're trusting that the balance reflects only intentional interactions. That trust is misplaced. The balance is a global variable that any address can increment.
This is why internal accounting isn't just a best practice — it's a security requirement. The March 2026 incidents prove that even audited, battle-tested protocols fall to this pattern when they rely on balanceOf() as a source of truth.
Conclusion
Donation attacks aren't new. The first-depositor attack variant has been known since 2022. But the pattern keeps evolving:
- 2022-2023: First-depositor attacks on ERC-4626 vaults
- 2024-2025: Exchange rate manipulation in lending protocol collateral
- 2026: Multi-month patience attacks that bypass all automated detection
The defense is straightforward: internal accounting, virtual shares, oracle rate limiting, and balance invariant checks. The hard part is convincing teams to implement all four layers — especially when an auditor flags the issue and the response is "won't fix."
Venus's $3.7 million lesson should be the last time we need to learn this one.
This analysis is based on publicly available incident reports and on-chain data. All code examples are simplified for clarity and should not be used in production without thorough review.
Tags: #security #defi #blockchain #smartcontracts
Top comments (0)