DEV Community

ohmygod
ohmygod

Posted on

The $25M Resolv USR Exploit: Why Your Minting Function's Off-Chain Signer Is the Biggest Single Point of Failure in DeFi

The $25M Resolv USR Exploit: Why Your Minting Function's Off-Chain Signer Is the Biggest Single Point of Failure in DeFi

On March 22, 2026, at 2:21 AM UTC, an attacker turned roughly $200,000 in USDC into $25 million in extracted ETH. No flash loan. No reentrancy. No oracle manipulation. They compromised a single AWS KMS key that controlled Resolv Labs' stablecoin minting function — and the smart contract had zero on-chain guardrails to stop what happened next.

80 million unbacked USR tokens were minted in two transactions. USR crashed from $1.00 to $0.025 in 17 minutes. Lending vaults on Morpho, Euler, and Curve took collateral damage. And every DeFi protocol using privileged off-chain signers for critical operations should be asking: could this happen to us?

The answer, for most, is yes.

How Resolv's Minting Worked (And Didn't)

Resolv's USR stablecoin used a two-step minting flow:

  1. requestSwap() — User deposits USDC, creating a pending mint request
  2. completeSwap() — An off-chain service (the SERVICE_ROLE) calls back to finalize how much USR to mint

The critical design flaw: the contract enforced a minimum USR output but no maximum. There was no on-chain ratio check between deposited collateral and minted tokens. No price oracle validation. No minting cap. Whatever the SERVICE_ROLE key authorized, the contract executed.

// Simplified Resolv minting logic (VULNERABLE)
function completeSwap(
    uint256 requestId,
    uint256 usrAmount,    // No upper bound!
    bytes calldata signature
) external {
    Request storage req = requests[requestId];
    require(req.status == Status.Pending, "Invalid request");

    // Verify SERVICE_ROLE signature
    bytes32 hash = keccak256(abi.encode(requestId, usrAmount));
    require(verifySignature(hash, signature, SERVICE_ROLE), "Bad sig");

    // Mint whatever amount the signer says
    // No check: usrAmount <= req.usdcDeposited * MAX_RATIO
    // No check: usrAmount <= GLOBAL_MINT_CAP
    // No check: totalSupply + usrAmount <= SUPPLY_CEILING
    usr.mint(msg.sender, usrAmount);  // 🔴 50,000,000 USR? Sure.

    req.status = Status.Completed;
}
Enter fullscreen mode Exit fullscreen mode

To make things worse, the SERVICE_ROLE was a plain externally owned address (EOA) — not a multisig. The admin role used a multisig, but the key with the power to mint arbitrary amounts did not.

The Attack Chain

Step 1: AWS KMS Compromise

The attacker gained access to Resolv's AWS Key Management Service environment where the SERVICE_ROLE private key was stored. The exact vector hasn't been fully disclosed, but common AWS KMS compromise paths include:

  • Stolen IAM credentials (phished or leaked)
  • Overly permissive IAM policies allowing kms:Sign from unexpected roles
  • Compromised CI/CD pipeline with KMS access
  • Session token hijacking from a developer's machine

Step 2: The Over-Mint

With the signing key, the attacker:

  1. Deposited ~$100K–$200K USDC via requestSwap()
  2. Called completeSwap() with the compromised key, authorizing 50 million USR for the first request
  3. Repeated for another 30 million USR

Total: 80 million USR minted against ~$200K in collateral. A 400x over-mint.

Step 3: The Cashout (17 Minutes)

The attacker converted USR → wstUSR (wrapped staked USR) → stablecoins → ETH across Curve, KyberSwap, and Velodrome. Multiple failed transactions on-chain show them racing against liquidity. Final haul: ~11,400 ETH (~$24M) plus ~20M wstUSR still held.

The Pattern: Off-Chain Signer as God Mode

This isn't unique to Resolv. An enormous number of DeFi protocols delegate critical operations to off-chain signers:

Operation Who Uses Off-Chain Signers Risk
Token minting Stablecoin protocols, bridges Infinite mint
Price feeds Custom oracle implementations Price manipulation
Withdrawal approval Centralized exchange hot wallets Fund drain
Upgrade authorization Proxy admin patterns Logic replacement
Batch settlement L2 sequencers, payment channels State corruption

The fundamental problem: off-chain signers move the trust boundary from \"trust the code\" to \"trust the infrastructure running the code.\" And infrastructure is almost always easier to compromise than audited smart contracts.

The Resolv-Step Finance Connection

