$137M Gone in 90 Days. Same Bugs, Different Protocols.
Q1 2026 has been devastating for DeFi. Fifteen protocols, $137 million drained — and the heartbreaking part is that most of these exploits reuse the same handful of anti-patterns that the industry has known about for years.
I've analyzed every major exploit from January through March 2026 and distilled them into the five recurring anti-patterns that account for the vast majority of losses. For each one, I'll show you the vulnerable code, explain the attack vector, and give you the exact fix.
Anti-Pattern #1: Unbounded Minting Without Supply Caps
Real-world cost: $25M+ (Resolv Labs, March 2026)
Resolv's delta-neutral stablecoin protocol allowed an attacker to mint tens of millions of unbacked USR tokens. The root cause? A minting function that trusted external parameters without enforcing hard supply caps at the contract level.
The Vulnerable Pattern
// ❌ VULNERABLE: No supply cap enforcement
function mint(address to, uint256 amount) external onlyMinter {
_mint(to, amount);
emit Minted(to, amount);
}
The assumption is that onlyMinter (an external key or service) will always behave correctly. When the minter's key management service was compromised, nothing stopped the attacker from minting unlimited tokens.
The Fix
// ✅ FIXED: Hard supply cap + per-epoch rate limiting
uint256 public constant MAX_SUPPLY = 500_000_000e18;
uint256 public constant EPOCH_MINT_LIMIT = 10_000_000e18;
mapping(uint256 => uint256) public epochMinted;
function mint(address to, uint256 amount) external onlyMinter {
require(totalSupply() + amount <= MAX_SUPPLY, "Supply cap exceeded");
uint256 epoch = block.timestamp / 1 hours;
epochMinted[epoch] += amount;
require(epochMinted[epoch] <= EPOCH_MINT_LIMIT, "Epoch limit exceeded");
_mint(to, amount);
emit Minted(to, amount);
}
Key principle: Never rely solely on off-chain access control for critical state changes. On-chain invariants must hold even if every privileged key is compromised.
Anti-Pattern #2: Supply Cap Bypass via Deposit Sequencing
Real-world cost: $3.7M (Venus Protocol, March 2026)
Venus Protocol had supply caps — but the attacker bypassed them by exploiting the order in which deposits and cap checks were evaluated. The cap was checked against the pre-deposit state, then the deposit increased the supply beyond the cap.
The Vulnerable Pattern
// ❌ VULNERABLE: Check-then-act with stale state
function deposit(address token, uint256 amount) external {
require(totalSupply[token] <= supplyCap[token], "Cap reached");
// ^ Checks BEFORE the deposit is applied
totalSupply[token] += amount;
_transferIn(token, amount);
_mintShares(msg.sender, amount);
}
The Fix
// ✅ FIXED: Check-after-effect pattern
function deposit(address token, uint256 amount) external {
totalSupply[token] += amount;
// Check AFTER state mutation
require(totalSupply[token] <= supplyCap[token], "Cap exceeded");
_transferIn(token, amount);
_mintShares(msg.sender, amount);
}
Key principle: For invariant checks (like caps), validate the post-mutation state, not the pre-mutation state. The EVM will revert all state changes if the require fails, so there's no risk in mutating first.
Anti-Pattern #3: _msgSender() vs msg.sender Inconsistency
Real-world cost: $149K (DBXen, March 2026)
This is a subtle but dangerous pattern in contracts that use ERC-2771 meta-transaction forwarders. When some functions use _msgSender() and others use raw msg.sender, an attacker can create identity confusion.
The Vulnerable Pattern
// ❌ VULNERABLE: Mixed sender resolution
contract VulnerableProtocol is ERC2771Context {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[_msgSender()] += msg.value;
}
function claimReward() external {
uint256 reward = _calculateReward(msg.sender);
balances[msg.sender] += reward;
}
}
The Fix
// ✅ FIXED: Consistent sender resolution everywhere
contract FixedProtocol is ERC2771Context {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[_msgSender()] += msg.value;
}
function claimReward() external {
address sender = _msgSender();
uint256 reward = _calculateReward(sender);
balances[sender] += reward;
}
}
Key principle: In any contract inheriting ERC2771Context, grep your entire codebase for raw msg.sender and replace every instance with _msgSender(). No exceptions.
Anti-Pattern #4: Flawed Delayed-Action Mechanisms
Real-world cost: $131K (AM Token) + similar patterns in other exploits
The AM Token exploit on BNB Chain abused a delayed-burn mechanism where tokens are marked for destruction but not immediately burned. The window between marking and execution created an exploitable state.
The Vulnerable Pattern
// ❌ VULNERABLE: Delayed burn with transfer window
mapping(address => uint256) public pendingBurn;
function transfer(address to, uint256 amount) public override returns (bool) {
if (pendingBurn[msg.sender] > 0) {
_burn(msg.sender, pendingBurn[msg.sender]);
pendingBurn[msg.sender] = 0;
}
return super.transfer(to, amount);
}
function scheduleBurn(address account, uint256 amount) external onlyOwner {
pendingBurn[account] += amount;
}
The Fix
// ✅ FIXED: Immediate escrow on scheduled burns
function scheduleBurn(address account, uint256 amount) external onlyOwner {
_transfer(account, address(this), amount);
totalPendingBurn += amount;
emit BurnScheduled(account, amount);
}
function executeBurn() external {
_burn(address(this), totalPendingBurn);
totalPendingBurn = 0;
}
Key principle: Any delayed-action mechanism must escrow or lock the affected assets immediately. The delay should only affect the final execution, never the initial restriction.
Anti-Pattern #5: Stale Oracle Prices in Liquidation Logic
Real-world cost: $1M+ (AAVE oracle incident) + $10.97M (YieldBlox)
Oracle-related exploits remain the #1 category by total losses in 2026. The AAVE incident involved a CAPO configuration where timestamp validation mismatches between different oracle sources created windows where liquidations used stale prices.
The Vulnerable Pattern
// ❌ VULNERABLE: No staleness check, no cross-oracle validation
function getPrice(address asset) public view returns (uint256) {
(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(price > 0, "Invalid price");
return uint256(price);
}
The Fix
// ✅ FIXED: Multi-layer oracle defense
uint256 public constant MAX_PRICE_AGE = 1 hours;
uint256 public constant MAX_DEVIATION_BPS = 500; // 5%
function getValidatedPrice(address asset) public view returns (uint256) {
(, int256 price, , uint256 updatedAt, ) =
primaryFeed[asset].latestRoundData();
require(price > 0, "Invalid price");
require(block.timestamp - updatedAt <= MAX_PRICE_AGE, "Stale price");
uint256 backupPrice = backupOracle[asset].getPrice();
uint256 deviation = _percentDiff(uint256(price), backupPrice);
require(deviation <= MAX_DEVIATION_BPS, "Oracle deviation too high");
uint256 twap = twapOracle[asset].consult(asset, 30 minutes);
uint256 twapDeviation = _percentDiff(uint256(price), twap);
require(twapDeviation <= MAX_DEVIATION_BPS * 2, "TWAP deviation");
return uint256(price);
}
Key principle: Implement defense-in-depth for oracle prices: staleness checks, cross-oracle validation, and TWAP sanity bounds.
The Meta-Pattern: Why These Keep Happening
| Anti-Pattern | Root Cause |
|---|---|
| Unbounded minting | Trusting off-chain controls |
| Supply cap bypass | Check-then-act ordering |
| Sender inconsistency | Implicit assumptions |
| Delayed burns | Temporal state gaps |
| Oracle staleness | Single point of failure |
They all share one trait: the contract assumes something external will behave correctly. The moment your contract's safety depends on something it can't verify on-chain, you have a ticking time bomb.
Your Pre-Deploy Checklist
- [ ] Mint/burn functions have hard on-chain caps and rate limits
- [ ] Supply caps are validated against post-mutation state
- [ ] Every function uses consistent sender resolution
- [ ] Delayed actions escrow/lock assets immediately upon scheduling
- [ ] Oracle prices have staleness checks, backup validation, and TWAP bounds
- [ ] All critical invariants hold even if every privileged key is compromised
- [ ] Static analysis with Slither, Sec3 X-ray, or Radar has been run
- [ ] At least one independent audit from a reputable firm
This analysis is part of the DeFi Security Research series. Data sourced from on-chain analysis and incident reports from BlockSec, Chainalysis, and protocol post-mortems.
Follow for weekly breakdowns of DeFi exploits and actionable security guidance.
Top comments (0)