Reproduced Exploit
Etherspot — a batch consumes lockedAmount twice
_validateBatchCall calls _validateTokenData for every item. That function accepts an item when the wallet balance covers amount and amount equals lockedAmount, then does claimedAmount += amount. It never checks that claimedAmount is still zero.
Chain
Other
Category
logic
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: 61395-c-01-consumers-can-double-the-lockedamount-in-the-session-dr. Standalone Foundry PoC and full write-up: 61395-c-01-consumers-can-double-the-lockedamount-in-the-session-dr_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/missing-check · vuln/logic/state-update
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/61395-c-01-consumers-can-double-the-lockedamount-in-the-session-dr_exp.sol.
Key info#
| Impact | HIGH — the session takes 1,000 extra tokens. The wallet loses 2,000 on a 1,000 lock, and claim pays 0 |
| Protocol | Etherspot CredibleAccountModule |
| Vulnerable code | _validateTokenData adds claimedAmount with no check that it is still zero |
| Bug class | A batch counts the same lock once per item |
| Finding | Shieldify — Credible Account Module mitigation review, July 2025 · C-01 · id 61395 |
| Report | Etherspot-CredibleAccountModule-Mitigation-Security-Review.md |
| Source | AuditVault finding |
| Status | Fixed by requiring claimedAmount == 0 before the increment. Reproduced here as a local synthetic. |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
_validateBatchCall calls _validateTokenData for every item. That function accepts an item when the wallet balance covers amount and amount equals lockedAmount, then does claimedAmount += amount. It never checks that claimedAmount is still zero.
A two-item batch, each for the 1,000 lock, against a wallet holding 2,000:
- Both validations pass.
claimedAmountbecomes 2,000. - The wallet sends 1,000 to the invoice and 1,000 extra to the session caller.
isSessionClaimedis false because 1,000 != 2,000.claimreturns 0.
The vulnerable code#
Pre-fix check, reduced to one locked token and a batch of two equal amounts:
function _validateTokenData(address sessionKey, address wallet, uint256 amount, address token) internal returns (bool) {
LockedToken[] storage tokens = _locks[sessionKey];
for (uint256 i; i < tokens.length; ++i) {
if (tokens[i].token == token) {
if (_walletTokenBalance(wallet, token) >= amount && amount == tokens[i].lockedAmount) {
// @> VULN: claimedAmount grows again with no check that this lock was already consumed
tokens[i].claimedAmount += amount;
return true;
}
}
}
return false;
}
The fixed condition also requires tokens[i].claimedAmount == 0. The second batch item then fails, only 1,000 moves, and claim pays the lock.
Root cause#
The balance check runs before any transfer, so every item in the batch still sees the full wallet balance. Equality with lockedAmount is not a one-time gate. Each passing item adds the whole lock to claimedAmount.
Preconditions#
- The session has one locked token. Here
lockedAmountis 1,000. - The wallet holds at least twice that, here 2,000, and has approved the module.
- The batch contains two items that each spend exactly
lockedAmountof that token.
Attack walkthrough#
- Mint 2,000 to the wallet and approve the module. Enable a session that locks 1,000.
executeBatchvalidates the amount twice, then pulls 1,000 to the invoice and 1,000 to the caller.claimedAmountis 2,000. The caller holds the extra 1,000. The wallet balance is 0.isSessionClaimedreturns false.claimreturns 0.
Control: the second validation fails because claimedAmount is no longer zero. taken is 1,000, the caller receives nothing, the wallet keeps 1,000, and claim pays 1,000.
Diagrams#
Impact#
The smart wallet loses 1,000 tokens beyond the lock. The session caller keeps that extra. The settlement claim pays 0, so the session cannot be closed as claimed. A longer batch of the same amount repeats the pull once per item, for as long as the wallet balance covers another lockedAmount.
Remediation#
Accept the token only when claimedAmount is still zero, then set it to lockedAmount once. Shieldify's fix adds tokens[i].claimedAmount == 0 to the condition in _validateTokenData.
Sources#
- AuditVault finding 61395
- Shieldify mitigation review, July 2025
- CredibleAccountModule.sol @ cb0645c lines 450-487 and
isSessionClaimed
Taxonomy#
vuln/logic/missing-checkvuln/logic/state-update
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 61395-c-01-consumers-can-double-the-lockedamount-in-the-session-dr_exp (evm-hack-registry mirror).
- AuditVault finding: 61395-c-01-consumers-can-double-the-lockedamount-in-the-session-dr.
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.