Resolv Incident Analysis

Resolv USR Incident Analysis

Updated: 2026-03-23, 2026-03-24, 2026-03-26, 2026-04-07, 2026-04-09
Last updated: 2026-04-09

USDC, USDT, and USR#

USDC and USDT are both fiat-referenced stablecoins designed to maintain a value of approximately 1 USD per token.
USDC is issued by Circle, while USDT is issued by Tether.

At a high level, the two assets are similar, but their reserve models are described differently. USDC is generally presented as being backed by highly liquid cash and cash-equivalent reserves, with a large portion of those reserves held through the Circle Reserve Fund. USDT, by contrast, has historically described its backing more broadly, including cash, cash equivalents, and other assets, with disclosure language that may also cover items such as loan receivables and affiliated exposures. In practical terms, both are fiat-backed stablecoins, but USDC is typically viewed as having a simpler and more narrowly defined reserve profile, whereas USDT has historically disclosed a broader reserve mix.

USR targets the same broad price objective—stability around 1 USD—but is structurally different. According to Resolv’s public description, USR is designed as an overcollateralized stable asset backed by a mix of ETH, BTC, and other USD-neutral assets, with an additional loss-absorbing layer provided by RLP.

Attack Narrative#

1. Initial Foothold – Third-party Compromise#

A Resolv contractor was compromised, and the attackers obtained credentials associated with the contractor’s GitHub account. This appears to have provided the initial foothold into Resolv’s code repository as well.

The circumstances of the initial upstream compromise are the subject of an ongoing investigation.1

2. Lateral Movement and Privilege Escalation – Access to Resolv’s Sensitive Infrastructure#

The attackers then deployed a malicious workflow to exfiltrate sensitive credentials.

A similar incident occurred on March 19, 2026, in Aqua Security’s Trivy ecosystem, where a GitHub Actions misconfiguration allowed attackers to extract a privileged token.2

The attacker compromised Resolv’s cloud infrastructure to gain access to Resolv’s AWS Key Management Service (KMS) environment where the protocol’s privileged signing key was stored. 3

The exfiltrated credentials appear to have provided initial access to Resolv’s cloud infrastructure. However, that access alone was not sufficient to use the signing key: according to Resolv’s postmortem, the attackers’ initial attempts were blocked, and they ultimately used a higher-privileged infrastructure role’s policy-management capabilities to modify the key’s access policy and gain signing authority corresponding to the protocol’s on-chain SERVICE_ROLE privileges.

Over the following days, the attackers conducted reconnaissance, enumerated services, and attempted to extract additional API keys.

3. The exploiter submitted swap requests#

The exploiter address, 0x04A288a7789DD6Ade935361a4fB1Ec5db513caEd, submitted multiple requestSwap(...) transactions using USDC as the deposit asset in the USR Counter flow.

Relevant transactions#

Example calldata#

#NameTypeData
0_depositTokenAddressaddress0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
1_amountuint256100000000000
2_minExpectedAmountuint2560

Observations#

  • Each request deposited 100,000 USDC (100000000000 in USDC base units).
  • The requester set _minExpectedAmount = 0, meaning the request imposed no effective lower-bound protection on the final output amount.

4. Completing the swap via privileged settlement#

After the requests were created, the privileged settlement path completed them through completeSwap(...), supplying the final _targetAmount used to settle the requests and mint the output token.

Relevant transactions#

Example calldata#

#NameTypeData
0_idempotencyKeybytes320xb9f73da47014b8fab2ad52a0cd90f07f1a54d801c4366b2e15d3931a3a5e3239
1_iduint25630
2_targetAmountuint25650000000000000000000000000

Aggregate output#

  • Total _targetAmount across the three completions: 80,099,995.213 USR
  • Total actually transferred to the attacker after fees: 80,019,895.217787 USR

Key observation#

The economically significant step was not the initial requestSwap(...) call, but the later completeSwap(...) call, where the privileged settlement path injected the final _targetAmount.

The key point is the mismatch between the requested input size and the eventual USR output.
Read chronologically, the pattern is clear:

  • Tx #1 / #3 / #5: the exploiter submitted swap requests funded with 100,000 USDC each
  • Tx #2 / #4 / #6: those requests were later completed through the privileged settlement path

In two of those completions, the resulting output was wildly disproportionate to the original 100,000 USDC input, leading to massively outsized USR issuance.

