TL;DR
On March 22, 2026, an attacker compromised Resolv Labs AWS KMS key, minted 80 million unbacked USR stablecoins from ~$200K in deposits, extracted $25M in ETH, and crashed USR peg by 80%. The smart contract worked exactly as designed. The vulnerability was entirely in the off-chain trust model. This article dissects the attack step-by-step and presents five off-chain trust boundaries that would have prevented this.
The Attack: Step by Step
The Setup
Resolv USR stablecoin uses a two-step minting flow:
- requestSwap() — User deposits USDC into the Counter contract
- completeSwap() — An off-chain service with SERVICE_ROLE access determines how much USR to mint
The contract enforced a minimum USR output. But critically: no maximum. No on-chain ratio check. No oracle validation. No cap.
function completeSwap(
uint256 requestId,
uint256 usrAmount // Unchecked parameter from off-chain
) external onlyRole(SERVICE_ROLE) {
SwapRequest memory req = requests[requestId];
require(usrAmount >= req.minOutput, "Below minimum");
// No maximum check!
_mint(req.recipient, usrAmount);
}
Whatever the SERVICE_ROLE key signed, the contract minted. The entire security model rested on a single key stored in AWS KMS.
The Kill Chain
Step 1: Attacker compromises AWS KMS environment (off-chain)
Step 2: Deposits ~$100K USDC via requestSwap() — looks normal
Step 3: Calls completeSwap() with 50M USR output — 500x the deposit
Step 4: Second deposit + 30M USR mint — another 80M total
Step 5: Converts USR to wstUSR (wrapped staked) to avoid immediate market impact
Step 6: Swaps wstUSR to USDC/USDT to ETH — ~$25M extracted
The 500x deviation between deposited value ($100K) and minted value ($50M) should have been an impossible event. But nothing on-chain prevented it.
Why Audits Alone Would Not Have Caught This
A standard smart contract audit might have noted the missing maximum check as an informational finding, but the logic was internally consistent. The SERVICE_ROLE was supposed to be trusted — that was the design.
The vulnerability was in the trust assumption:
The SERVICE_ROLE key will always behave correctly because we control the infrastructure.
This is the same class of assumption that killed:
- Ronin Bridge ($625M, 2022) — compromised validator keys
- Harmony Horizon ($100M, 2022) — 2-of-5 multisig compromised
- Step Finance ($27M, Jan 2026) — executive key compromise
Off-chain key compromise is DeFi most expensive vulnerability class in 2026, exceeding smart contract bugs by total dollar value.
The 5 Off-Chain Trust Boundaries
1. On-Chain Mint Ratio Ceiling
Never let the contract mint more than X% above the deposited collateral:
uint256 public constant MAX_MINT_RATIO = 105;
uint256 public constant RATIO_DENOMINATOR = 100;
function completeSwap(uint256 requestId, uint256 usrAmount) external onlyRole(SERVICE_ROLE) {
SwapRequest memory req = requests[requestId];
uint256 maxMint = (req.depositedAmount * MAX_MINT_RATIO) / RATIO_DENOMINATOR;
require(usrAmount <= maxMint, "Exceeds mint ceiling");
require(usrAmount >= req.minOutput, "Below minimum");
_mint(req.recipient, usrAmount);
}
Impact: Attacker gets ~$105K in USR from a $100K deposit. Attack neutralized.
2. Per-Epoch Mint Caps
Limit total minting throughput to prevent sudden supply shocks:
uint256 public constant EPOCH_DURATION = 1 hours;
uint256 public constant EPOCH_MINT_CAP = 5_000_000e18;
mapping(uint256 => uint256) public epochMinted;
function completeSwap(uint256 requestId, uint256 usrAmount) external onlyRole(SERVICE_ROLE) {
uint256 epoch = block.timestamp / EPOCH_DURATION;
epochMinted[epoch] += usrAmount;
require(epochMinted[epoch] <= EPOCH_MINT_CAP, "Epoch cap exceeded");
}
Impact: Attacker could only mint 5M USR per hour instead of 80M in minutes.
3. Timelock on Large Mints
Any mint above a threshold requires a delay:
uint256 public constant LARGE_MINT_THRESHOLD = 1_000_000e18;
uint256 public constant TIMELOCK_DELAY = 30 minutes;
function completeSwap(uint256 requestId, uint256 usrAmount) external onlyRole(SERVICE_ROLE) {
if (usrAmount > LARGE_MINT_THRESHOLD) {
require(block.timestamp >= requests[requestId].createdAt + TIMELOCK_DELAY, "Timelock");
}
}
4. Multi-Sig for Minting
A single AWS KMS key is a single point of failure. Require M-of-N signatures distributed across different cloud providers and HSMs:
function completeSwap(
uint256 requestId,
uint256 usrAmount,
bytes[] calldata signatures
) external {
require(_verifyMultiSig(abi.encode(requestId, usrAmount), signatures) >= 3, "Need 3 sigs");
}
5. Circuit Breaker with Anomaly Detection
Automatic pause when statistical anomalies are detected:
uint256 public rollingMintAvg;
uint256 public constant ANOMALY_MULTIPLIER = 10;
function completeSwap(uint256 requestId, uint256 usrAmount) external onlyRole(SERVICE_ROLE) {
if (usrAmount > rollingMintAvg * ANOMALY_MULTIPLIER && rollingMintAvg > 0) {
_pause();
revert("Anomaly detected");
}
rollingMintAvg = (rollingMintAvg * 9 + usrAmount) / 10;
}
Complete Hardened Pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
abstract contract HardenedMinter is Pausable, AccessControl {
uint256 public constant MAX_MINT_RATIO = 105;
uint256 public constant RATIO_DENOMINATOR = 100;
uint256 public constant EPOCH_DURATION = 1 hours;
uint256 public constant EPOCH_MINT_CAP = 5_000_000e18;
uint256 public constant LARGE_MINT_THRESHOLD = 1_000_000e18;
uint256 public constant TIMELOCK_DELAY = 30 minutes;
uint256 public constant ANOMALY_MULTIPLIER = 10;
uint256 public rollingMintAvg;
mapping(uint256 => uint256) public epochMinted;
function _validateMint(
uint256 depositedAmount,
uint256 mintAmount,
uint256 requestCreatedAt
) internal {
require(mintAmount <= (depositedAmount * MAX_MINT_RATIO) / RATIO_DENOMINATOR, "Ratio");
uint256 epoch = block.timestamp / EPOCH_DURATION;
epochMinted[epoch] += mintAmount;
require(epochMinted[epoch] <= EPOCH_MINT_CAP, "Epoch cap");
if (mintAmount > LARGE_MINT_THRESHOLD) {
require(block.timestamp >= requestCreatedAt + TIMELOCK_DELAY, "Timelock");
}
if (rollingMintAvg > 0 && mintAmount > rollingMintAvg * ANOMALY_MULTIPLIER) {
_pause();
revert("Anomaly");
}
rollingMintAvg = (rollingMintAvg * 9 + mintAmount) / 10;
}
event AnomalyDetected(uint256 mintAmount, uint256 rollingAvg);
}
The Bigger Picture: Q1 2026 Attack Surface Shift
Three of the top five Q1 2026 exploits involve key compromise, not code bugs:
- Resolv Labs ($25M) — AWS KMS key compromise
- Step Finance ($27M) — Executive private key compromise
- IoTeX ($4.4M) — Private key compromise
- Truebit ($26.2M) — Smart contract logic flaw
- YieldBlox ($11M) — Oracle manipulation
The industry has gotten better at writing secure Solidity. It has not gotten better at securing the infrastructure that controls privileged operations.
The Mental Model Shift
Stop thinking of smart contracts as isolated programs. Think of them as the last line of defense in a system where every other component can be compromised:
- Your cloud provider will be breached eventually
- Your team member laptop will be compromised eventually
- Your CI/CD pipeline will be targeted eventually
The smart contract must be written to limit damage even when the off-chain world is fully hostile.
Checklist: Is Your Protocol Resolv-Proof?
- Does every privileged minting/burning function have an on-chain ceiling?
- Are there per-epoch throughput caps on critical state changes?
- Do large operations require timelocks or multi-party approval?
- Is your signing key distributed across multiple providers/HSMs?
- Do you have on-chain circuit breakers for statistical anomalies?
- Can your team pause the protocol independently of the compromised key?
- Do you have real-time monitoring on privileged role transactions?
- Have you modeled the maximum damage a compromised SERVICE_ROLE can inflict?
If you checked fewer than 6, your protocol has the same vulnerability class as Resolv.
References
- Chainalysis: Lessons from the Resolv Hack
- QuillAudits: Resolv Labs Exploit Explained
- CCN: DeFi Hacks 2026
Building DeFi infrastructure? The code is the easy part. The hard part is designing systems that survive when everything around the code fails.
Top comments (0)