Reproduced Exploit
Etherspot ResourceLockValidator — a lock signature authorizes a different batch
validateUserOp recovers the wallet owner over the merkle root of a ResourceLock parsed from the first enableSessionKey execution. A batch is not required to have length 1, and the loop returns on that first execution. The owner signature therefore authorizes a second execution the owner never signe…
Loss
1,000.000000 USDC leaves the modular wallet and arrives at the attacker
Chain
Ethereum
Category
auth
Date
Jun 2025
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 61410-c-08-in-resourcelockvalidator-the-validateuserop-function-la. Standalone Foundry PoC and full write-up: 61410-c-08-in-resourcelockvalidator-the-validateuserop-function-la_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/auth/signature-validation · vuln/logic/missing-check
Key info#
| Field | Value |
|---|---|
| Loss | 1,000.000000 USDC leaves the modular wallet and arrives at the attacker |
| Vulnerable contract | ResourceLockValidator.validateUserOp |
| Attacker EOA | 0x1111111111111111111111111111111111111111 |
| Attack contract | Exploit |
| Attack tx | Exploit.run() |
| Chain / block / date | Ethereum model · block 0 · 2025-06 |
| Compiler | solc 0.8.27, cancun, via IR |
| Bug class | Signature covers the ResourceLock root, not the executed batch |
TL;DR#
validateUserOp recovers the wallet owner over the merkle root of a ResourceLock parsed from the first enableSessionKey execution. A batch is not required to have length 1, and the loop returns on that first execution. The owner signature therefore authorizes a second execution the owner never signed: USDC.transfer of 1,000 USDC to the attacker. The wallet runs both calls. A fixed validator that returns failure unless the batch length is 1 rejects the same userOp, and the 1,000 USDC stays in the wallet.
Background#
Shieldify's review of Etherspot's Credible Account Module (C-08, critical) covered ResourceLockValidator at commit d4774db9f544cc6f69000c55e97627f93fe7242b (2025-06-09). The validator is meant to authorize session-key setup. In the merkle path the signed message is the resource-lock root. The batch decoder is documented as expecting a single execution, but it returns as soon as it sees selector 0x495079a0 (enableSessionKey(bytes)).
The vulnerable code#
Execution[] calldata batchExecs = ExecutionLib.decodeBatch(callData[100:]);
for (uint256 i; i < batchExecs.length; ++i) {
if (bytes4(batchExecs[i].callData[:4]) == ENABLE_SELECTOR) {
return _lockFromEnable(batchExecs[i].callData);
}
}
// later, in validateUserOp
// @> VULN: the owner signature is recovered over the resource-lock root, not the userOp
if (walletOwner == ECDSA.recover(root, ecdsaSignature)) {
return SIG_VALIDATION_SUCCESS;
}
The length fix from the report, which is what the control implements:
Execution[] calldata batchExecs = ExecutionLib.decodeBatch(callData[100:]);
+if (batchExecs.length != 1) return SIG_VALIDATION_FAILED;
Root cause#
The recovered signer is compared to ECDSA.recover(root, signature), and root is the hash of the ResourceLock fields only (chainId, wallet, session key, validity, bid hash, token data). userOp.callData's execution list is not in that hash. Because _getResourceLock returns inside the loop, a later Execution is never read during validation and is still performed by the wallet after validation succeeds.
Preconditions#
- The modular wallet has installed the validator with
owner = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf(private key0x1). - The owner has signed the raw resource-lock root. The proof is empty, so the root equals the leaf. The signature is 97 bytes:
r || s || v || root. - The wallet holds 1,000 USDC and will execute every item in a batch once validation returns 0.
Attack walkthrough#
- The wallet installs the validator for the owner.
- The exploit builds a two-item batch. Item 0 is
enableSessionKeywith the signed lock. Item 1 isUSDC.transfer(attacker, 1000e6). decodeBatchexposes both items. The loop returns the lock from item 0.ecrecoverof the root returns the owner. Validation returns success.- The wallet calls both targets. The transfer debits the wallet and credits the attacker 1,000 USDC.
executionsis 2 and the wallet balance is 0.
Diagrams#
Impact#
1,000.000000 USDC moves from the modular wallet to the attacker in one userOp. The control validator returns SIG_VALIDATION_FAILED for that same batch, handleOp reverts, and the wallet still holds 1,000 USDC. A length-1 batch that only calls enableSessionKey is accepted and does not move USDC.
PoC#
cd 61410-c-08-in-resourcelockvalidator-the-validateuserop-function-la_exp
forge test -vv
test_exploit_batch_drains_1000_usdc checks the hardcoded signature against vm.sign of the lock root and then asserts the attacker balance is 1,000e6. test_control_length_check_rejects_userop asserts the two-item userOp reverts on the fixed wallet and the balance stays 1,000e6.
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/61410-c-08-in-resourcelockvalidator-the-validateuserop-function-la.md
- Shieldify report: https://github.com/shieldify-security/audits-portfolio-md/blob/main/Etherspot-CredibleAccountModule-Security-Review.md
- Reviewed source: https://github.com/etherspot/etherspot-modular-accounts/blob/d4774db9f544cc6f69000c55e97627f93fe7242b/src/modules/validators/ResourceLockValidator.sol
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 61410-c-08-in-resourcelockvalidator-the-validateuserop-function-la_exp (evm-hack-registry mirror).
- AuditVault finding: 61410-c-08-in-resourcelockvalidator-the-validateuserop-function-la.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.