5. Shifting exposure by converting into wstUSR#

The attacker converted a significant portion of the received USR into wstUSR.

This step is important not only as a token conversion, but as a market-impact decision. Chainalysis characterizes it as a move out of spot USR into a derivative form that was less likely to trigger an immediate collapse through direct selling. In that framing, converting into wstUSR allowed the attacker to avoid dumping all of the newly minted USR directly into visible liquidity at once, while repositioning into an instrument that could later be unwound more flexibly across subsequent cash-out steps. 3

Within the Resolv system, wstUSR is the non-rebasing wrapper of staked USR, while unstaking remained possible on a 1:1 basis without a waiting period. In practice, this meant the attacker could shift into a different token form without obviously giving up exit optionality. More importantly, later recovery updates suggest that this conversion also changed the protocol’s remediation path: attacker-held plain USR could be burned directly, whereas a much larger amount held as wstUSR required a separate contract-upgrade and blacklist process subject to additional timing constraints.

In that sense, the move from USR into wstUSR should be understood not only as a market-impact decision, but also as a shift into an asset form that was operationally harder to neutralize immediately.

Relevant transactions#

6. Funds were routed through trading venues to external wallets#

The attacker then used major swap / aggregation routers to unwind exposure and move value out.

Observed router / venue examples#

  • MetaMask: Swap Router
  • KyberSwap: Meta Aggregation Router v2
  • Velora: Augustus V6.2
  • Uniswap V4: Universal Router

External transfers#

The aggregate Value_OUT (ETH) was 10,884.245 ETH, which was approximately $22.35M at incident-time valuation. Additional outflows also occurred in non-ETH assets such as WETH, USDC, and USDT.

Large outbound examples#

Relevant contract: USR Counter#

High-Level Overview#

USR Counter is a request-completion swap manager. A user deposits an approved input token, and the protocol later completes the request by minting and delivering the configured output token. Public materials indicate that input-token eligibility is controlled through protocol configuration rather than being fully inferred from this contract alone. In the audit materials, USDC and USDT are described as the operational assets used for mint / burn flows, while the incident transactions publicly described so far appear to have been funded with USDC.

Process Flow / Swimlane Workflow Diagram#

  1. The user calls requestSwap(depositToken, amount, minExpectedAmount).
  2. USR Contract transfers the deposit token from the user and records the request on-chain.
  3. The off-chain service (backend layer) then performs the settlement-side workflow:
    a. It observes newly submitted swap requests on-chain.
    b. It determines the final mint amount off-chain based on its own pricing / exchange-rate logic.
    c. It calls completeSwap(id, targetAmount) on-chain using the SERVICE_ROLE account.
  4. The contract transfers the deposited asset to treasury.
  5. The contract mints the output token.
  6. The contract transfers the net output amount to the user.
Unable to render diagram.

Deployed Source Code#

TheCounter.sol

requestSwap(...)#

function requestSwap(  
    address _depositTokenAddress,  
    uint256 _amount,  
    uint256 _minExpectedAmount  
) public allowedToken(_depositTokenAddress) whenNotPaused {  
    require(_amount != 0, InvalidAmount(_amount));  
    uint256 minSwapAmount = minSwapAmounts[_depositTokenAddress];  
    require(_amount >= minSwapAmount, InsufficientAmount(_amount, minSwapAmount));  
  
    IERC20(_depositTokenAddress).safeTransferFrom(msg.sender, address(this), _amount);  
    Request memory request = _addSwapRequest(_depositTokenAddress, _amount, _minExpectedAmount);  
  
    emit SwapRequestCreated(  
        request.id,  
        request.provider,  
        request.token,  
        request.amount,  
        request.minExpectedAmount  
    );  
}
Key points
  • allowedToken(...) restricts the deposit asset to an approved token.
  • The function rejects zero-value requests.
  • The function enforces a token-specific minimum deposit amount.
  • minExpectedAmount is user-specified and represents the minimum acceptable output amount, not the deposit amount.

completeSwap(...)#

