Asset Duplication Attack
Exploit where players can create multiple copies of valuable in-game NFTs or tokens through contract vulnerabilities, state desynchronization, or race conditions.
Asset Duplication Attacks represent one of the most devastating vulnerabilities in GameFi protocols, allowing attackers to create multiple copies of valuable NFTs, tokens, or in-game items. These exploits can instantly collapse gaming economies, destroy player trust, and result in massive financial losses for both players and protocols.
Types of Asset Duplication Attacks
1. Reentrancy-Based Duplication
Classic reentrancy vulnerabilities applied to gaming assets:
1// VULNERABLE: Reentrancy in item transfer2contract VulnerableGameItems is ERC721 {3 mapping(uint256 => bool) public itemInUse;4 mapping(address => uint256[]) public playerInventory;56 function useItem(uint256 tokenId) external {7 require(ownerOf(tokenId) == msg.sender, "Not owner");8 require(!itemInUse[tokenId], "Already in use");910 itemInUse[tokenId] = true;1112 // VULNERABILITY: External call before state update13 IGameEffect(getItemEffect(tokenId)).applyEffect(msg.sender, tokenId);1415 // State update happens after external call - too late!16 updatePlayerStats(msg.sender, tokenId);17 }1819 function transferFrom(address from, address to, uint256 tokenId) public override {20 require(!itemInUse[tokenId], "Item in use"); // Check2122 super.transferFrom(from, to, tokenId); // Transfer2324 // VULNERABILITY: No protection against reentrancy during transfer25 updateInventory(from, to, tokenId);26 }27}2829// ATTACK CONTRACT: Exploits reentrancy to duplicate items30contract DuplicationAttack {31 VulnerableGameItems public gameItems;32 uint256 public targetTokenId;33 address public accomplice;3435 function exploit(uint256 tokenId, address _accomplice) external {36 targetTokenId = tokenId;37 accomplice = _accomplice;3839 // Start the attack40 gameItems.useItem(tokenId);41 }4243 function applyEffect(address player, uint256 tokenId) external {44 // This function is called during useItem()45 // We can trigger a transfer during the external call46 if (msg.sender == address(gameItems)) {47 // Transfer the item to accomplice while it's still marked as "in use"48 gameItems.transferFrom(player, accomplice, tokenId);4950 // Now we have:51 // 1. Item transferred to accomplice52 // 2. Original player still has "in use" state53 // 3. Both can potentially claim benefits54 }55 }56}5758// SECURE: Reentrancy protection59contract SecureGameItems is ERC721, ReentrancyGuard {60 mapping(uint256 => bool) public itemInUse;6162 function useItem(uint256 tokenId) external nonReentrant {63 require(ownerOf(tokenId) == msg.sender, "Not owner");64 require(!itemInUse[tokenId], "Already in use");6566 itemInUse[tokenId] = true;6768 // Safe external call after state update69 IGameEffect(getItemEffect(tokenId)).applyEffect(msg.sender, tokenId);7071 // Additional validations after external call72 require(ownerOf(tokenId) == msg.sender, "Ownership changed during use");73 }74}
2. State Desynchronization Duplication
When multiple contracts tracking the same asset become out of sync:
1// VULNERABLE: Separate contracts without proper synchronization2contract GameNFT is ERC721 {3 // Only handles NFT ownership4 function transferFrom(address from, address to, uint256 tokenId) public override {5 super.transferFrom(from, to, tokenId);6 // Missing: notification to game logic7 }8}910contract GameLogic {11 mapping(address => uint256[]) public playerInventory;12 mapping(uint256 => ItemState) public itemStates;1314 function useItemInBattle(uint256 tokenId) external {15 require(gameNFT.ownerOf(tokenId) == msg.sender, "Not owner");1617 // VULNERABILITY: No check for existing usage18 itemStates[tokenId] = ItemState.IN_BATTLE;1920 // Player can transfer NFT to another address and use it again21 }22}2324// ATTACK SCENARIO:25// 1. Player A owns NFT #12326// 2. Player A calls useItemInBattle(123) - item marked as IN_BATTLE27// 3. Player A transfers NFT #123 to Player B28// 4. Player B can call useItemInBattle(123) again29// 5. Same item is now used in two battles simultaneously3031// SECURE: Proper synchronization32contract SecureGameLogic {33 mapping(uint256 => ItemState) public itemStates;3435 modifier requiresItemOwnership(uint256 tokenId) {36 require(gameNFT.ownerOf(tokenId) == msg.sender, "Not owner");37 require(!isItemLocked(tokenId), "Item locked");38 _;39 }4041 function useItemInBattle(uint256 tokenId) external requiresItemOwnership(tokenId) {42 // Lock the item to prevent transfers during use43 lockItem(tokenId, BATTLE_DURATION);44 itemStates[tokenId] = ItemState.IN_BATTLE;4546 emit ItemUsedInBattle(msg.sender, tokenId);47 }4849 function lockItem(uint256 tokenId, uint256 duration) internal {50 itemLocks[tokenId] = ItemLock({51 locked: true,52 owner: msg.sender,53 expiresAt: block.timestamp + duration54 });5556 // Notify NFT contract about the lock57 gameNFT.setItemLocked(tokenId, true);58 }59}
3. Race Condition Duplication
Exploiting timing windows in multi-step operations:
1// VULNERABLE: Race condition in crafting2contract VulnerableCrafting {3 mapping(address => uint256[]) public playerItems;4 mapping(uint256 => bool) public itemUsed;56 function craftItem(uint256[] calldata inputItems, uint256 outputItemId) external {7 // Validate inputs8 for (uint256 i = 0; i < inputItems.length; i++) {9 require(gameNFT.ownerOf(inputItems[i]) == msg.sender, "Not owner");10 require(!itemUsed[inputItems[i]], "Item already used");11 }1213 // VULNERABILITY: Gap between validation and consumption1415 // Consume inputs16 for (uint256 i = 0; i < inputItems.length; i++) {17 itemUsed[inputItems[i]] = true;18 gameNFT.burn(inputItems[i]);19 }2021 // Mint output22 gameNFT.mint(msg.sender, outputItemId);23 }24}2526// ATTACK: Submit multiple identical transactions in same block27// All pass validation before any are processed, allowing multiple crafts2829// SECURE: Atomic operations with locks30contract SecureCrafting {31 mapping(bytes32 => bool) public operationExecuted;32 mapping(address => uint256) public playerNonces;3334 function craftItem(35 uint256[] calldata inputItems,36 uint256 outputItemId,37 uint256 nonce38 ) external {39 require(nonce == playerNonces[msg.sender]++, "Invalid nonce");4041 bytes32 operationId = keccak256(abi.encode(42 msg.sender,43 inputItems,44 outputItemId,45 nonce46 ));4748 require(!operationExecuted[operationId], "Operation already executed");49 operationExecuted[operationId] = true;5051 // Atomic validation and consumption52 for (uint256 i = 0; i < inputItems.length; i++) {53 require(gameNFT.ownerOf(inputItems[i]) == msg.sender, "Not owner");54 gameNFT.burn(inputItems[i]); // Immediate consumption55 }5657 gameNFT.mint(msg.sender, outputItemId);5859 emit ItemCrafted(msg.sender, inputItems, outputItemId, operationId);60 }61}
4. Bridge/Cross-Chain Duplication
Assets duplicated across different chains or layers:
1// VULNERABLE: Cross-chain bridge without proper locking2contract VulnerableBridge {3 mapping(uint256 => bool) public bridgedTokens;45 function bridgeToL2(uint256 tokenId) external {6 require(gameNFT.ownerOf(tokenId) == msg.sender, "Not owner");7 require(!bridgedTokens[tokenId], "Already bridged");89 bridgedTokens[tokenId] = true;1011 // VULNERABILITY: Token not locked or burned on L112 // Player still has the original token on L11314 // Emit bridge event15 emit BridgeInitiated(msg.sender, tokenId);16 }17}1819// SECURE: Proper locking during bridge operations20contract SecureBridge {21 enum BridgeState { NONE, PENDING, COMPLETED, FAILED }2223 struct BridgeOperation {24 address owner;25 uint256 tokenId;26 uint256 targetChain;27 BridgeState state;28 uint256 timestamp;29 }3031 mapping(uint256 => BridgeOperation) public bridgeOperations;32 mapping(uint256 => bool) public tokenLocked;3334 function initiiateBridge(uint256 tokenId, uint256 targetChain) external {35 require(gameNFT.ownerOf(tokenId) == msg.sender, "Not owner");36 require(!tokenLocked[tokenId], "Token locked");3738 // Lock token immediately39 tokenLocked[tokenId] = true;40 gameNFT.setTransferability(tokenId, false);4142 bridgeOperations[tokenId] = BridgeOperation({43 owner: msg.sender,44 tokenId: tokenId,45 targetChain: targetChain,46 state: BridgeState.PENDING,47 timestamp: block.timestamp48 });4950 emit BridgeInitiated(msg.sender, tokenId, targetChain);51 }5253 function completeBridge(uint256 tokenId) external onlyBridgeValidator {54 BridgeOperation storage op = bridgeOperations[tokenId];55 require(op.state == BridgeState.PENDING, "Invalid state");5657 // Burn token on source chain58 gameNFT.burn(tokenId);59 tokenLocked[tokenId] = false;60 op.state = BridgeState.COMPLETED;6162 emit BridgeCompleted(tokenId);63 }6465 function failBridge(uint256 tokenId) external onlyBridgeValidator {66 BridgeOperation storage op = bridgeOperations[tokenId];67 require(op.state == BridgeState.PENDING, "Invalid state");6869 // Unlock token on failure70 tokenLocked[tokenId] = false;71 gameNFT.setTransferability(tokenId, true);72 op.state = BridgeState.FAILED;7374 emit BridgeFailed(tokenId);75 }76}
5. Upgrade-Based Duplication
Assets duplicated during contract upgrades:
1// VULNERABLE: Upgrade without proper asset migration2contract GameV1 {3 mapping(address => uint256[]) public playerAssets;45 function upgrade() external onlyOwner {6 // VULNERABILITY: Assets not properly migrated7 // Old contract still holds asset state8 // New contract can mint same assets again910 implementation = gameV2Address;11 emit Upgraded(gameV2Address);12 }13}1415// SECURE: Atomic migration during upgrade16contract GameV2 {17 mapping(address => uint256[]) public playerAssets;18 mapping(uint256 => bool) public migrated;19 bool public migrationCompleted;2021 function migrateAsset(uint256 tokenId) external {22 require(!migrationCompleted, "Migration period ended");23 require(!migrated[tokenId], "Already migrated");2425 address owner = gameV1.ownerOf(tokenId);26 require(owner != address(0), "Asset doesn't exist");2728 // Mark as migrated to prevent double migration29 migrated[tokenId] = true;3031 // Mint in new contract32 _mint(owner, tokenId);3334 // Burn in old contract35 gameV1.burnFromMigration(tokenId);3637 emit AssetMigrated(tokenId, owner);38 }3940 function completeMigration() external onlyOwner {41 require(block.timestamp > migrationDeadline, "Migration period active");42 migrationCompleted = true;4344 // Any remaining assets in old contract are now inaccessible45 emit MigrationCompleted();46 }47}
Detection and Prevention Mechanisms
1. Asset Integrity Verification
1contract AssetIntegrityChecker {2 struct AssetRecord {3 uint256 tokenId;4 address currentOwner;5 uint256 lastTransfer;6 bytes32 stateHash;7 bool verified;8 }910 mapping(uint256 => AssetRecord) public assetRecords;11 mapping(address => uint256) public playerAssetCounts;1213 function verifyAssetIntegrity(uint256 tokenId) external view returns (bool) {14 AssetRecord memory record = assetRecords[tokenId];1516 // Verify NFT exists and ownership matches17 try gameNFT.ownerOf(tokenId) returns (address actualOwner) {18 if (actualOwner != record.currentOwner) {19 return false; // Ownership mismatch20 }21 } catch {22 return false; // NFT doesn't exist23 }2425 // Verify state hash26 bytes32 currentStateHash = calculateStateHash(tokenId);27 if (currentStateHash != record.stateHash) {28 return false; // State corruption29 }3031 // Verify player asset count32 uint256 actualCount = gameNFT.balanceOf(record.currentOwner);33 if (actualCount != playerAssetCounts[record.currentOwner]) {34 return false; // Asset count mismatch35 }3637 return true;38 }3940 function auditAllAssets() external returns (uint256 corruptedCount) {41 uint256 totalSupply = gameNFT.totalSupply();4243 for (uint256 i = 1; i <= totalSupply; i++) {44 if (!verifyAssetIntegrity(i)) {45 corruptedCount++;46 emit AssetCorruption(i, "Integrity check failed");47 }48 }4950 return corruptedCount;51 }52}
2. Real-Time Duplication Detection
1contract DuplicationDetector {2 struct ActivityLog {3 address player;4 uint256 tokenId;5 string action;6 uint256 timestamp;7 bytes32 transactionHash;8 }910 ActivityLog[] public activities;11 mapping(uint256 => uint256[]) public tokenActivities;1213 function logActivity(14 address player,15 uint256 tokenId,16 string memory action17 ) external {18 uint256 activityId = activities.length;1920 activities.push(ActivityLog({21 player: player,22 tokenId: tokenId,23 action: action,24 timestamp: block.timestamp,25 transactionHash: keccak256(abi.encode(block.number, tx.origin, msg.data))26 }));2728 tokenActivities[tokenId].push(activityId);2930 // Check for suspicious patterns31 if (detectSuspiciousActivity(tokenId)) {32 flagSuspiciousToken(tokenId);33 }34 }3536 function detectSuspiciousActivity(uint256 tokenId) internal view returns (bool) {37 uint256[] memory activities = tokenActivities[tokenId];3839 if (activities.length < 2) return false;4041 // Check for rapid consecutive actions42 ActivityLog memory lastActivity = activities[activities.length - 1];43 ActivityLog memory secondLast = activities[activities.length - 2];4445 if (lastActivity.timestamp - secondLast.timestamp < 60) { // 1 minute46 if (keccak256(bytes(lastActivity.action)) == keccak256(bytes(secondLast.action))) {47 return true; // Same action twice within a minute48 }49 }5051 // Check for impossible simultaneous usage52 for (uint256 i = activities.length; i > 0; i--) {53 ActivityLog memory activity = activities[i - 1];5455 if (block.timestamp - activity.timestamp > 3600) break; // 1 hour lookback5657 if (keccak256(bytes(activity.action)) == keccak256(bytes("USE_IN_BATTLE")) ||58 keccak256(bytes(activity.action)) == keccak256(bytes("USE_IN_CRAFTING"))) {5960 // Token can't be used in multiple places simultaneously61 for (uint256 j = i + 1; j <= activities.length; j++) {62 ActivityLog memory otherActivity = activities[j - 1];6364 if (activity.timestamp == otherActivity.timestamp &&65 !compareStrings(activity.action, otherActivity.action)) {66 return true; // Simultaneous different uses67 }68 }69 }70 }7172 return false;73 }74}
3. Economic Duplication Detection
1contract EconomicAnomalyDetector {2 struct EconomicSnapshot {3 uint256 totalTokenSupply;4 uint256 totalNFTSupply;5 uint256 playerCount;6 uint256 timestamp;7 }89 EconomicSnapshot[] public snapshots;10 uint256 public constant SNAPSHOT_INTERVAL = 1 hours;1112 function takeEconomicSnapshot() external {13 require(14 snapshots.length == 0 ||15 block.timestamp > snapshots[snapshots.length - 1].timestamp + SNAPSHOT_INTERVAL,16 "Too soon for snapshot"17 );1819 snapshots.push(EconomicSnapshot({20 totalTokenSupply: gameToken.totalSupply(),21 totalNFTSupply: gameNFT.totalSupply(),22 playerCount: getActivePlayerCount(),23 timestamp: block.timestamp24 }));2526 // Analyze for anomalies27 if (snapshots.length > 1) {28 analyzeEconomicTrends();29 }30 }3132 function analyzeEconomicTrends() internal {33 if (snapshots.length < 2) return;3435 EconomicSnapshot memory current = snapshots[snapshots.length - 1];36 EconomicSnapshot memory previous = snapshots[snapshots.length - 2];3738 // Check for impossible growth rates39 uint256 tokenGrowthRate = (current.totalTokenSupply - previous.totalTokenSupply) * 100 / previous.totalTokenSupply;40 uint256 nftGrowthRate = (current.totalNFTSupply - previous.totalNFTSupply) * 100 / previous.totalNFTSupply;4142 // Flag suspicious growth43 if (tokenGrowthRate > 50) { // 50% growth in one hour44 emit EconomicAnomaly("TOKEN_GROWTH_ANOMALY", tokenGrowthRate);45 pauseTokenMinting();46 }4748 if (nftGrowthRate > 20) { // 20% NFT growth in one hour49 emit EconomicAnomaly("NFT_GROWTH_ANOMALY", nftGrowthRate);50 pauseNFTMinting();51 }5253 // Check supply/demand ratios54 if (current.totalNFTSupply > current.playerCount * 10) {55 emit EconomicAnomaly("OVERSUPPLY_DETECTED", current.totalNFTSupply / current.playerCount);56 }57 }58}
Recovery from Asset Duplication
1. Asset Snapshot and Rollback
1contract AssetRecovery {2 struct AssetSnapshot {3 uint256 tokenId;4 address owner;5 uint256 timestamp;6 bytes32 stateHash;7 }89 mapping(uint256 => AssetSnapshot[]) public assetHistory;10 uint256 public lastValidSnapshot;1112 function createAssetSnapshot() external onlyOwner {13 uint256 snapshotId = lastValidSnapshot++;14 uint256 totalSupply = gameNFT.totalSupply();1516 for (uint256 tokenId = 1; tokenId <= totalSupply; tokenId++) {17 try gameNFT.ownerOf(tokenId) returns (address owner) {18 assetHistory[tokenId].push(AssetSnapshot({19 tokenId: tokenId,20 owner: owner,21 timestamp: block.timestamp,22 stateHash: calculateAssetState(tokenId)23 }));24 } catch {25 // Token doesn't exist, skip26 continue;27 }28 }2930 emit SnapshotCreated(snapshotId, totalSupply);31 }3233 function rollbackAssets(uint256 snapshotId) external onlyOwner {34 require(snapshotId < lastValidSnapshot, "Invalid snapshot");3536 uint256 totalSupply = gameNFT.totalSupply();3738 for (uint256 tokenId = 1; tokenId <= totalSupply; tokenId++) {39 AssetSnapshot[] memory history = assetHistory[tokenId];4041 if (history.length > snapshotId) {42 AssetSnapshot memory snapshot = history[snapshotId];43 address currentOwner = gameNFT.ownerOf(tokenId);4445 if (currentOwner != snapshot.owner) {46 // Restore ownership47 gameNFT.adminTransfer(currentOwner, snapshot.owner, tokenId);48 emit AssetRestored(tokenId, currentOwner, snapshot.owner);49 }50 }51 }5253 emit AssetsRolledBack(snapshotId);54 }55}
2. Compensation Mechanisms
1contract CompensationPool {2 struct CompensationClaim {3 address player;4 uint256 tokenId;5 uint256 lossAmount;6 bool verified;7 bool paid;8 uint256 timestamp;9 }1011 mapping(uint256 => CompensationClaim) public claims;12 uint256 public claimCounter;13 uint256 public compensationPool;1415 function fileCompensationClaim(16 uint256 tokenId,17 uint256 lossAmount,18 bytes calldata proof19 ) external {20 require(verifyLossProof(msg.sender, tokenId, lossAmount, proof), "Invalid proof");2122 uint256 claimId = ++claimCounter;23 claims[claimId] = CompensationClaim({24 player: msg.sender,25 tokenId: tokenId,26 lossAmount: lossAmount,27 verified: false,28 paid: false,29 timestamp: block.timestamp30 });3132 emit CompensationClaimed(claimId, msg.sender, tokenId, lossAmount);33 }3435 function verifyAndPayCompensation(uint256 claimId) external onlyOwner {36 CompensationClaim storage claim = claims[claimId];37 require(!claim.verified, "Already verified");38 require(compensationPool >= claim.lossAmount, "Insufficient pool");3940 claim.verified = true;41 claim.paid = true;42 compensationPool -= claim.lossAmount;4344 // Pay compensation45 payable(claim.player).transfer(claim.lossAmount);4647 emit CompensationPaid(claimId, claim.player, claim.lossAmount);48 }4950 function fundCompensationPool() external payable onlyOwner {51 compensationPool += msg.value;52 emit PoolFunded(msg.value, compensationPool);53 }54}
Testing Asset Duplication Vulnerabilities
Comprehensive Duplication Test Suite
1describe("Asset Duplication Security", function() {2 it("should prevent reentrancy-based duplication", async function() {3 // Deploy malicious contract4 const attacker = await MaliciousReentrancy.deploy(gameItems.address);56 // Mint NFT to attacker7 await gameItems.mint(attacker.address, 1);89 // Attempt reentrancy attack10 await expect(11 attacker.attemptDuplication(1)12 ).to.be.revertedWith("ReentrancyGuard: reentrant call");1314 // Verify only one copy exists15 expect(await gameItems.totalSupply()).to.equal(1);16 });1718 it("should prevent race condition duplication", async function() {19 // Setup crafting scenario20 await gameItems.mint(player.address, 1);21 await gameItems.mint(player.address, 2);2223 // Attempt to submit identical crafting transactions24 const tx1 = gameCrafting.connect(player).craftItem([1, 2], 100, 0);25 const tx2 = gameCrafting.connect(player).craftItem([1, 2], 100, 0);2627 // First should succeed, second should fail28 await expect(tx1).to.not.be.reverted;29 await expect(tx2).to.be.revertedWith("Operation already executed");3031 // Verify only one output was created32 expect(await gameItems.ownerOf(100)).to.equal(player.address);33 expect(await gameItems.totalSupply()).to.equal(1); // Only output exists34 });3536 it("should detect asset state desynchronization", async function() {37 // Create state desync condition38 await gameItems.mint(player.address, 1);39 await gameLogic.useItem(1);4041 // Transfer NFT without updating game logic42 await gameItems.adminTransfer(player.address, player2.address, 1);4344 // Verification should detect the desync45 expect(await assetIntegrity.verifyAssetIntegrity(1)).to.be.false;4647 // Audit should flag the corruption48 const corruptedCount = await assetIntegrity.auditAllAssets();49 expect(corruptedCount).to.be.greaterThan(0);50 });51});
Asset Duplication Attacks represent existential threats to GameFi protocols. These vulnerabilities can instantly destroy gaming economies by inflating asset supplies, breaking game balance, and undermining player trust. Preventing asset duplication requires comprehensive security measures including reentrancy protection, atomic operations, state synchronization, real-time monitoring, and robust testing. Protocols that successfully prevent these attacks build sustainable gaming ecosystems where digital assets maintain their intended scarcity and value.
Articles Using This Term
Learn more about Asset Duplication Attack in these articles:
Related Terms
NFT State Synchronization
Ensuring consistency between NFT ownership records and game logic state across all smart contracts to prevent asset duplication and desync exploits.
Play-to-Earn Tokenomics
Economic model where players earn cryptocurrency tokens through gameplay, requiring careful balance of rewards, token sinks, and inflation control.
Randomness Manipulation Gaming
Techniques used to predict or influence random outcomes in games for unfair advantage in loot drops, rewards, and chance-based mechanics.
Need expert guidance on Asset Duplication Attack?
Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.
Get a Quote

