From 9099ae7099eba6da2eed1bfad3c878496da94f06 Mon Sep 17 00:00:00 2001 From: Sachin Anand Date: Thu, 5 Dec 2024 09:18:25 +0530 Subject: [PATCH] more unit tests added --- notes.txt | 1 + script/DeployDungeonMOW.s.sol | 2 +- src/Dungeon.sol | 95 +++++-------- src/sampleNfts/Nft1.sol | 16 +++ src/sampleNfts/Nft2.sol | 16 +++ src/sampleNfts/Potion.sol | 16 +++ src/sampleNfts/Sword.sol | 16 +++ test/DungeonTest.t.sol | 254 +++++++++++++++++++++++++++++++--- test/mocks/MockNFT.sol | 19 --- 9 files changed, 338 insertions(+), 97 deletions(-) create mode 100644 notes.txt create mode 100644 src/sampleNfts/Nft1.sol create mode 100644 src/sampleNfts/Nft2.sol create mode 100644 src/sampleNfts/Potion.sol create mode 100644 src/sampleNfts/Sword.sol delete mode 100644 test/mocks/MockNFT.sol diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..3ce0e11 --- /dev/null +++ b/notes.txt @@ -0,0 +1 @@ +Movable (sword for eg) NFT will not be in the ownership of the dungeon owner but dungeon contract only. diff --git a/script/DeployDungeonMOW.s.sol b/script/DeployDungeonMOW.s.sol index b40cc54..c67409b 100644 --- a/script/DeployDungeonMOW.s.sol +++ b/script/DeployDungeonMOW.s.sol @@ -11,4 +11,4 @@ contract DeployDungeonMOW is Script { vm.stopBroadcast(); return dungeonMOW; } -} \ No newline at end of file +} diff --git a/src/Dungeon.sol b/src/Dungeon.sol index 5ece57f..f5882dc 100644 --- a/src/Dungeon.sol +++ b/src/Dungeon.sol @@ -18,8 +18,8 @@ error IncorrectPaymentAmount(); contract DungeonMOW is ERC721URIStorage { struct LinkedAsset { address nftContract; // Address of the NFT contract - uint256 tokenId; // ID of the NFT - bool usageGranted; // Whether usage rights have been granted + uint256 tokenId; // ID of the NFT + bool usageGranted; // Whether usage rights have been granted } struct Dungeon { @@ -49,29 +49,13 @@ contract DungeonMOW is ERC721URIStorage { // Add a mapping to store the validity period set by each NFT owner for each dungeon mapping(address => mapping(uint256 => mapping(uint256 => uint256))) public nftTermsValidity; - event DungeonCreated( - uint256 indexed id, - address indexed owner, - string metadataURI - ); + event DungeonCreated(uint256 indexed id, address indexed owner, string metadataURI); - event LinkedAssetAdded( - uint256 indexed dungeonId, - address indexed nftContract, - uint256 indexed tokenId - ); + event LinkedAssetAdded(uint256 indexed dungeonId, address indexed nftContract, uint256 indexed tokenId); - event ItemImported( - address indexed nftContract, - uint256 indexed tokenId, - address indexed player - ); + event ItemImported(address indexed nftContract, uint256 indexed tokenId, address indexed player); - event ItemExported( - address indexed nftContract, - uint256 indexed tokenId, - address indexed player - ); + event ItemExported(address indexed nftContract, uint256 indexed tokenId, address indexed player); event ItemMovedBetweenDungeons( uint256 indexed fromDungeonId, @@ -92,11 +76,7 @@ contract DungeonMOW is ERC721URIStorage { _safeMint(msg.sender, tokenId); _setTokenURI(tokenId, metadataURI); - _dungeons[tokenId] = Dungeon({ - id: tokenId, - owner: msg.sender, - metadataURI: metadataURI - }); + _dungeons[tokenId] = Dungeon({id: tokenId, owner: msg.sender, metadataURI: metadataURI}); emit DungeonCreated(tokenId, msg.sender, metadataURI); } @@ -107,11 +87,7 @@ contract DungeonMOW is ERC721URIStorage { * @param nftContract Address of the NFT contract. * @param tokenId ID of the linked NFT. */ - function addLinkedAsset( - uint256 dungeonId, - address nftContract, - uint256 tokenId - ) external payable { + function addLinkedAsset(uint256 dungeonId, address nftContract, uint256 tokenId) external payable { if (ownerOf(dungeonId) != msg.sender) revert NotDungeonOwner(); IERC721 nft = IERC721(nftContract); @@ -131,11 +107,9 @@ contract DungeonMOW is ERC721URIStorage { payable(itemOwner).transfer(msg.value); // Store the linked asset in the new mapping - _dungeonLinkedAssets[dungeonId].push(LinkedAsset({ - nftContract: nftContract, - tokenId: tokenId, - usageGranted: true - })); + _dungeonLinkedAssets[dungeonId].push( + LinkedAsset({nftContract: nftContract, tokenId: tokenId, usageGranted: true}) + ); emit LinkedAssetAdded(dungeonId, nftContract, tokenId); } @@ -147,9 +121,11 @@ contract DungeonMOW is ERC721URIStorage { * @param tokenId ID of the NFT to import. */ function importItem(uint256 dungeonId, address nftContract, uint256 tokenId) public { - if (ownerOf(dungeonId) != msg.sender) revert NotDungeonOwner(); - - IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId); + // Add check to ensure caller owns the NFT + IERC721 nft = IERC721(nftContract); + if (nft.ownerOf(tokenId) != msg.sender) revert NotItemOwner(); + + nft.transferFrom(msg.sender, address(this), tokenId); dungeonTokenOwners[dungeonId][nftContract][tokenId] = msg.sender; emit ItemImported(nftContract, tokenId, msg.sender); } @@ -162,7 +138,7 @@ contract DungeonMOW is ERC721URIStorage { */ function exportItem(uint256 dungeonId, address nftContract, uint256 tokenId) public { if (dungeonTokenOwners[dungeonId][nftContract][tokenId] != msg.sender) revert NotItemOwner(); - + delete dungeonTokenOwners[dungeonId][nftContract][tokenId]; // Clear ownership before transfer IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId); emit ItemExported(nftContract, tokenId, msg.sender); @@ -184,12 +160,9 @@ contract DungeonMOW is ERC721URIStorage { * @param nftContract Address of the NFT contract. * @param tokenId ID of the NFT to move. */ - function moveItemBetweenDungeons( - uint256 fromDungeonId, - uint256 toDungeonId, - address nftContract, - uint256 tokenId - ) external { + function moveItemBetweenDungeons(uint256 fromDungeonId, uint256 toDungeonId, address nftContract, uint256 tokenId) + external + { if (dungeonTokenOwners[fromDungeonId][nftContract][tokenId] != msg.sender) revert NotItemOwner(); if (ownerOf(toDungeonId) != msg.sender) revert NotDungeonOwner(); @@ -203,12 +176,7 @@ contract DungeonMOW is ERC721URIStorage { } // Function for NFT owners to set the linking charge for a specific dungeon - function setLinkingCharge( - address nftContract, - uint256 tokenId, - uint256 dungeonId, - uint256 charge - ) external { + function setLinkingCharge(address nftContract, uint256 tokenId, uint256 dungeonId, uint256 charge) external { IERC721 nft = IERC721(nftContract); if (nft.ownerOf(tokenId) != msg.sender) revert NotItemOwner(); nftLinkingCharges[nftContract][tokenId][dungeonId] = charge; @@ -229,14 +197,19 @@ contract DungeonMOW is ERC721URIStorage { console.log("entered 227"); // Generate the terms string - string memory terms = string(abi.encodePacked( - "I, the owner of NFT at contract address ", nftContract, - ", hereby grant permission to the owner of Dungeon NFT with ID ", dungeonId, - ", to display my NFT within their dungeon. In exchange, the dungeon owner agrees to pay me ", - nftLinkingCharges[nftContract][tokenId][dungeonId], - " wei. This agreement is valid until ", validityPeriod, - " seconds from the time of signing." - )); + string memory terms = string( + abi.encodePacked( + "I, the owner of NFT at contract address ", + nftContract, + ", hereby grant permission to the owner of Dungeon NFT with ID ", + dungeonId, + ", to display my NFT within their dungeon. In exchange, the dungeon owner agrees to pay me ", + nftLinkingCharges[nftContract][tokenId][dungeonId], + " wei. This agreement is valid until ", + validityPeriod, + " seconds from the time of signing." + ) + ); // Store the hash of the terms, the timestamp, and the validity period bytes32 termsHash = keccak256(abi.encodePacked(terms)); diff --git a/src/sampleNfts/Nft1.sol b/src/sampleNfts/Nft1.sol new file mode 100644 index 0000000..59013d5 --- /dev/null +++ b/src/sampleNfts/Nft1.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; + +contract Nft1 is ERC721 { + uint256 private _nextTokenId; + + constructor() ERC721("Nft1", "NFT1") {} + + function mint() public returns (uint256) { + uint256 tokenId = _nextTokenId++; + _safeMint(msg.sender, tokenId); + return tokenId; + } +} diff --git a/src/sampleNfts/Nft2.sol b/src/sampleNfts/Nft2.sol new file mode 100644 index 0000000..dfa82d1 --- /dev/null +++ b/src/sampleNfts/Nft2.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; + +contract Nft2 is ERC721 { + uint256 private _nextTokenId; + + constructor() ERC721("Nft2", "NFT2") {} + + function mint() public returns (uint256) { + uint256 tokenId = _nextTokenId++; + _safeMint(msg.sender, tokenId); + return tokenId; + } +} diff --git a/src/sampleNfts/Potion.sol b/src/sampleNfts/Potion.sol new file mode 100644 index 0000000..8bc288e --- /dev/null +++ b/src/sampleNfts/Potion.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; + +contract Potion is ERC721 { + uint256 private _nextTokenId; + + constructor() ERC721("Potion", "PTN") {} + + function mint() public returns (uint256) { + uint256 tokenId = _nextTokenId++; + _safeMint(msg.sender, tokenId); + return tokenId; + } +} diff --git a/src/sampleNfts/Sword.sol b/src/sampleNfts/Sword.sol new file mode 100644 index 0000000..1e609f6 --- /dev/null +++ b/src/sampleNfts/Sword.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; + +contract Sword is ERC721 { + uint256 private _nextTokenId; + + constructor() ERC721("Sword", "SWRD") {} + + function mint() public returns (uint256) { + uint256 tokenId = _nextTokenId++; + _safeMint(msg.sender, tokenId); + return tokenId; + } +} diff --git a/test/DungeonTest.t.sol b/test/DungeonTest.t.sol index 5b46626..c418801 100644 --- a/test/DungeonTest.t.sol +++ b/test/DungeonTest.t.sol @@ -3,23 +3,49 @@ pragma solidity ^0.8.28; import {Test} from "../lib/forge-std/src/Test.sol"; import {DeployDungeonMOW} from "../script/DeployDungeonMOW.s.sol"; -import {DungeonMOW} from "../src/Dungeon.sol"; -import {MyMockNFT} from "../test/mocks/MockNFT.sol"; +import {DungeonMOW, NotDungeonOwner, NFTDoesNotExist, DungeonDoesNotExist, NotItemOwner, TermsNotSigned, TermsExpired, IncorrectPaymentAmount} from "../src/Dungeon.sol"; +import {Nft1} from "../src/sampleNfts/Nft1.sol"; +import {Nft2} from "../src/sampleNfts/Nft2.sol"; +import {Potion} from "../src/sampleNfts/Potion.sol"; +import {Sword} from "../src/sampleNfts/Sword.sol"; contract DungeonTest is Test { DungeonMOW dungeonMOW; - MyMockNFT mockNFT; + Nft1 nft1; + Nft2 nft2; + Sword sword; + Potion potion; + uint256 nft1Id; + uint256 nft2Id; + uint256 swordId; + uint256 potionId; address public USER = makeAddr("user"); - address public NFT_OWNER = makeAddr("nftOwner"); + address public NFT1_OWNER = makeAddr("nft1Owner"); + address public NFT2_OWNER = makeAddr("nft2Owner"); + address public SWORD_OWNER = makeAddr("swordOwner"); + address public POTION_OWNER = makeAddr("potionOwner"); uint256 public constant STARTING_USER_BALANCE = 10 ether; function setUp() public { DeployDungeonMOW deployScript = new DeployDungeonMOW(); dungeonMOW = deployScript.run(); - mockNFT = new MyMockNFT("Mock NFT", "MNFT"); - mockNFT.mint(NFT_OWNER, 1); + vm.startBroadcast(); + nft1 = new Nft1(); + nft2 = new Nft2(); + sword = new Sword(); + potion = new Potion(); + vm.stopBroadcast(); + + vm.prank(NFT1_OWNER); + nft1Id = nft1.mint(); + vm.prank(NFT2_OWNER); + nft2Id = nft2.mint(); + vm.prank(SWORD_OWNER); + swordId = sword.mint(); + vm.prank(POTION_OWNER); + potionId = potion.mint(); } function testCreateDungeon() public { @@ -33,27 +59,223 @@ contract DungeonTest is Test { } function testSetLinkingCharge() public { - uint256 tokenId = 1; uint256 dungeonId = 0; uint256 charge = 0.5 ether; - vm.prank(NFT_OWNER); - dungeonMOW.setLinkingCharge(address(mockNFT), tokenId, dungeonId, charge); + vm.prank(NFT1_OWNER); + dungeonMOW.setLinkingCharge(address(nft1), nft1Id, dungeonId, charge); - uint256 storedCharge = dungeonMOW.nftLinkingCharges(address(mockNFT), tokenId, dungeonId); + uint256 storedCharge = dungeonMOW.nftLinkingCharges(address(nft1), nft1Id, dungeonId); assertEq(storedCharge, charge); } function testSignTerms() public { - address nftContract = address(0x123); - uint256 tokenId = 1; uint256 dungeonId = 0; uint256 validityPeriod = 7200; // 2 hours - vm.prank(NFT_OWNER); - dungeonMOW.signTerms(nftContract, tokenId, dungeonId, validityPeriod); + vm.prank(NFT1_OWNER); + dungeonMOW.signTerms(address(nft1), nft1Id, dungeonId, validityPeriod); - bytes32 termsHash = dungeonMOW.nftTermsHashes(nftContract, tokenId, dungeonId); + bytes32 termsHash = dungeonMOW.nftTermsHashes(address(nft1), nft1Id, dungeonId); assert(termsHash != bytes32(0)); } -} \ No newline at end of file + + function testAddLinkedAsset() public { + // Setup + string memory metadataURI = "ipfs://example"; + uint256 dungeonId = 0; + uint256 charge = 0.5 ether; + uint256 validityPeriod = 7200; + + // Create dungeon + vm.prank(USER); + dungeonMOW.createDungeon(metadataURI); + + // Set linking charge and sign terms + vm.prank(NFT1_OWNER); + dungeonMOW.setLinkingCharge(address(nft1), nft1Id, dungeonId, charge); + + vm.prank(NFT1_OWNER); + dungeonMOW.signTerms(address(nft1), nft1Id, dungeonId, validityPeriod); + + // Add linked asset + vm.deal(USER, charge); + vm.prank(USER); + dungeonMOW.addLinkedAsset{value: charge}(dungeonId, address(nft1), nft1Id); + + // Check NFT1_OWNER received the payment + assertEq(NFT1_OWNER.balance, charge); + } + + function testImportItem() public { + // Setup + string memory metadataURI = "ipfs://example"; + uint256 dungeonId = 0; + + // Create dungeon + vm.prank(USER); + dungeonMOW.createDungeon(metadataURI); + + // Give sword to USER + vm.prank(SWORD_OWNER); + sword.transferFrom(SWORD_OWNER, USER, swordId); + + // Verify initial ownership + assertEq(sword.ownerOf(swordId), USER); + + // Approve and import sword + vm.startPrank(USER); + sword.approve(address(dungeonMOW), swordId); + dungeonMOW.importItem(dungeonId, address(sword), swordId); + vm.stopPrank(); + + // Verify: + // 1. Dungeon contract owns the NFT + // 2. SWORD_OWNER has actual ownership of the NFT + assertEq(sword.ownerOf(swordId), address(dungeonMOW)); + assertEq(dungeonMOW.dungeonTokenOwners(dungeonId, address(sword), swordId), USER); + } + + function testExportItem() public { + // Setup + string memory metadataURI = "ipfs://example"; + uint256 dungeonId = 0; + + // Create dungeon + vm.prank(USER); + dungeonMOW.createDungeon(metadataURI); + + vm.startPrank(SWORD_OWNER); + sword.approve(address(dungeonMOW), swordId); + dungeonMOW.importItem(dungeonId, address(sword), swordId); + + // Verify dungeon contract owns the NFT after import + assertEq(sword.ownerOf(swordId), address(dungeonMOW)); + + // Export sword + dungeonMOW.exportItem(dungeonId, address(sword), swordId); + vm.stopPrank(); + + // Verify: + // 1. SWORD_OWNER owns the NFT again + // 2. Management rights in dungeon are cleared + assertEq(sword.ownerOf(swordId), SWORD_OWNER); + assertEq(dungeonMOW.dungeonTokenOwners(dungeonId, address(sword), swordId), address(0)); + } + + function testFailImportItemNotOwner() public { + string memory metadataURI = "ipfs://example"; + uint256 dungeonId = 0; + + vm.prank(USER); + dungeonMOW.createDungeon(metadataURI); + + // Try to import sword without owning it + vm.startPrank(USER); + sword.approve(address(dungeonMOW), swordId); + vm.expectRevert("ERC721: transfer from incorrect owner"); + dungeonMOW.importItem(dungeonId, address(sword), swordId); + vm.stopPrank(); + } + + function testFailExportItemNotOwner() public { + string memory metadataURI = "ipfs://example"; + uint256 dungeonId = 0; + + vm.prank(USER); + dungeonMOW.createDungeon(metadataURI); + + // Import sword as USER + vm.prank(SWORD_OWNER); + sword.transferFrom(SWORD_OWNER, USER, swordId); + + vm.startPrank(USER); + sword.approve(address(dungeonMOW), swordId); + dungeonMOW.importItem(dungeonId, address(sword), swordId); + vm.stopPrank(); + + // Try to export sword as different user + vm.prank(SWORD_OWNER); + vm.expectRevert(NotItemOwner.selector); + dungeonMOW.exportItem(dungeonId, address(sword), swordId); + } + + function testFailMoveItemBetweenDungeonsNotOwner() public { + string memory metadataURI = "ipfs://example"; + uint256 dungeonId1 = 0; + uint256 dungeonId2 = 1; + + // Create dungeons + vm.startPrank(USER); + dungeonMOW.createDungeon(metadataURI); + dungeonMOW.createDungeon(metadataURI); + vm.stopPrank(); + + // Import sword to first dungeon + vm.prank(SWORD_OWNER); + sword.transferFrom(SWORD_OWNER, USER, swordId); + + vm.startPrank(USER); + sword.approve(address(dungeonMOW), swordId); + dungeonMOW.importItem(dungeonId1, address(sword), swordId); + vm.stopPrank(); + + // Try to move sword as different user + vm.prank(SWORD_OWNER); + vm.expectRevert(NotItemOwner.selector); + dungeonMOW.moveItemBetweenDungeons(dungeonId1, dungeonId2, address(sword), swordId); + } + + function testFailAddLinkedAssetIncorrectPayment() public { + string memory metadataURI = "ipfs://example"; + uint256 dungeonId = 0; + uint256 charge = 0.5 ether; + uint256 validityPeriod = 7200; + + vm.prank(USER); + dungeonMOW.createDungeon(metadataURI); + + vm.startPrank(NFT1_OWNER); + dungeonMOW.setLinkingCharge(address(nft1), nft1Id, dungeonId, charge); + dungeonMOW.signTerms(address(nft1), nft1Id, dungeonId, validityPeriod); + vm.stopPrank(); + + // Try to add linked asset with incorrect payment + vm.deal(USER, charge); + vm.prank(USER); + // vm.expectRevert(IncorrectPaymentAmount.selector); + dungeonMOW.addLinkedAsset{value: charge - 0.1 ether}(dungeonId, address(nft1), nft1Id); + } + + function testMoveItemBetweenDungeons() public { + // Setup + string memory metadataURI = "ipfs://example"; + uint256 dungeonId1 = 0; + uint256 dungeonId2 = 1; + + // Create dungeons + vm.startPrank(USER); + dungeonMOW.createDungeon(metadataURI); + dungeonMOW.createDungeon(metadataURI); + vm.stopPrank(); + + vm.startPrank(SWORD_OWNER); + sword.approve(address(dungeonMOW), swordId); + dungeonMOW.importItem(dungeonId1, address(sword), swordId); + + // Verify initial state + assertEq(sword.ownerOf(swordId), address(dungeonMOW)); + assertEq(dungeonMOW.dungeonTokenOwners(dungeonId1, address(sword), swordId), SWORD_OWNER); + + // Move sword between dungeons + dungeonMOW.moveItemBetweenDungeons(dungeonId1, dungeonId2, address(sword), swordId); + vm.stopPrank(); + + // Verify: + // 1. Dungeon contract still owns the NFT + // 2. Management rights moved to new dungeon + assertEq(sword.ownerOf(swordId), address(dungeonMOW)); + assertEq(dungeonMOW.dungeonTokenOwners(dungeonId1, address(sword), swordId), address(0)); + assertEq(dungeonMOW.dungeonTokenOwners(dungeonId2, address(sword), swordId), SWORD_OWNER); + } +} diff --git a/test/mocks/MockNFT.sol b/test/mocks/MockNFT.sol deleted file mode 100644 index df31a88..0000000 --- a/test/mocks/MockNFT.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import "../../lib/forge-std/src/mocks/MockERC721.sol"; - -contract MyMockNFT is MockERC721 { - constructor(string memory name, string memory symbol) { - initialize(name, symbol); - } - - function mint(address to, uint256 tokenId) external { - _mint(to, tokenId); - } - - function burn(uint256 tokenId) external { - require(msg.sender == ownerOf(tokenId), "NOT_OWNER"); - _burn(tokenId); - } -} \ No newline at end of file