function completeSwap(  
    bytes32 _idempotencyKey,  
    uint256 _id,  
    uint256 _targetAmount  
) external onlyRole(SERVICE_ROLE) swapRequestExist(_id) {  
    Request storage request = swapRequests[_id];  
    require(State.CREATED == request.state, IllegalState(State.CREATED, request.state));  
    uint256 takenFee = (_targetAmount * fee) / FEE_DENOMINATOR;  
    uint256 transferableAmount = _targetAmount - takenFee;  
    uint256 minExpectedAmount = request.minExpectedAmount;  
    require(transferableAmount >= minExpectedAmount, InsufficientAmount(transferableAmount, minExpectedAmount));  
  
    request.state = State.COMPLETED;  
  
    IERC20 requestToken = IERC20(request.token);  
    requestToken.safeTransfer(treasury, request.amount);  
  
    ISimpleToken simpleToken = ISimpleToken(SWAP_TOKEN_ADDRESS);  
    simpleToken.mint(_idempotencyKey, address(this), _targetAmount);  
    collectedFee += takenFee;  
    IERC20 token = IERC20(SWAP_TOKEN_ADDRESS);  
    token.safeTransfer(request.provider, transferableAmount);  
  
    emit SwapRequestCompleted(_idempotencyKey, _id, transferableAmount, takenFee);  
}
Key points
  • completeSwap(...) is restricted by onlyRole(SERVICE_ROLE).
  • The final settlement amount, _targetAmount, is supplied externally by the privileged service-side caller.
  • The function enforces only a lower-bound check:
    • transferableAmount >= minExpectedAmount
  • There is no visible on-chain upper-bound check tying _targetAmount to the deposited amount.
  • Once called successfully, the function sends the deposit token to treasury, mints the output token, deducts fees, and transfers the remainder to the requester.

Security-Relevant Observation#

The critical control point in this design is completeSwap(...). The user defines the minimum acceptable output, but the privileged SERVICE_ROLE caller determines the actual settlement amount by supplying _targetAmount.
As implemented, the contract checks that the user does not receive too little, but it does not independently verify that the requested mint amount is economically consistent with the deposited collateral.

Root Cause#

Public reporting now supports a clearer conclusion: this was a cross-layer compromise, not a conventional smart-contract exploit.

The attack appears to have begun with a contractor-linked GitHub credential exposed through an upstream third-party compromise. From there, the attackers gained access to Resolv repositories, deployed a malicious workflow to exfiltrate credentials, moved into Resolv’s cloud environment, and ultimately obtained signing authority over a privileged service path.

The on-chain damage was then amplified by architecture. In the relevant completion flow, the privileged service could supply the final settlement amount, while the contract did not appear to enforce a strong independent upper-bound or collateral-backing check on that output. As a result, once signing authority was compromised, illegitimate mint completions could still succeed through an otherwise authorized path.

Remaining uncertainties#

Public materials still do not fully establish the exact upstream intrusion path, the full internal approval workflow around privileged operations, or the complete internal validation logic used before settlement.

Incident Response#

Resolv’s response unfolded in stages. According to the postmortem, real-time monitoring detected the first anomalous transaction and triggered an alert. The team then began containment by halting backend services, preparing on-chain pauses, and investigating how the attackers had obtained signing authority. At 05:16 UTC, all relevant smart contracts with available pause functionality were fully paused, and at 05:30 UTC the compromised credentials were revoked, severing the attackers’ access to the cloud environment. Forensic logs reportedly show that the attackers had still been active as late as 05:15 UTC.

Resolv also publicly indicated that it had contacted the exploiter on-chain and proposed a settlement under which 90% of the stolen funds would be returned while 10% could be retained by the attacker.

Resolv later reported a broader remediation effort aimed at removing illicitly minted supply from circulation and providing relief to certain pre-exploit holders. According to the project, approximately 46 million of the 80 million illicitly minted USR had been permanently removed from circulation through a combination of direct burns, blacklist actions affecting wstUSR.

This remediation appears to have been constrained by token form. Resolv was able to burn only the portion of illicit supply that remained as plain USR, while a significant share had already been converted into wstUSR and therefore required a different response path.

Specifically:

In parallel, Resolv stated that pre-hack USR holders would be compensated on a 1:1 basis, with the majority of redemptions already processed or in the pipeline.

As of Mar. 26, Resolv warned that illicitly minted USR had already mixed with pre-hack USR on secondary markets, carried no collateral backing or redemption rights, and that further trading could complicate recovery efforts and reduce any eventual holder relief.

Takeaways#