One week before Resolv, Step Finance lost $40M through compromised executive devices. Both exploits share the same root cause: a single compromised private key with disproportionate on-chain authority. The difference is cosmetic — Step Finance stored keys on laptops, Resolv stored them in AWS KMS.

The DeFi industry is facing a key management crisis. In Q1 2026 alone, private key compromises have caused more dollar damage than all smart contract bugs combined.

Defense Pattern 1: On-Chain Minting Invariants

The most critical fix is also the simplest: enforce minting bounds on-chain, regardless of what the off-chain signer authorizes.

contract SecureMinting {
    uint256 public constant MAX_MINT_RATIO = 1.05e18;  // 105% max
    uint256 public constant SINGLE_MINT_CAP = 1_000_000e18;  // 1M per tx
    uint256 public constant DAILY_MINT_CAP = 10_000_000e18;  // 10M per day

    uint256 public dailyMinted;
    uint256 public dailyResetTimestamp;

    function completeSwap(
        uint256 requestId,
        uint256 mintAmount,
        bytes calldata signature
    ) external {
        Request storage req = requests[requestId];

        // Invariant 1: Mint ratio cannot exceed MAX_MINT_RATIO
        uint256 maxAllowed = (req.collateralAmount * MAX_MINT_RATIO) / 1e18;
        require(mintAmount <= maxAllowed, "Exceeds mint ratio");

        // Invariant 2: Single transaction cap
        require(mintAmount <= SINGLE_MINT_CAP, "Exceeds single mint cap");

        // Invariant 3: Daily aggregate cap
        _resetDailyIfNeeded();
        dailyMinted += mintAmount;
        require(dailyMinted <= DAILY_MINT_CAP, "Exceeds daily cap");

        // Invariant 4: Total supply ceiling
        require(
            usr.totalSupply() + mintAmount <= TOTAL_SUPPLY_CEILING,
            "Exceeds supply ceiling"
        );

        // Only THEN verify the signature and mint
        require(verifySignature(...), "Bad sig");
        usr.mint(msg.sender, mintAmount);
    }

    function _resetDailyIfNeeded() internal {
        if (block.timestamp >= dailyResetTimestamp + 1 days) {
            dailyMinted = 0;
            dailyResetTimestamp = block.timestamp;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Even if the attacker had the signing key, a 105% mint ratio cap would have limited the damage to ~$210K — the approximate value of their deposit.

Defense Pattern 2: Tiered Signing Authority

Replace the single SERVICE_ROLE EOA with a tiered system where the required authorization escalates with the transaction size:

contract TieredMinting {
    // Tier 1: Automated signer (EOA/KMS) — small mints only
    uint256 public constant TIER1_LIMIT = 100_000e18;  // $100K
    address public automatedSigner;

    // Tier 2: Multisig — medium mints
    uint256 public constant TIER2_LIMIT = 1_000_000e18;  // $1M  
    address public multisig;  // 3-of-5 Gnosis Safe

    // Tier 3: Timelock + Multisig — large mints
    uint256 public constant TIMELOCK_DELAY = 24 hours;

    function completeMint(uint256 amount, bytes calldata auth) external {
        if (amount <= TIER1_LIMIT) {
            // Automated: single signature sufficient
            require(verifySigner(auth, automatedSigner), "Bad T1 sig");
        } else if (amount <= TIER2_LIMIT) {
            // Requires multisig approval
            require(verifySigner(auth, multisig), "Bad T2 sig");
        } else {
            // Must go through timelock — 24h delay
            require(
                timelockQueue[requestId].timestamp + TIMELOCK_DELAY 
                    <= block.timestamp,
                "Timelock not expired"
            );
            require(verifySigner(auth, multisig), "Bad T3 sig");
        }

        _executeMint(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Defense Pattern 3: Circuit Breaker with Anomaly Detection

Implement an on-chain circuit breaker that automatically pauses minting when anomalous patterns are detected:

contract CircuitBreaker {
    uint256 public constant VELOCITY_WINDOW = 1 hours;
    uint256 public constant VELOCITY_LIMIT = 5_000_000e18;  // $5M/hour
    bool public circuitBroken;

    modifier circuitCheck() {
        require(!circuitBroken, "Circuit breaker active");
        _;
        _checkVelocity();
    }

    function _checkVelocity() internal {
        uint256 windowStart = block.timestamp - VELOCITY_WINDOW;
        uint256 windowTotal = 0;

        for (uint i = recentMints.length; i > 0; i--) {
            if (recentTimestamps[i-1] < windowStart) break;
            windowTotal += recentMints[i-1];
        }

        if (windowTotal > VELOCITY_LIMIT) {
            circuitBroken = true;
            emit CircuitBreakerTriggered(windowTotal, block.timestamp);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Defense Pattern 4: KMS Key Isolation for DeFi

For protocols that must use cloud KMS, the key management architecture needs defense-in-depth:

┌─────────────────────────────────────────────────┐
│ AWS Account: resolv-minting-prod                │
│ (Isolated account — no other workloads)         │
│                                                 │
│  ┌──────────────────────────────────────────┐   │
│  │ KMS Key: usr-minting-signer              │   │
│  │ Policy:                                  │   │
│  │  - Only Lambda role can call kms:Sign    │   │
│  │  - CloudTrail logging (immutable)        │   │
│  │  - Alert on ANY kms:Sign call            │   │
│  │  - Rate limit: 10 calls/minute           │   │
│  │  - Deny kms:Sign from console/CLI        │   │
│  └──────────────────────────────────────────┘   │
│                                                 │
│  ┌──────────────────────────────────────────┐   │
│  │ Lambda: mint-authorizer                  │   │
│  │ - Validates collateral ratio             │   │
│  │ - Checks against daily caps              │   │
│  │ - Requires Step Functions approval flow  │   │
│  │   for amounts > $100K                    │   │
│  │ - Signs only after all checks pass       │   │
│  └──────────────────────────────────────────┘   │
│                                                 │
│  GuardDuty + CloudTrail → SNS → PagerDuty      │
└─────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key principles:

  • Dedicated AWS account — no shared infrastructure, no lateral movement
  • IAM least privilege — only one Lambda function can invoke kms:Sign
  • Rate limiting at the KMS level — even a compromised Lambda can't sign rapidly
  • Immutable audit trail — CloudTrail logs shipped to a separate account

The Solana Equivalent: PDA Authority Hardening

Solana protocols face the same pattern with Program Derived Addresses (PDAs) and authority keys:

// VULNERABLE: Single EOA mint authority
#[account(
    mut,
    constraint = mint.mint_authority == COption::Some(authority.key())
)]
pub mint: Account<'info, Mint>,
pub authority: Signer<'info>,  // Single key controls all minting

// HARDENED: PDA authority with on-chain constraints
#[account(
    mut,
    constraint = mint.mint_authority == COption::Some(mint_authority_pda.key())
)]
pub mint: Account<'info, Mint>,

#[account(
    seeds = [b"mint_authority", config.key().as_ref()],
    bump = config.mint_authority_bump,
)]
pub mint_authority_pda: AccountInfo<'info>,

#[account(
    constraint = config.daily_minted + amount <= config.daily_cap 
        @ ErrorCode::DailyCapExceeded,
    constraint = amount <= config.per_tx_cap 
        @ ErrorCode::TxCapExceeded,
)]
pub config: Account<'info, MintConfig>,
Enter fullscreen mode Exit fullscreen mode

The PDA-based approach ensures that minting authority lives entirely on-chain, governed by program logic rather than a single compromised key.

The 8-Point Privileged Signer Audit Checklist

For any protocol using off-chain signers for critical operations:

Key Management

  • [ ] No EOA for privileged roles — use multisig (M-of-N) for any role that can mint, burn, upgrade, or transfer significant value
  • [ ] KMS isolation — dedicated cloud account, least-privilege IAM, rate-limited signing
  • [ ] Key rotation schedule — automated rotation with zero-downtime handoff

On-Chain Guardrails

  • [ ] Bound all privileged operations — per-transaction caps, daily caps, supply ceilings
  • [ ] Validate ratios on-chain — never trust off-chain computation for collateral/mint ratios
  • [ ] Circuit breakers — automatic pause on anomalous velocity or depeg detection

Monitoring

  • [ ] Real-time alerting — every privileged function call triggers an alert with amount + caller + context
  • [ ] Honeypot requests — periodically submit known-bad requests to verify the system rejects them

Takeaway

The Resolv exploit is the clearest case study yet for a simple principle: on-chain code must enforce its own invariants, regardless of what off-chain systems tell it to do.

The contract's job is to be the last line of defense, not a rubber stamp for whatever a privileged key signs. A MAX_MINT_RATIO check — literally three lines of Solidity — would have reduced the damage from $25 million to essentially zero.

Every DeFi protocol should audit not just their smart contracts, but the trust assumptions their contracts make about off-chain signers. If a single key compromise can drain your protocol, you don't have a security architecture. You have a single point of failure with extra steps.


This analysis is part of the DeFi Security Research series by DreamWork Security. Sources: Chainalysis Hexagate analysis, DeFiPrime investigation, D2 Finance on-chain analysis, PeckShield alerts (March 22, 2026).

Top comments (0)