Reproduced Exploit

Etherspot — wallet B spends 1000 GLUE from wallet A

When the module is installed as a hook, the wallet calls preCheck before execution. The function decodes the wallet from msgData and uses that address as the session owner. It never checks that the decoded wallet is msg.sender. Wallet B encodes wallet A and 1000 GLUE. preCheck pulls 1000 GLUE from…

Jul 2025•Other•access-control•3 min read

Chain

Other

Category

access-control

Date

Jul 2025

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

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.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 61413-h-03-the-prepostcheck-is-not-authorizing-the-wallet-paramete. Standalone Foundry PoC and full write-up: 61413-h-03-the-prepostcheck-is-not-authorizing-the-wallet-paramete_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/fake-account-substitution · 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/61413-h-03-the-prepostcheck-is-not-authorizing-the-wallet-paramete_exp.sol.


Key info#

ImpactHIGH — wallet B calls preCheck with wallet A's address and takes 1000 GLUE from A
ProtocolEtherspot CredibleAccountModule
Vulnerable codepreCheck
Bug classThe hook wallet is not msg.sender
FindingShieldify — Credible Account Module, July 2025 · id 61413 / H-03
ReportEtherspot-CredibleAccountModule-Security-Review.md
SourceAuditVault finding
StatusFixed by Etherspot. Reproduced here as a local synthetic.
Compilersolc 0.8.27, cancun, via IR

TL;DR#

When the module is installed as a hook, the wallet calls preCheck before execution. The function decodes the wallet from msgData and uses that address as the session owner. It never checks that the decoded wallet is msg.sender. Wallet B encodes wallet A and 1000 GLUE. preCheck pulls 1000 GLUE from A to B and adds 1000 to A's locked total. A did not call. B is not A.

The fixed preCheck reverts WalletMismatch unless msg.sender == sender. The same payload from B moves 0, and A still holds 1000 GLUE.

Vulnerable code#

The report's body is the decode. This reduction keeps that decode and shows the transfer it authorizes:

SOLIDITY
function preCheck(address, uint256, bytes calldata msgData) external returns (bytes memory hookData) {
    (address sender, uint256 amount) = abi.decode(msgData, (address, uint256));
    // @> VULN: sender is taken from the payload and is not checked against msg.sender
    token.transferFrom(sender, msg.sender, amount);
    locked[sender] += amount;
    hookData = abi.encode(sender, locked[sender]);
}
DIFF
 (address sender, uint256 amount) = abi.decode(msgData, (address, uint256));
+if (msg.sender != sender) revert WalletMismatch(msg.sender, sender);
 token.transferFrom(sender, msg.sender, amount);

Attack walkthrough#

  1. Wallet A is minted 1000 GLUE and approves the module.
  2. Wallet B builds abi.encode(wallet A, 1000 ether).
  3. B calls preCheck(B, 0, payload). msg.sender is B. The decoded sender is A.
  4. transferFrom moves 1000 GLUE from A to B. locked[A] becomes 1000.
  5. The hook return value names A. B's address is not A's. B's balance is 1000 GLUE and A's is 0.

Control: B sends the same payload to the fixed module. The call reverts WalletMismatch. A still holds 1000 GLUE. B holds 0.

Diagrams#

flowchart TD A["Wallet A holds 1000 GLUE and approves the module"] --> B["Wallet B encodes A and 1000 in msgData"] B --> C["B calls preCheck"] C --> D["sender is decoded as A and is not compared to B"] D --> E["transferFrom moves 1000 GLUE from A to B"] E --> F["B holds 1000 and A holds 0"]

Impact#

Any wallet that installed the hook can name another wallet in preCheck and move that wallet's approved balance. Here B takes 1000 GLUE from A and the module records the charge against A.

Remediation#

Require msg.sender == sender at the start of preCheck and postCheck before any balance or session write. A hook call may only account the wallet that actually called.

Sources#

Taxonomy#

  • severity/high
  • vuln/access-control/fake-account-substitution · vuln/access-control/missing-check · vuln/logic/missing-check

Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

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.