Reproduced Exploit
Etherspot — an outsider userOp moves 1000 GLUE
onInstall sets validatorInitialized[msg.sender] when a wallet installs the module as a validator. validateUserOp never reads that flag. It checks the signature length, recovers the session key from the first 20 bytes, checks that the key is bound to the userOp wallet, and then transferFroms the amo…
Chain
Other
Category
access-control
Date
Jul 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: 61412-h-02-in-credibleaccountmodule-the-validateuserop-function-is. Standalone Foundry PoC and full write-up: 61412-h-02-in-credibleaccountmodule-the-validateuserop-function-is_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/missing-auth · vuln/access-control/missing-check · vuln/logic/missing-check
Reproduction: a self-contained Foundry PoC that compiles and runs offline with only
forge-std— no fork and no RPC. Trace: output.txt. PoC: test/61412-h-02-in-credibleaccountmodule-the-validateuserop-function-is_exp.sol.
Key info#
| Impact | HIGH — a caller who never installed the module moves 1000 GLUE out of a wallet that did |
| Protocol | Etherspot CredibleAccountModule |
| Vulnerable code | validateUserOp |
| Bug class | The validator does not authenticate its caller |
| Finding | Shieldify — Credible Account Module, July 2025 · id 61412 / H-02 |
| Report | Etherspot-CredibleAccountModule-Security-Review.md |
| Source | AuditVault finding |
| Status | Fixed by Etherspot. Reproduced here as a local synthetic. |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
onInstall sets validatorInitialized[msg.sender] when a wallet installs the module as a validator. validateUserOp never reads that flag. It checks the signature length, recovers the session key from the first 20 bytes, checks that the key is bound to the userOp wallet, and then transferFroms the amount. An outsider who did not install the module submits that userOp and receives 1000 GLUE. The wallet's installed flag stays true. The caller's flag stays false.
The fixed function reverts CredibleAccountModule_ModuleNotInstalled when validatorInitialized[msg.sender] is false. The same userOp moves 0 and the wallet still holds 1000 GLUE.
Vulnerable code#
function validateUserOp(address wallet, uint256 amount, bytes calldata signature) external returns (uint256) {
if (signature.length < 65) return 1;
address sessionKey = address(bytes20(signature));
if (sessionWallet[sessionKey] != wallet) return 1;
// @> VULN: the caller is not required to be a wallet that installed this validator
token.transferFrom(wallet, msg.sender, amount);
live[sessionKey] = false;
return 0;
}
The check Etherspot added:
function validateUserOp(address wallet, uint256 amount, bytes calldata signature) external returns (uint256) {
+ if (!validatorInitialized[msg.sender]) revert CredibleAccountModule_ModuleNotInstalled(msg.sender);
if (signature.length < 65) return 1;
Attack walkthrough#
- The wallet is minted 1000 GLUE and approves the module.
- The wallet calls
onInstall.validatorInitialized[wallet]becomes true. - A session key is bound to that wallet.
- The outsider calls
validateUserOp(wallet, 1000 ether, signature). The signature is 65 bytes and starts with the bound key.validatorInitialized[outsider]is false and is not consulted. transferFrommoves 1000 GLUE to the outsider. The call returns 0.- The wallet balance is 0. The outsider balance is 1000 GLUE.
Control: the wallet still installs the module, and the same userOp is sent by a caller who did not. The call reverts ModuleNotInstalled. The wallet still holds 1000 GLUE.
Diagrams#
Impact#
Any caller can settle a userOp against a wallet that installed the module. Here that moves 1000 GLUE from the wallet to the outsider. The installation flag that is supposed to restrict validateUserOp to the installing wallet is never read.
Remediation#
At the start of validateUserOp, require validatorInitialized[msg.sender] and revert CredibleAccountModule_ModuleNotInstalled otherwise. Only a wallet that installed the module as a validator may change its session state or move its tokens through this function.
Sources#
- AuditVault finding 61412
- Shieldify Etherspot Credible Account Module review
- Reviewed module: etherspot-modular-accounts@d4774db
src/modules/validators/CredibleAccountModule.sol
Taxonomy#
severity/highvuln/access-control/missing-auth·vuln/access-control/missing-check·vuln/logic/missing-check
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 61412-h-02-in-credibleaccountmodule-the-validateuserop-function-is_exp (evm-hack-registry mirror).
- AuditVault finding: 61412-h-02-in-credibleaccountmodule-the-validateuserop-function-is.
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.