Files
zeekayandHanzo Dev 4d8f97f19f docs(market): the Ethereum and BSC collections, measured
The import had no collection behind it. There are two, both live: Lux Genesis
(LUXNFT, 50 tokens, 28 holders) on Ethereum mainnet, and CryptoZoo (ANML, 5840
tokens, 2676 holders) on BSC. Every figure enumerated over eth_call today, with
the command beside it.

Records what an import must carry that a snapshot alone does not: lux.town has
lost its delegation, so all 100 Ethereum URIs are dead; the video is recovered
on cdn.lux.network but the metadata was a dynamic route and needs a renderer,
not a rehost. The Media contract cannot be repaired on Ethereum — only the 28
holders can rewrite their own tokens.

Weighs the three routes. The bridge corridor's contract half exists and its Go
half does not; the snapshot-and-mint is already planned and verified but leaves
the Ethereum originals alive, which the record states rather than hides.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-29 18:19:02 -07:00

18 KiB
Raw Permalink Blame History

Ethereum, measured — 2026-08-29

Every line below was probed live today against a public RPC, at Ethereum block 25863208 and BSC block 118829025. Prefer these over any inference. Re-measure rather than trusting a stale line; do not contradict one without measuring first.

The question this answers: the user said our NFTs are "principally imported from ethereum". They are. There is one Ethereum collection and one BSC collection, both real, both live, both ours.

The collection on Ethereum

Lux Genesis — LUXNFT, ERC-721, 50 tokens, Ethereum mainnet.

Media (the ERC-721) 0x31e0F919C67ceDd2Bc3E294340Dc900735810311
Drop ("Gen 0", the type registry) 0x941E3B4fC883B1746E52814EB8574b85E6Fa4E66
App (orchestrator, UUPS) 0x44A210571E135C6a536564e9B14dc4DA63f3D398
cast call 0x31e0F919C67ceDd2Bc3E294340Dc900735810311 "name()(string)"        --rpc-url https://ethereum-rpc.publicnode.com   → "LUXNFT"
cast call 0x31e0F919C67ceDd2Bc3E294340Dc900735810311 "symbol()(string)"      → "LUXNFT"
cast call 0x31e0F919C67ceDd2Bc3E294340Dc900735810311 "totalSupply()(uint256)"→ 50
cast call 0x31e0F919C67ceDd2Bc3E294340Dc900735810311 "supportsInterface(bytes4)(bool)" 0x80ac58cd → true
cast call 0x31e0F919C67ceDd2Bc3E294340Dc900735810311 "supportsInterface(bytes4)(bool)" 0xd9b67a26 → false
cast code  0x31e0F919C67ceDd2Bc3E294340Dc900735810311 → 20318 bytes
cast code  0x941E3B4fC883B1746E52814EB8574b85E6Fa4E66 →  9912 bytes
cast code  0x44A210571E135C6a536564e9B14dc4DA63f3D398 →   680 bytes

It is a Zora v1 fork: Media carries per-token content and metadata URIs plus per-token content and metadata hashes. Source is github.com/luxdefi/town (contracts/src/{Media,Drop,App}.sol). Provenance and history already live in ~/work/lux/tokenomics — that repo is the source of truth and every figure it states about Ethereum reproduced exactly under live query today.

Every one of the 50, enumerated

ownerOf, tokenURI, tokenContentHashes, tokenMetadataHashes and tokenMetadataURI called for ids 1..51 over eth_call. Full output saved at scratchpad/lux_genesis_eth.json.

Measured Value
ids live 1–50, contiguous; id 51 reverts nonexistent token
distinct holders 28
holdings shape 20 wallets hold 1 · 4 hold 2 · 2 hold 3 · 1 holds 6 · 1 holds 10
largest holder 0xb7c5819f928a02ff3946b369d997e1d52712bf41 — 10 tokens
second 0x8d56c7cf8b17a11580822c7fff90b05b6a3e1b5e — 6 tokens
burned none — zero tokens at 0x0 or 0xdead
contentHash 50/50 non-zero, 50/50 distinct
metadataHash 50/50 non-zero, 50/50 distinct

Type is derived from the tokenURI, and that is the only on-chain type signal:

tokenURI shape Meaning Count
validator.mov?...&type=__validator__ Genesis Validator 32
wallet.mov?lux=1000000000&... Coin, 1B bond 2
wallet.mov?lux=100000000&... Coin, 100M bond 5
wallet.mov?lux=10000000&... Coin, 10M bond 8
wallet.mov?lux=1000000&... Coin, 1M bond 3

The Drop agrees, queried independently:

