Reproduced Exploit

A seller installs an OnChainLab executor before listing and drains 5 ETH after the buyer takes the NFT

installModule does not bump state, and executeFromExecutor does not check that the NFT owner is still the account that installed the module. The seller installs a drain executor, the buyer reads an unchanged state of 5, the NFT is transferred, and the executor still sends the lab's 5 ETH to the sel…

May 2026•Ethereum•access-control•4 min read

Loss

5 ETH moved from the sold lab to the seller. The buyer's state snapshot never changes

Chain

Ethereum

Category

access-control

Date

May 2026

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: 65951-onchainlabexecutefromexecutor-pre-listing-module-backdoor-by. Standalone Foundry PoC and full write-up: 65951-onchainlabexecutefromexecutor-pre-listing-module-backdoor-by_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/missing-auth · vuln/logic/missing-check

Key info#

FieldValue
Loss5 ETH moved from the sold lab to the seller. The buyer's state snapshot never changes
Vulnerable contractOnChainLab.executeFromExecutor
Attacker EOA0x0000000000000000000000000000000000065951 (the seller)
Attack contractExploit
Attack txExploit.run()
Chain / block / dateEthereum model · block 0 · 2026-05
Compilersolc 0.8.27, cancun, via IR
Bug classPre-listing executor survives the NFT sale and is invisible to state

TL;DR#

installModule does not bump state, and executeFromExecutor does not check that the NFT owner is still the account that installed the module. The seller installs a drain executor, the buyer reads an unchanged state of 5, the NFT is transferred, and the executor still sends the lab's 5 ETH to the seller. Binding the executor to the installer makes the same call revert and leaves the 5 ETH in the lab.

Background#

Cyfrin's May 2026 Molecule OnChainLab review. A lab is an NFT-bound account. Marketplaces are told to snapshot state so a buyer can see if the account was mutated after listing. Five of the mutating paths bump that counter. Installing an executor and calling it are not among them, and the executor is not cleared when the NFT moves.

The vulnerable code#

SOLIDITY
function executeFromExecutor(address to, uint256 value, bytes calldata) external {
    require(executorInstalled[msg.sender], "not executor");
    // @> VULN: executeFromExecutor does not bump state and does not check the NFT owner, so a pre-listing executor still drains
    asset.transfer(to, value);
}

installModule sets executorInstalled[module] = true and returns. state stays at the listing value. After transferNft, nftOwner is the buyer, and the executor call still succeeds.

Root cause#

The marketplace signal and the execution check are both missing the same fact: an executor installed by the seller is still authorized after the seller no longer owns the NFT.

Preconditions#

  • The seller owns the lab and can install an executor module.
  • The lab holds 5 ETH, which is what the buyer expects to receive.
  • The buyer treats an unchanged state as "the account was not touched."

Attack walkthrough#

  1. state is 5. The seller funds the lab with 5 ETH.
  2. The seller installs the drain executor. state is still 5.
  3. The NFT is transferred to the buyer. state is still 5.
  4. The seller's executor calls executeFromExecutor and transfers the 5 ETH to the seller.
  5. The lab balance is 0. The buyer's snapshot never moved.
  6. The control bumps state on install and reverts the executor once the owner changes. The 5 ETH stays in the lab.

Diagrams#

flowchart TD A["Seller funds the lab with 5 ETH. state is 5"] --> B["installModule stores the drain executor and does not bump state"] B --> C["The NFT is transferred to the buyer. state is still 5"] C --> D["VULN: executeFromExecutor transfers the 5 ETH to the seller"] D --> E["The buyer owns an empty lab and the snapshot never changed"] B --> F["Control records the installer and bumps state"] F --> G["Control reverts the executor after the sale and the lab keeps 5 ETH"]

Impact#

The seller receives 5 ETH. The buyer holds the NFT and an empty account, and every read of state still returns the pre-listing value. The control leaves the 5 ETH in the lab and reverts the post-sale executor call.

Remediation#

Increment state in installModule and executeFromExecutor. Store the NFT owner at install time and revert executeFromExecutor when the current owner is no longer that installer.

How to reproduce#

BASH
cd 65951-onchainlabexecutefromexecutor-pre-listing-module-backdoor-by_exp
forge test -vv

test_exploit_prelisting_executor_drains_buyer asserts the seller holds 5 ETH, the lab is empty, and state did not move. test_control_executor_dies_when_owner_changes asserts the fixed executor reverts and the lab keeps 5 ETH. Captured output is output.txt.

Sources#


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.