Regardless of the ultimate root cause, this review establishes a separate and important architectural point: SERVICE_ROLE carries broad authority across the public Resolv codebase. In the current public repository, SERVICE_ROLE is not limited to token minting and burning; it also gates request settlement, swap finalization, reward distribution, oracle updates, redemption execution, and treasury operations.

The key takeaway is one of security architecture: a single service-role pattern appears to span a wide range of economically sensitive functions. Under a stricter least-privilege design, issuance, settlement, rewards, oracle maintenance, and treasury execution would be separated into more narrowly scoped operational roles.

The inventory below should therefore be read not merely as a list of role-protected functions, but as a map of the protocol’s operational trust surface.

Contracts / Functions Using SERVICE_ROLE#

1. SimpleToken.sol#

  • Source file: contracts/SimpleToken.sol
  • Deployed contracts using this implementation: USR, RLP
  • Functions protected by SERVICE_ROLE:
    • mint(address,uint256)
    • mint(bytes32,address,uint256)
    • burn(address,uint256)
    • burn(bytes32,address,uint256)
  • Operational purpose: These are the base issuance and destruction functions for the core protocol tokens.
  • Why SERVICE_ROLE is required: These functions directly change token supply and therefore must be restricted to protocol-controlled operators.

2. UsrExternalRequestsManager.sol#

  • Source file: contracts/UsrExternalRequestsManager.sol
  • Deployed contract: USR Requests Manager
  • Functions protected by SERVICE_ROLE:
    • completeMint(bytes32,uint256,uint256)
    • completeBurn(bytes32,uint256,uint256)
  • Operational purpose: These functions finalize previously created USR mint and burn requests by completing settlement, moving collateral, and updating supply.
  • Why SERVICE_ROLE is required: They are execution paths, not mere request-registration paths. Once called, they cause treasury movement and token issuance or destruction.

3. ExternalRequestsManager.sol#

  • Source file: contracts/ExternalRequestsManager.sol
  • Deployed contract: RLP Requests Manager
  • Functions protected by SERVICE_ROLE:
    • completeMint(bytes32,uint256,uint256)
    • completeBurn(bytes32,uint256,uint256)
  • Operational purpose: These are the RLP-side settlement functions for mint and burn requests.
  • Why SERVICE_ROLE is required: They finalize economically meaningful state transitions and move assets in and out of treasury.

4. TheCounter.sol#

  • Source file: contracts/TheCounter.sol
  • Deployed contracts using this implementation: USR Counter, RLP Counter
  • Functions protected by SERVICE_ROLE:
    • completeSwap(bytes32,uint256,uint256)
  • Operational purpose: This function completes a previously registered swap request, transfers the input asset to treasury, mints the configured output token, and pays the requester.
  • Why SERVICE_ROLE is required: This is the protocol’s settlement finalization path. The service-side caller supplies the final _targetAmount, so the function must remain under privileged control.

5. RewardDistributor.sol#

  • Source file: contracts/RewardDistributor.sol
  • Deployed contract: RewardDistributor
  • Functions protected by SERVICE_ROLE:
    • distribute(bytes32,uint256,uint256)
  • Operational purpose: This function allocates staking rewards and protocol fee rewards.
  • Why SERVICE_ROLE is required: Although this is not a mint/burn request manager, it still performs privileged reward issuance and therefore has direct economic impact on supply.

6. UsrPriceStorage.sol#

7. RlpPriceStorage.sol#

8. UsrRedemptionExtension.sol#

  • Source file: contracts/UsrRedemptionExtension.sol
  • Known deployment: UsrRedemptionExtension
  • Functions protected by SERVICE_ROLE:
    • redeem(uint256,address,address)
  • Operational purpose: This is the actual redemption execution module behind the public USR redemption flow. It performs the burn-side settlement and withdrawal-token payout logic.
  • Why SERVICE_ROLE is required: Redemption here is implemented as a controlled operational flow with pricing and cap logic, not as a raw public burn primitive.

9. Treasury.sol#

  • Source file: contracts/Treasury.sol
  • Public code reference: Treasury.sol
  • Functions protected by SERVICE_ROLE:
    • transferETH(...)
    • transferERC20(...)
    • increaseAllowance(...)
    • decreaseAllowance(...)
    • lidoDeposit(...)
    • lidoRequestWithdrawals(...)
    • lidoClaimWithdrawals(...)
    • aaveSupply(...)
    • aaveBorrow(...)
    • aaveSupplyAndBorrow(...)
    • aaveRepay(...)
    • aaveWithdraw(...)
    • aaveRepayAndWithdraw(...)
    • dineroDeposit(...)
    • dineroInitiateRedemption(...)
    • dineroInstantRedeemWithApxEth(...)
    • dineroRedeem(...)
  • Operational purpose: These are treasury control functions covering direct asset movement as well as integrations with Lido, Aave, and Dinero.
  • Why SERVICE_ROLE is required: These functions control protocol-owned assets and external position management and therefore cannot be exposed publicly.