cast call 0x941E3B4fC883B1746E52814EB8574b85E6Fa4E66 "title()(string)" → "Gen 0"
cast call 0x941E3B4fC883B1746E52814EB8574b85E6Fa4E66 "totalMinted(string)(uint256)" "Validator"       → 32
                                                                                    "Wallet 1B Lux"   →  2
                                                                                    "Wallet 100M Lux" →  5
                                                                                    "Wallet 10M Lux"  →  8
                                                                                    "Wallet 1M Lux"   →  3
                                                                                    "Wallet 10B Lux"  →  0
                                                                                    "ATM"             →  0

32 + 2 + 5 + 8 + 3 = 50. Two independent contracts, same answer.

The art survives; the metadata does not

This is the part an import has to plan around, and it splits in two.

lux.town is gone. All 50 tokenURIs and all 50 metadataURIs point at it.

dig +short NS  lux.town @1.1.1.1   → (nothing)
dig +short SOA lux.town @1.1.1.1   → (nothing)
dig            lux.town @1.1.1.1   → status: SERVFAIL
curl https://lux.town/nfts/validator.mov → could not resolve host

No nameservers at all — the delegation itself has lapsed, so this is not a server that is down, it is a name that no longer exists.

cdn.lux.network is live and carries the video, but not the JSON.

curl -o /dev/null -w '%{http_code} %{content_type} %{size_download}' \
  https://cdn.lux.network/nfts/validator.mov  → 200 video/mp4  908069
  https://cdn.lux.network/nfts/wallet.mov     → 200 video/mp4 1297800
  https://cdn.lux.network/api/metadata/validator.json → 404
  https://cdn.lux.network/nfts/validator.json        → 404

So the two video assets — the whole collection is two pieces of art, 32 tokens share one and 18 share the other — are recovered and served. The metadata JSON is not, and it cannot simply be copied across: the old path was lux.town/api/metadata/{validator,wallet}.json?lux=N&name=…&type=…, a dynamic route whose response depended on the query string. Restoring it means re-implementing the renderer, not re-hosting a file.

The Ethereum URIs cannot be repaired on Ethereum. The deployed Media has no setTokenURI, no setBaseURI and no owner override. Its only mutator is updateTokenURI(uint256,string) gated onlyApprovedOrOwner, so only each of the 28 holders can rewrite their own tokens; owner() cannot. The existing fix is client-side and already shipped — rewrite host and extension at read time, leave the chain alone (NFT_MEDIA_BASE in lux/cloud/apps/web/src/lib/brand.ts, applied by nftMedia() in lib/chain.ts). Rewriting the on-chain URIs would also destroy the only type signal the collection has, since ?type=__validator__ and ?lux=N are what classify every token. Do not do it.

The per-token content hashes do not help here. They are distinct for all 50 — including for the 32 validators that share one video — so they commit to something per-token, not to the artwork, and cannot be used to prove a recovered file is the original.

The collection on BSC

Zoo's NFT history is BSC, not Ethereum, and it is much larger than the Lux one.

CryptoZoo — ANML, ERC-721, 5840 tokens, BSC chain 56.

Media (the ERC-721) 0x91109c6e2AaF421456aafAb4ba3a122A95b46B28
Drop 0x6f918d5E359276A8A4120BC4Af89d0A8a044Fe48
Market (Zora-style, live) 0x3a85FCD47573e217728E0EF0d9A3b932B9601afc
Savage 0xfa397EFCA5FD60E672EFCB511fDB885D815e2fb8
ZooKeeper 0x24d4B6Ba0726Ec6Ba0297084be938278247e046B
DAO 0x85Bb05348905eDE5D6f91EC0F0B1e7957d978461

ZooKeeper and DAO are both 680-byte EIP-1967 proxies. Reading slot 0x360894a1…382bbc gives ZooKeeper's implementation as 0x2e0e0bbaa0fa2a7821fcfc197c1cafb98bcba3e1 (23748 bytes) and the DAO's as 0xf1dbecf359d5bb9c79f3ce268dc2a4f28af44ce3 (2441 bytes). Both are upgradeable, which matters if anything is ever built against them.

