We do not have a reliable USD price for the recorded assets yet.
0x498b681a6b691dbbccae0671f6d2d0848aa9f2eba3db0dc9dd2806377aaa89910x50f5474724e0ee42d9a4e711ccfb275809fd6d4aEthereumThe Sandbox Land ERC-721 contract on Ethereum exposes a public _burn(address from, address owner, uint256 id) function that lacks any msg.sender or approval checks. At least three on-chain transactions demonstrate that non-owner EOAs can call _burn(victim, victim, id) to irreversibly burn LAND tokens owned by other addresses, resulting in unauthorized destruction of user NFTs.
The Sandbox Land contract implements an ERC-721-style NFT representing LAND parcels, with standard semantics that only token owners or their approved operators should be able to transfer or burn tokens. In the Land implementation, high-level user-facing burn functions are burn(uint256 id) and burnFrom(address from, uint256 id). These functions enforce ownership and operator checks using _ownerOf(id), _ownerAndOperatorEnabledOf(id), metaTransactionContracts, per-token operators, superOperators, and operatorsForAll, before delegating to an internal burn routine. The shared burn routine is implemented as ERC721BaseToken._burn(address from, address owner, uint256 id). In a secure design this function would be internal only and rely on its callers to supply a correct from/owner pair after authorization; however, in this contract _burn is declared public and is exposed externally through the Land ABI. Because _burn is public and only checks from == owner, any EOA can construct calldata that passes this check by setting from == owner == the current ownerOf(id) and thereby induce a burn for any publicly known LAND token id, regardless of who actually controls that owner address.
The root cause is a public access-control vulnerability: ERC721BaseToken._burn(address from, address owner, uint256 id) is declared public and exposed in the Land ABI, but it enforces only from == owner and performs no msg.sender or approval checks, allowing any unprivileged EOA to directly burn LAND tokens they do not own by calling _burn(victim, victim, id).
Invariant: For any LAND token id, a state transition from an owned state (ownerOf(id) == v != address(0) and _owners[id] not equal to the burn sentinel) to a burned state (ownerOf(id) == address(0) and _owners[id] == 2**160, with an ERC-721 Transfer(v, address(0), id) event) must only be reachable via code paths that enforce that msg.sender is either (a) the token owner v, (b) an approved per-token operator for id, (c) a globally approved operator for v, or (d) a designated meta-transaction contract acting on behalf of v, under the checks implemented in burn and burnFrom.
Breakpoint: In ERC721BaseToken.sol, function _burn(address from, address owner, uint256 id) is declared public and implemented as require(from == owner, "not owner"); _owners[id] = 2**160; _numNFTPerAddress[from]--; emit Transfer(from, address(0), id); and Land.sol exposes this function in its external ABI. Because _burn never references msg.sender, _ownerOf(id), _ownerAndOperatorEnabledOf(id), or any approval mappings, an external caller can supply from == owner == current ownerOf(id) and cause a burn even when msg.sender is not an owner or approved operator, violating the invariant.
function _burn(address from, address owner, uint256 id) public {
require(from == owner, "not owner");
_owners[id] = 2**160; // cannot mint it again
_numNFTPerAddress[from]--;
emit Transfer(from, address(0), id);
}
Caption: ERC721BaseToken._burn implementation from the verified Land contract, showing the lack of any msg.sender or approval checks while exposing the function as public.
The Land contract is built atop ERC721BaseToken, which defines a shared burn routine _burn(address from, address owner, uint256 id). The intended design is that user-facing methods enforce access control before calling this internal routine. Indeed, burn(uint256 id) and burnFrom(address from, uint256 id) enforce that msg.sender is either the token owner, a meta-transaction contract, a per- token operator when operatorEnabled is true, a superOperator, or an operatorForAll entry, and only then delegate to _burn. However, ERC721BaseToken._burn is mistakenly declared public rather than internal, and Land.sol includes this function in its ABI. As a result, the function is callable directly from arbitrary EOAs. At the code level, _burn implements only require(from == owner, "not owner"); then writes _owners[id] = 2160 (a sentinel value indicating that the id can never be minted again), decrements _numNFTPerAddress[from], and emits Transfer(from, address(0), id). It never verifies that (from, owner) corresponds to the current ownerOf(id), and crucially it never checks msg.sender or any approval/role mapping. Because ownerOf(id) is publicly observable from on- chain state, an adversary can, at any block where a victim v owns a given LAND id, construct calldata _burn(v, v, id) that passes the only require and deterministically burns the token. The representative exploit tx 0x498b681a6b691dbbccae0671f6d2d0848aa9f2eba3db0dc9dd2806377aaa8991 at block 14164047 exemplifies this. The Land burn scan shows that at blockNumber-1, ownerOf(16653) == 0x2cf80a3efe4119d8e4ee3345267bc72b68aa5649, while tx_from == 0x794151bccaecc44a05e5d411293e5b8ad804258c. The tx's calldata encodes _burn(0x2cf8...5649, 0x2cf8...5649, 16653). The debug trace shows a single CALL into the Land contract executing _burn, which checks from == owner, writes _owners[16653] = 2160, decrements _numNFTPerAddress[0x2cf8...5649], and emits Transfer(0x2cf8...5649, address(0), 16653). No logic in _burn inspects msg.sender or any approval flags, and no other internal calls are made. Thus, the invariant that only owner/approved actors can trigger burns is broken exactly at the public _burn entrypoint.
{
"from": "0x794151bccaecc44a05e5d411293e5b8ad804258c",
"to": "0x50f5474724e0ee42d9a4e711ccfb275809fd6d4a",
"input": "0x6ee678ae0000000000000000000000002cf80a3efe4119d8e4ee3345267bc72b68aa56490000000000000000000000002cf80a3efe4119d8e4ee3345267bc72b68aa5649000000000000000000000000000000000000000000000000000000000000410d",
"type": "CALL"
}
Caption: Seed transaction trace for tx 0x498b681a6b691dbbccae0671f6d2d0848aa9f2eba3db0dc9dd2806377aaa8991, showing a direct CALL from adversary EOA 0x7941...258c to the Land contract with calldata encoding _burn(victim, victim, 16653).
Single-transaction exploit where an unprivileged EOA directly calls the publicly exposed Land._burn(address,address,uint256) function with from == owner == victim and id equal to a LAND token that victim currently owns, thereby bypassing burn/burnFrom authorization logic and burning the token in one step.
Adversary-related accounts:
Victim accounts and tokens:
Lifecycle stages:
[1] Land contract source - ERC721BaseToken._burn implementation — artifacts/root_cause/data_collector/iter_1/contract/1/0x50f5474724e0ee42d9a4e711ccfb275809fd6d4a/source/src/src/Land/erc721/ERC721BaseToken.sol [2] Land.sol and compiled ABI exposing _burn(address,address,uint256) — artifacts/root_cause/data_collector/iter_1/contract/1/0x50f5474724e0ee42d9a4e711ccfb275809fd6d4a/source/out/Land.sol/Land.json [3] Land _burn scan with ownerOf@prev-block and sender_is_owner classification — artifacts/root_cause/data_collector/iter_4/other/1/land_burn_scan_with_owner_checks.json [4] Full Land transaction list including all _burn calls — artifacts/root_cause/data_collector/iter_4/other/1/land_txlist_full.json [5] Debug trace for representative exploit tx 0x498b681a6b691dbbccae0671f6d2d0848aa9f2eba3db0dc9dd2806377aaa8991 — artifacts/root_cause/data_collector/iter_4/tx/1/0x498b681a6b691dbbccae0671f6d2d0848aa9f2eba3db0dc9dd2806377aaa8991/trace.debug_call.json [6] Seed LAND _burn transaction 0x34516ee081c221d8576939f68aee71e002dd5557180d45194209d6692241f7b1 and associated artifacts — artifacts/root_cause/seed/1/0x34516ee081c221d8576939f68aee71e002dd5557180d45194209d6692241f7b1