10. LidoTreasuryExtension.sol#

  • Source file: contracts/LidoTreasuryExtension.sol
  • Public code reference: LidoTreasuryExtension.sol
  • Functions protected by SERVICE_ROLE:
    • unwrap(bytes32,uint256)
    • wrap(bytes32,uint256)
  • Operational purpose: These functions convert between stETH and wstETH in the treasury stack.
  • Why SERVICE_ROLE is required: They are treasury-side asset transformation functions and are therefore reserved for protocol operators.

11. LPExternalRequestsManager.sol#

  • Source file: contracts/LPExternalRequestsManager.sol
  • Public code reference: LPExternalRequestsManager.sol
  • Functions protected by SERVICE_ROLE:
    • completeMint(bytes32,uint256,uint256)
    • processBurns(bytes32,uint256[])
    • unprocessCurrentBurnEpoch(bytes32)
    • completeBurns(bytes32,CompleteBurnItem[],address[])
  • Operational purpose: This contract handles LP-side mint completion and epoch-based burn processing.
  • Why SERVICE_ROLE is required: It acts as a settlement operator with batching and finalization authority over economically sensitive burn flows.

Appendix#

Deployed Contracts#

USR#

roles: Core stablecoin unit of Resolv; primary non-yield-bearing USD-denominated asset.
contract:

  • USR (ERC20) address:
  • 0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110

RLP#

roles: Liquidity / insurance unit of Resolv; loss-absorbing and excess-collateral exposure token.
contract:

  • RLP (ERC20) address:
  • 0x4956b52aE2fF65D74CA2d61207523288e4528f96

stUSR#

roles: Yield-bearing staked USR unit.
contract:

  • stUSR (ERC20) address:
  • 0x6c8984bc7DBBeDAf4F6b2FD766f16eBB7d10AAb4

wstUSR#

roles: Wrapped non-rebasing representation of stUSR.
contract:

  • wstUSR (ERC20) address:
  • 0x1202F5C7b4B9E47a1A484E8B270be34dbbC75055

Whitelist#

roles: Access-control / participant allowlist unit used by operational contracts.
contract:

  • Whitelist address:
  • 0x5943026E21E3936538620ba27e01525bBA311255

USR Requests Manager#

roles: Operational unit handling USR mint / burn request flow.
contract:

  • USR requests manager address:
  • 0xAC85eF29192487E0a109b7f9E40C267a9ea95f2e

RLP Requests Manager#

roles: Operational unit handling RLP mint / burn request flow.
contract:

  • RLP requests manager address:
  • 0x10f4d4EAd6Bcd4de7849898403d88528e3Dfc872

Reward Distributor#

roles: Operational unit handling reward distribution.
contract:

  • RewardDistributor address:
  • 0xbE23BB6D817C08E7EC4Cd0adB0E23156189c1bA9

USR Counter#

roles: Operational swap / counter unit for USR flows; independently deployed and verified on mainnet.
contract:

  • USR Counter address:
  • 0xa27a69Ae180e202fDe5D38189a3F24Fe24E55861 verified source:
  • TheCounter.sol

RLP Counter#

roles: Operational swap / counter unit for RLP flows; independently deployed and verified on mainnet.
contract:

  • RLP Counter address:
  • 0xc7AB90c2Ea9271EFB31f5fA2843Eeb4B331eaFA0 verified source:
  • TheCounter.sol

References#

Footnotes#

  1. Resolv Lab, Resolv Postmortem: March 22, 2026 Incident. Available at: https://resolv.xyz/blog/resolv-postmortem-march-22-2026-incident

  2. Acua Team, Update: Ongoing Investigation and Continued Remediation. Available at: https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know/

  3. Chainalysis, The Resolv Hack: How One Compromised Key Printed $23 Million. Available at: https://www.chainalysis.com/blog/lessons-from-the-resolv-hack/ 2