TL;DR
On March 22, 2026, an attacker deposited ~$100,000 USDC into Resolv's USR stablecoin protocol and minted approximately 80 million unbacked USR tokens — a 500x amplification. The attacker drained ~$25 million by swapping through DEXs, crashing USR to $0.25. The root cause: the completeSwap function blindly trusted a _mintAmount parameter from an off-chain service without on-chain validation.
This is a textbook case of the trusted oracle anti-pattern — and it happened in production, today.
The Attack Flow
Phase 1: Minimal Collateral Deposit
The attacker address 0x04A288a7789DD6Ade935361a4fB1Ec5db513caEd deposited approximately 100,000 USDC into the USR Counter contract via the requestSwap function. This is a standard entry point — nothing unusual here.
Phase 2: The Amplified Mint
Here's where the vulnerability kicks in. The completeSwap function processed the request and authorized minting of 49.95 million USR tokens — roughly 500x the deposited collateral. A second transaction minted an additional 30 million USR.
The total: ~80 million unbacked USR from a $100K deposit.
Phase 3: Liquidation Cascade
The attacker converted USR → wstUSR → USDC/USDT → ETH across multiple DEXs (Kyber Network, Uniswap). The selling pressure crashed USR from $1.00 to as low as $0.257. Approximately 5,500 ETH (~$25M) was extracted and distributed across multiple addresses:
-
0x6Db6006c38468CDc0fD7d1c251018b1B696232Ed(5,500 ETH) -
0xb945ec1be1f42777f3aa7d683562800b4cdd3890(additional swapped funds)
Resolv Labs paused all protocol functions shortly after.
Root Cause: The Blind Trust Pattern
The vulnerability is architecturally simple and devastating. Resolv's minting flow works in two phases:
User → requestSwap(collateral) → Off-chain SERVICE_ROLE → completeSwap(_mintAmount)
The completeMint/completeSwap function completely trusts the _mintAmount provided by SERVICE_ROLE — an off-chain service that monitors requests and queries an oracle (Pyth) to determine asset values.
Three possible attack vectors exist for this design:
1. Oracle Manipulation
If the Pyth oracle feed was manipulated to report an inflated USDC price, the off-chain service would calculate an amplified mint amount. However, manipulating Pyth for a major asset like USDC is non-trivial.
2. Compromised Off-Chain Validator
The SERVICE_ROLE private key could have been compromised, allowing the attacker to directly call completeSwap with an arbitrary _mintAmount. This is the most likely vector given the 500x amplification.
3. Missing Amount Validation (The Real Bug)
The smart contract had no on-chain sanity check between the collateral deposited in requestSwap and the amount minted in completeSwap. No upper bound. No ratio check. No independent oracle verification on-chain.
// Pseudocode of the vulnerable pattern
function completeSwap(
bytes32 requestId,
uint256 _mintAmount // ← Blindly trusted from SERVICE_ROLE
) external onlyRole(SERVICE_ROLE) {
// No check: _mintAmount <= collateral * maxRatio
// No check: _mintAmount <= totalCollateral * supplyCapRatio
// Just... mint it.
_mint(recipient, _mintAmount);
}
This is the trusted intermediary anti-pattern: the contract outsources a critical security decision to an off-chain component and provides no fallback validation.
Why This Matters: The Recurring Pattern
This isn't a novel attack. It's a well-known anti-pattern that keeps killing protocols:
| Incident | Year | Root Cause | Loss |
|---|---|---|---|
| Wormhole Bridge | 2022 | Missing signature validation | $320M |
| Euler Finance | 2023 | Missing health check | $197M |
| Mango Markets | 2022 | Oracle price manipulation | $114M |
| Resolv USR | 2026 | Blind trust in off-chain mint amount | ~$25M |
The common thread: security-critical validation deferred to a component that can be manipulated.
Defensive Patterns: How to Prevent This
1. On-Chain Invariant Checks
Every minting function should enforce on-chain bounds, regardless of who calls it:
function completeSwap(
bytes32 requestId,
uint256 _mintAmount
) external onlyRole(SERVICE_ROLE) {
SwapRequest memory req = requests[requestId];
// Hard ceiling: mint cannot exceed collateral * maxRatio
uint256 maxMint = req.collateralAmount * MAX_MINT_RATIO / PRECISION;
require(_mintAmount <= maxMint, "Mint exceeds collateral bound");
// Global supply cap check
require(totalSupply() + _mintAmount <= SUPPLY_CAP, "Supply cap exceeded");
_mint(req.recipient, _mintAmount);
}
2. Dual Oracle Verification
Don't rely on a single price source for minting decisions:
function validateMintAmount(
uint256 collateral,
uint256 mintAmount
) internal view {
uint256 pythPrice = pythOracle.getPrice(collateralAsset);
uint256 chainlinkPrice = chainlinkOracle.getPrice(collateralAsset);
// Prices must agree within 2% tolerance
require(
_withinTolerance(pythPrice, chainlinkPrice, 200),
"Oracle divergence"
);
uint256 expectedMint = collateral * pythPrice / 1e18;
require(
mintAmount <= expectedMint * 105 / 100, // 5% max slippage
"Mint amount exceeds oracle valuation"
);
}
3. Rate Limiting and Circuit Breakers
uint256 public constant MAX_MINT_PER_BLOCK = 1_000_000e18; // 1M per block
uint256 public mintedThisBlock;
modifier rateLimited(uint256 amount) {
if (block.number > lastMintBlock) {
mintedThisBlock = 0;
lastMintBlock = block.number;
}
mintedThisBlock += amount;
require(mintedThisBlock <= MAX_MINT_PER_BLOCK, "Rate limit exceeded");
_;
}
4. Time-Locked Large Mints
For amounts above a threshold, enforce a time delay:
if (_mintAmount > LARGE_MINT_THRESHOLD) {
pendingMints[requestId] = PendingMint({
amount: _mintAmount,
recipient: req.recipient,
executeAfter: block.timestamp + TIMELOCK_DELAY
});
emit LargeMintQueued(requestId, _mintAmount);
return; // Don't mint immediately
}
Audit Checklist: Finding This Before Attackers Do
When reviewing stablecoin or token minting contracts, check for:
- [ ] Unbounded mint authority: Can any role mint arbitrary amounts without on-chain validation?
- [ ] Missing collateral-to-mint ratio checks: Is the relationship between deposited collateral and minted tokens enforced on-chain?
- [ ] Single point of oracle failure: Does the contract rely on one oracle or one off-chain service for pricing?
- [ ] No supply caps or rate limits: Can the total supply be inflated without bounds in a single transaction?
- [ ] Two-phase operations without validation: Does
complete*blindly trust parameters fromrequest*without re-validating? - [ ] Privileged role = single key: Is
SERVICE_ROLEa single EOA, or a multisig/MPC?
Timeline
| Time (UTC) | Event |
|---|---|
| ~12:00 Mar 22 | Attacker deposits 100K USDC, mints 49.95M USR |
| ~12:05 | Second mint: 30M additional USR |
| ~12:10-12:30 | USR → wstUSR → USDC/USDT → ETH swaps across DEXs |
| ~12:15 | USR depegs to $0.40, continues falling |
| ~12:30 | USR hits $0.257 on some pools |
| ~12:45 | Resolv Labs pauses protocol |
| ~13:00 | D2 security researchers publish initial analysis |
Key Takeaways
Never blindly trust off-chain services for mint amounts. On-chain invariant checks are your last line of defense.
The 500x amplification should have been caught by any reasonable bound check. A
require(_mintAmount <= collateral * 2)would have stopped this cold.Stablecoin protocols need circuit breakers. If 50 million tokens are minted in one transaction when total supply is orders of magnitude lower, something is wrong.
Two-phase commit patterns (request/complete) need the complete step to re-validate, not just execute. The separation of concerns should add security, not remove it.
This exploit cost $100K to execute. The ROI for the attacker was 250x. That's the asymmetry defenders are up against.
Follow for more DeFi security research. Previous analyses: Aave CAPO Oracle Incident, Solana Noisy Neighbor Attack.
Disclaimer: This analysis is based on publicly available information as of March 22, 2026. The investigation is ongoing. Technical details may be refined as more on-chain data becomes available.
Top comments (0)