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#
- Tx #1:
0x590b5c66df27b7f34cde721ca1b5f973ae047ffda370610491f694dade732c89_amount:100000000000= 100,000 USDC
- Tx #3:
0x2989be8683cdc9eec0e1e877d826624140405982bb63decdf4504b75b55a6757_amount:100000000000= 100,000 USDC
- Tx #5:
0xe5bae64ee813c742f06b5d045397c2eefa31532e4d6c21b7c6da129eaa17ae4b_amount:100000000000= 100,000 USDC
Example calldata#
| # | Name | Type | Data |
|---|---|---|---|
| 0 | _depositTokenAddress | address | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| 1 | _amount | uint256 | 100000000000 |
| 2 | _minExpectedAmount | uint256 | 0 |
Observations#
- Each request deposited 100,000 USDC (
100000000000in 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#
- Tx #2:
0xfe37f25efd67d0a4da4afe48509b258df48757b97810b28ce4c649658dc33743_targetAmount:50000000000000000000000000- Gross output: 50,000,000 USR
- Net to attacker after fee: 49,950,000 USR
- Tx #4:
0x7f914328a67f7094eedb0efda7aef74aafdb7f862ad7bc78259564fd453a931d_targetAmount:99995213000000000012278- Gross output: 99,995.213000000000012278 USR
- Tx #6:
0x41b6b9376d174165cbd54ba576c8f6675ff966f17609a7b80d27d8652db1f18f_targetAmount:30000000000000000000000000- Gross output: 30,000,000 USR
Example calldata#
| # | Name | Type | Data |
|---|---|---|---|
| 0 | _idempotencyKey | bytes32 | 0xb9f73da47014b8fab2ad52a0cd90f07f1a54d801c4366b2e15d3931a3a5e3239 |
| 1 | _id | uint256 | 30 |
| 2 | _targetAmount | uint256 | 50000000000000000000000000 |
Aggregate output#
- Total
_targetAmountacross 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#
0x035b13adafc9abe3804580db3785d0b43e60aa3ecaa686a1ddf7886c3f9333d1- 17,652,777.925065953520352958 wstUSR
0x1a5d16b8ee50d1144a6a0251c81e680e4e20a137ec0268bd51dcd9bc06dcfcd0- 13,239,583.443799465140264718 wstUSR
0x8b329ba0b8a83adcdd971882abe0c2cf618b20176155deeebf0843cc7a096d2d- 88,263.889625329767601764 wstUSR
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#
- 5,500 ETH →
0x6db6006c... - 4,793 ETH →
0x8ed8cf0c... - 500 ETH →
0x8ed8cf0c... - 89.7 ETH →
0x8ed8cf0c...
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#
- The user calls
requestSwap(depositToken, amount, minExpectedAmount). USR Contracttransfers the deposit token from the user and records the request on-chain.- 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 callscompleteSwap(id, targetAmount)on-chain using theSERVICE_ROLEaccount. - The contract transfers the deposited asset to treasury.
- The contract mints the output token.
- The contract transfers the net output amount to the user.
Unable to render diagram.
Deployed Source Code#
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.
minExpectedAmountis 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 byonlyRole(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
_targetAmountto 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:
- Approximately 9 million USR was burned across two transactions on March 22.
- Another approximately 36 million USR held by exploiter-associated wallets in the form of wstUSR was blacklisted. According to Resolv, this was achieved through a wstUSR contract upgrade, which required initiation of a 72-hour timelock due to contract-level constraints.
- The remaining exploiter-held USR was later burned.
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_ROLEis 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_ROLEis 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_ROLEis 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_ROLEis 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_ROLEis 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#
- Source file:
contracts/UsrPriceStorage.sol - Oracle deployment path: proxy
0x7f45180d6fFd0435D8dD695fd01320E6999c261c, implementation0xc16b2a7a773c23e3e9d3325c7b173ef24fc2785d - Functions protected by
SERVICE_ROLE:setReserves(bytes32,uint256,uint256)
- Operational purpose: This function updates the reserve and supply inputs that feed the protocol’s fundamental USR valuation.
- Why
SERVICE_ROLEis required: This is an operational oracle/accounting update and must be restricted to trusted protocol operators.
7. RlpPriceStorage.sol#
- Source file:
contracts/RlpPriceStorage.sol - Oracle deployment path: proxy
0xaE238Cf8268442d0FEc07470F690F58647e296E6, implementation0x5e90D6f6f5F4f3Ee0Cbd9f58a8A4D477A8F0A0f7 - Functions protected by
SERVICE_ROLE:setPrice(bytes32,uint256)
- Operational purpose: This function updates the protocol’s fundamental RLP price.
- Why
SERVICE_ROLEis required: Pricing updates are protocol-controlled accounting actions and are not intended to be permissionless.
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_ROLEis 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_ROLEis 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
stETHandwstETHin the treasury stack. - Why
SERVICE_ROLEis 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_ROLEis required: It acts as a settlement operator with batching and finalization authority over economically sensitive burn flows.
Appendix#
Deployed Contracts#
- https://github.com/resolv-im/resolv-contracts-public
- operational unit (USR Counter, RLP Counter)
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:
Whitelistaddress:0x5943026E21E3936538620ba27e01525bBA311255
USR Requests Manager#
roles: Operational unit handling USR mint / burn request flow.
contract:
USR requests manageraddress:0xAC85eF29192487E0a109b7f9E40C267a9ea95f2e
RLP Requests Manager#
roles: Operational unit handling RLP mint / burn request flow.
contract:
RLP requests manageraddress:0x10f4d4EAd6Bcd4de7849898403d88528e3Dfc872
Reward Distributor#
roles: Operational unit handling reward distribution.
contract:
RewardDistributoraddress:0xbE23BB6D817C08E7EC4Cd0adB0E23156189c1bA9
USR Counter#
roles: Operational swap / counter unit for USR flows; independently deployed and verified on mainnet.
contract:
USR Counteraddress:0xa27a69Ae180e202fDe5D38189a3F24Fe24E55861verified source:TheCounter.sol
RLP Counter#
roles: Operational swap / counter unit for RLP flows; independently deployed and verified on mainnet.
contract:
RLP Counteraddress:0xc7AB90c2Ea9271EFB31f5fA2843Eeb4B331eaFA0verified source:TheCounter.sol
References#
Footnotes#
-
Resolv Lab, Resolv Postmortem: March 22, 2026 Incident. Available at: https://resolv.xyz/blog/resolv-postmortem-march-22-2026-incident ↩
-
Acua Team, Update: Ongoing Investigation and Continued Remediation. Available at: https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know/ ↩
-
Chainalysis, The Resolv Hack: How One Compromised Key Printed $23 Million. Available at: https://www.chainalysis.com/blog/lessons-from-the-resolv-hack/ ↩ ↩2