Found in ~/work/zoo/zoo-v4/packages/contracts/deployments/mainnet/*.json (and mirrored in zoo3/, zoo-ai/, zoo/contracts/deployments/v4/). The directory is named mainnet but it is BSC: its UniswapV2Router02 is 0x10ED43C718714eb63d5aA57B78B54704E256024E, the PancakeSwap router, and its BNB entry is WBNB.

cast call 0x91109c6e2AaF421456aafAb4ba3a122A95b46B28 "name()(string)" --rpc-url https://bsc-dataseed.binance.org → "CryptoZoo"
                                                     "symbol()(string)"       → "ANML"
                                                     "totalSupply()(uint256)" → 5840
                                                     "supportsInterface(bytes4)(bool)" 0x80ac58cd → true
                                                     "supportsInterface(bytes4)(bool)" 0x780e9d63 → true   (enumerable)
cast code 0x91109c6e2AaF421456aafAb4ba3a122A95b46B28 → 17778 bytes

All 5840 ownerOf calls resolved (scratchpad/cryptozoo_owners.json):

Measured Value
ids live 1–5840, contiguous; 0 and 5841 revert
distinct holders 2676
holdings shape 820 hold 1 · 548 hold 2 · 1308 hold 3 — nothing holds more
burned none
tokenURI https://db.zoolabs.io/egg.mp4 — identical on every token sampled
metadataURI https://db.zoolabs.io/egg.json — likewise
contentHash per-token distinct (ids 1, 2, 5840 all differ)

The hard cap of 3 per wallet is visible in the data and is a mint rule, not an accident of distribution.

db.zoolabs.io does not resolve, but zoolabs.io does.

dig +short zoolabs.io    @1.1.1.1 → 104.21.42.77, 172.67.159.61   (Cloudflare)
dig +short db.zoolabs.io @1.1.1.1 → (nothing)
curl https://cdn.lux.network/nfts/egg.mp4 → 404

That is a materially better position than lux.town. The parent zone is alive and ours, so the subdomain is a DNS record away from working — assuming the two files still exist somewhere. They are not on the Lux CDN. Whether egg.mp4 and egg.json are recoverable is the single open question on the Zoo side, and it should be answered before anything is built against this collection.

Not ours, despite appearances

Two of the Zoo BSC addresses also have code on Ethereum mainnet. They are unrelated contracts, and reporting them as ours would be wrong.

cast code 0x91109c6e2AaF421456aafAb4ba3a122A95b46B28 --rpc-url https://ethereum-rpc.publicnode.com → 4940 bytes
cast code 0x19263F2b4693da0991c4Df046E4bAA5386F5735E --rpc-url https://ethereum-rpc.publicnode.com → 4940 bytes
cast keccak <both bodies> → 0xfb8e4f92556434f9ff5f64cd8ef561f33412bfe90b6a85162bf8f4f6025f0b81 (identical)
cast call <either> "name()/symbol()/totalSupply()/supportsInterface(0x80ac58cd)" → all revert
cast call <either> "owner()(address)" → 0x0167b39485A704D28F44C5ec67e8263a41E4BfB1   (agrees on two RPCs)

Identical bytecode at both, and its selectors resolve to withdrawUSDT(uint256), withdrawEther(), withdrawAllTokens(address) and usdtToken(). A USDT sweeper, twice deployed, by an unrelated party whose deployer nonces happened to collide with Zoo's. Not an NFT, not ours.

The other seven candidate addresses return 0 bytes on Ethereum: 0x3a85FCD4…, 0x6f918d5E…, 0x24d4B6Ba…, 0x8e7788ee…, 0x7fFC1243…, 0xfa397EFC…, 0x85Bb0534….

The three ZOO ERC-20s on BSC are live and are tokens, not collectibles — v1 0x8e7788ee…, v2 0x19263F2b…, v3 0x7fFC1243…, all name() "ZOO", 18 decimals, ~2e30 base units. Recorded so nobody mistakes them for the NFT.

Where the Lux side stands today

The re-mint target named throughout tokenomics/ is empty:

cast code 0x004287C47efc912FEc391979154454a8017A76C6 --rpc-url https://api.lux.network → 0 bytes

This is expected and documented: the 2026-07-10 genesis reset on 96369 wiped all on-chain NFT state. What is live instead is a partial, defective re-mint:

cast call 0x9e04fc57c20b2ee45627c4aa280eb471f2ca6ea5 "name()(string)"         → "Lux Genesis"
                                                     "symbol()(string)"       → "GENESIS"
                                                     "totalSupply()(uint256)" → 3
                                                     "owner()(address)"       → 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714  (DAO)
cast call 0x9e04fc57… "tokenURI(uint256)(string)" 0 → "https://lux.town/nfts/https://lux.town/nfts/validator.mov"

Three tokens out of a canonical 118, all three carrying a doubled tokenURI — a base-URI concatenated onto an already-absolute URI — pointing at the dead host. Two are held by 0x55be906E…, one by 0x67bD7c7C…. Neither address appears in the Ethereum holder set. Whatever these three are, they are not the migration.

The tier table is real and should be trusted: _getLuxForTier in ~/work/lux/standard/contracts/nft/GenesisNFTs.sol returns 1B / 100M / 10M / 1M LUX for GENESIS / VALIDATOR / MINI / NANO. A reward multiplier does not exist anywhere in that contract — grep -rniE 'multiplier' contracts/nft/ returns nothing. Rewards are claimed against luxLocked, not a tier factor. Any UI showing 10x/5x/2x/1x is showing a number nothing produced.

Bringing them over

Three routes. They differ in who is trusted, not in difficulty alone.

A bridge corridor

~/work/lux/standard/contracts/bridge/XChainVault.sol already has vaultERC721(address,uint256,uint32,address) and a TokenType.ERC721, and its release path branches on that type. The contract half is written.

The Go half is not. grep -ril 'erc721' bridge/pkg bridge/cmd bridge/internal matches only vendored dist/ bundles and rollup caches — no handler, no message type, no mint call. A locked token would be taken custody of and never appear on the far side.

Building it means: an ERC-721 vault event the relayer recognises, a message carrying (collection, tokenId, recipient) and enough metadata to reconstruct the token, a mint authority on the Lux side, and a replay guard. Then it has to be operated and secured indefinitely, because a bridge is a standing liability, not a one-time job. For 50 tokens across 28 holders this is the most expensive option by a wide margin, and it buys nothing the others don't.

A snapshot and a fresh mint

Read all 50 owners at a pinned block, mint the same ids to the same addresses on 96369, done in one transaction batch.

This is already built. ~/work/lux/tokenomics/data/genesis-remint-plan.json is an un-executed plan of 118 mints — 100 validators, 18 coins — each row carrying serial, tier, to, originTokenId and bond, generated by scripts/gen-remint-plan.mjs. GenesisNFTs.sol stores originTokenId as a first-class field, so the link back to Ethereum is preserved on-chain. standard/script/DeployGenesisNFTsMigrate.s.sol and the three proofs in migration/tokenomics-split-forkproof/test/ already exist. My own enumeration today independently reproduces the Ethereum half of that plan exactly.

What it costs honestly: the Ethereum originals keep existing. Nothing burns them, nothing locks them, and the Media contract has no mechanism that could. So after the mint, two chains each hold a token claiming to be Genesis #7, and the only thing making the Lux one authoritative is that we say so. If an Ethereum token trades afterwards, its Lux twin is already sitting with the previous owner. A snapshot is a promise about a moment, and it decays from the moment it is taken.

A claim contract

Deploy on 96369, let a holder prove they own Ethereum token N, mint N to them. Nothing is minted until someone claims, so the snapshot never goes stale and the double-claim question answers itself with a claimed[tokenId] flag.

The cost is the proof. There is no trustless read of Ethereum state from 96369 today — no light client, no state-proof precompile — so the proof is either a signature from the holder's Ethereum key checked against a merkle root we publish, which is a snapshot again with extra steps, or an attestation from an oracle we run, which is us again. And every unclaimed token stays unclaimed forever; some of those 28 wallets are four years old and some are certainly lost.

What I would do

Take the snapshot-and-mint, and make the snapshot honest rather than pretending it is a bridge.

The numbers decide it. Fifty tokens. Twenty-eight holders. Two pieces of art. A plan file that already exists, verified against the chain twice. Building a standing bridge corridor for that is not caution, it is spending a permanent security budget to avoid writing down a fact we already know.

Two things make it defensible rather than merely convenient:

Pin the snapshot to a stated Ethereum block and publish it. The claim being made is "these were the owners at block N", which is checkable by anyone forever. It is not "these are the owners", which stops being true the next time a token moves.

Say plainly, in the UI and on-chain, that the Ethereum tokens still exist. The originTokenId field is already there for exactly this. A Lux Genesis token is the Lux representation of Ethereum token N — not a claim that N stopped existing. Overstating that is the one thing that would make this dishonest, and it costs nothing to avoid.

Then leave a claim path open for the tail. If a token moves on Ethereum after the pin, the new owner has a route that does not require re-running the migration for everyone.

CryptoZoo is a different decision and should be made separately. 5840 tokens and 2676 holders is a real population, the art is not currently retrievable, and BSC's live Zora Market at 0x3a85FCD4… means those tokens may still be trading. Recover egg.mp4 and egg.json first. Deciding how to import a collection whose art we cannot serve is deciding in the dark.

Absent, stated plainly

  • No Ethereum NFT beyond 0x31e0F919…. Searched ~/work/lux, ~/work/zoo and ~/work/hanzo for OpenSea links, Etherscan token links, deployments/ trees, addresses.json, .env.example, decks, papers and LLM.md, and git log --diff-filter=D for deleted address files in both trees. That search returns nothing deleted and nothing else live. Hanzo has no NFT anywhere.
  • No ERC-1155 anywhere. supportsInterface(0xd9b67a26) is false on both collections.
  • No import machinery for a collection we do not own. Everything that exists is hard-wired to 0x31e0F919…. If the user names another collection, it will need a parameter, not an edit.
  • No CryptoZoo migration record. No snapshot, no claim contract, no mapping file anywhere in ~/work/zoo. Zoo chain 200200 holds no NFT of any kind, and 0x91109c6e… has 0 bytes there.
  • The art host question is open on the Zoo side only. lux.town's video is recovered on cdn.lux.network; its metadata JSON is not, and needs a renderer rather than a file. db.zoolabs.io has neither, though the parent zone is ours.