Reproduced Exploit
NFTMirror: executeCallback on the native NFT rolls back the ownership update, so the old owner still pulls 1000
triggerOwnershipUpdate sets the new owner and then calls executeCallback on shadowAddress. On the NFT's native chain that address is the NFT itself, and the NFT's executeCallback reverts. The owner write rolls back. The old owner is unchanged and pulls 1000. The control beacon does not call the cal…
Chain
Other
Category
logic
Date
Dec 2024
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: 50037-h-01-executecallback-call-causes-that-ownership-cannot-be-up. Standalone Foundry PoC and full write-up: 50037-h-01-executecallback-call-causes-that-ownership-cannot-be-up_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/incorrect-state-transition · vuln/dependency/unsafe-external-call
Key info#
| Protocol | NFTMirror |
| Finding | Pashov H-01, AuditVault 50037 |
| Vulnerable code | Beacon.triggerOwnershipUpdate |
| Harm | The native-chain callback reverts, so the new owner is not stored. The old owner still pulls 1000. The control skips the callback, stores the new owner, and that owner receives 1000 |
| Chain | local synthetic |
TL;DR#
triggerOwnershipUpdate sets the new owner and then calls executeCallback on shadowAddress. On the NFT's native chain that address is the NFT itself, and the NFT's executeCallback reverts. The owner write rolls back. The old owner is unchanged and pulls 1000. The control beacon does not call the callback. The new owner is stored, the old owner's pull reverts, and the new owner receives 1000.
The vulnerable code#
function triggerOwnershipUpdate(address shadowAddress, address newOwner, bytes32 guid) external {
NFT(shadowAddress).setOwner(newOwner);
INFTShadow _shadow = INFTShadow(shadowAddress);
// @> VULN: on the native chain shadowAddress is the NFT, so executeCallback reverts and the owner write rolls back
_shadow.executeCallback(guid);
}
The report's fix is to call executeCallback only when the update is not running on the NFT's native chain.
Root cause#
_updateOwnership always ends in _shadow.executeCallback(guid). The decoded shadowAddress is the NFT contract when the message is applied on the native chain. That contract has no successful callback, so the whole update reverts and the previous owner remains.
Preconditions#
The NFT holds 1000 and its owner is the old owner. The beacon calls executeCallback on that same NFT. The control beacon only calls setOwner. run() does not warp.
Attack walkthrough#
- Mint 1000 to the native NFT and 1000 to the control NFT.
triggerOwnershipUpdatesets the new owner and callsexecuteCallback. The NFT reverts withno callback on native nft.- The owner is still the old owner. That owner pulls 1000.
- The control update stores the new owner and does not call the callback.
- The old owner's pull reverts. The new owner pulls 1000.
Diagrams#
Impact#
The ownership update that should have removed the old owner does not stick. The old owner moves 1000. On the control path the update completes and the new owner receives 1000.
How to reproduce#
cd 50037-h-01-executecallback-call-causes-that-ownership-cannot-be-up_exp
forge test -vv
run() does not warp. The old owner ends with 1000 from the native NFT. The control NFT's new owner ends with 1000.
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/50037-h-01-executecallback-call-causes-that-ownership-cannot-be-up.md
- Pashov NFTMirror review (30 December 2024): https://github.com/pashov/audits/blob/master/team/md/NFTMirror-security-review_2024-12-30.md
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 50037-h-01-executecallback-call-causes-that-ownership-cannot-be-up_exp (evm-hack-registry mirror).
- AuditVault finding: 50037-h-01-executecallback-call-causes-that-ownership-cannot-be-up.
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.