From 509ba7f68aa2844cbf943c15c6917f11cd5804f1 Mon Sep 17 00:00:00 2001 From: Sachin Anand Date: Wed, 4 Dec 2024 23:49:58 +0530 Subject: [PATCH] init --- .gitignore | 1 + .gitmodules | 3 + lib/openzeppelin-contracts | 1 + script/Counter.s.sol | 19 --- script/DeployDungeonMOW.s.sol | 14 ++ src/Counter.sol | 14 -- src/Dungeon.sol | 250 ++++++++++++++++++++++++++++++++++ test/Counter.t.sol | 24 ---- test/DungeonTest.t.sol | 59 ++++++++ test/mocks/MockNFT.sol | 19 +++ 10 files changed, 347 insertions(+), 57 deletions(-) create mode 160000 lib/openzeppelin-contracts delete mode 100644 script/Counter.s.sol create mode 100644 script/DeployDungeonMOW.s.sol delete mode 100644 src/Counter.sol create mode 100644 src/Dungeon.sol delete mode 100644 test/Counter.t.sol create mode 100644 test/DungeonTest.t.sol create mode 100644 test/mocks/MockNFT.sol diff --git a/.gitignore b/.gitignore index 85198aa..c4e5e00 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ docs/ # Dotenv file .env +.broadcast diff --git a/.gitmodules b/.gitmodules index 888d42d..690924b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..69c8def --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/script/DeployDungeonMOW.s.sol b/script/DeployDungeonMOW.s.sol new file mode 100644 index 0000000..b40cc54 --- /dev/null +++ b/script/DeployDungeonMOW.s.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {DungeonMOW} from "../src/Dungeon.sol"; + +contract DeployDungeonMOW is Script { + function run() external returns (DungeonMOW) { + vm.startBroadcast(); + DungeonMOW dungeonMOW = new DungeonMOW(); + vm.stopBroadcast(); + return dungeonMOW; + } +} \ No newline at end of file diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/Dungeon.sol b/src/Dungeon.sol new file mode 100644 index 0000000..5ece57f --- /dev/null +++ b/src/Dungeon.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IERC721} from "../lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; +import {ERC721URIStorage} from "../lib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import {ERC721} from "../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {console} from "../lib/forge-std/src/console.sol"; + +// At the top of the contract, add custom errors +error NotDungeonOwner(); +error NFTDoesNotExist(); +error DungeonDoesNotExist(); +error NotItemOwner(); +error TermsNotSigned(); +error TermsExpired(); +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 + } + + struct Dungeon { + uint256 id; + address owner; + string metadataURI; // Points to metadata storage + } + + uint256 private _nextTokenId; + mapping(uint256 => Dungeon) private _dungeons; + + // New mapping to store linked assets for each dungeon + mapping(uint256 => LinkedAsset[]) private _dungeonLinkedAssets; + + // Mapping to track imported movable NFTs for each dungeon + mapping(uint256 => mapping(address => mapping(uint256 => address))) public dungeonTokenOwners; + + // Add a mapping to store the linking charge set by each NFT owner for each dungeon + mapping(address => mapping(uint256 => mapping(uint256 => uint256))) public nftLinkingCharges; + + // Add a mapping to store signed terms by NFT owners for each dungeon + mapping(address => mapping(uint256 => mapping(uint256 => bytes32))) public nftTermsHashes; + + // Add a mapping to store the timestamp when terms were signed + mapping(address => mapping(uint256 => mapping(uint256 => uint256))) public nftTermsTimestamps; + + // 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 LinkedAssetAdded( + uint256 indexed dungeonId, + address indexed nftContract, + uint256 indexed tokenId + ); + + event ItemImported( + 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, + uint256 indexed toDungeonId, + address indexed nftContract, + uint256 tokenId, + address player + ); + + constructor() ERC721("DungeonMOW", "DGM") {} + + /** + * @dev Create a new dungeon MOW. + * @param metadataURI URI pointing to metadata (e.g., IPFS/Arweave). + */ + function createDungeon(string memory metadataURI) external { + uint256 tokenId = _nextTokenId++; + _safeMint(msg.sender, tokenId); + _setTokenURI(tokenId, metadataURI); + + _dungeons[tokenId] = Dungeon({ + id: tokenId, + owner: msg.sender, + metadataURI: metadataURI + }); + + emit DungeonCreated(tokenId, msg.sender, metadataURI); + } + + /** + * @dev Add a fixed linked NFT asset to the dungeon map. + * @param dungeonId ID of the dungeon map. + * @param nftContract Address of the NFT contract. + * @param tokenId ID of the linked NFT. + */ + function addLinkedAsset( + uint256 dungeonId, + address nftContract, + uint256 tokenId + ) external payable { + if (ownerOf(dungeonId) != msg.sender) revert NotDungeonOwner(); + + IERC721 nft = IERC721(nftContract); + address itemOwner = nft.ownerOf(tokenId); + if (itemOwner == address(0)) revert NFTDoesNotExist(); + + // Check if terms are signed and valid + bytes32 storedHash = nftTermsHashes[nftContract][tokenId][dungeonId]; + uint256 signedTimestamp = nftTermsTimestamps[nftContract][tokenId][dungeonId]; + uint256 validityPeriod = nftTermsValidity[nftContract][tokenId][dungeonId]; + if (storedHash == bytes32(0)) revert TermsNotSigned(); + if (block.timestamp > signedTimestamp + validityPeriod) revert TermsExpired(); + + // Ensure payment matches the charge set by the NFT owner + uint256 charge = nftLinkingCharges[nftContract][tokenId][dungeonId]; + if (msg.value != charge) revert IncorrectPaymentAmount(); + payable(itemOwner).transfer(msg.value); + + // Store the linked asset in the new mapping + _dungeonLinkedAssets[dungeonId].push(LinkedAsset({ + nftContract: nftContract, + tokenId: tokenId, + usageGranted: true + })); + + emit LinkedAssetAdded(dungeonId, nftContract, tokenId); + } + + /** + * @dev Import a movable NFT to a specific dungeon. + * @param dungeonId ID of the dungeon map. + * @param nftContract Address of the NFT contract. + * @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); + dungeonTokenOwners[dungeonId][nftContract][tokenId] = msg.sender; + emit ItemImported(nftContract, tokenId, msg.sender); + } + + /** + * @dev Export a movable NFT from a specific dungeon. + * @param dungeonId ID of the dungeon map. + * @param nftContract Address of the NFT contract. + * @param tokenId ID of the NFT to export. + */ + 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); + } + + /** + * @dev Get Dungeon details. + * @param dungeonId ID of the dungeon map. + */ + function getDungeon(uint256 dungeonId) external view returns (Dungeon memory) { + if (ownerOf(dungeonId) == address(0)) revert DungeonDoesNotExist(); + return _dungeons[dungeonId]; + } + + /** + * @dev Move a movable NFT from one dungeon to another. + * @param fromDungeonId ID of the source dungeon. + * @param toDungeonId ID of the destination dungeon. + * @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 { + if (dungeonTokenOwners[fromDungeonId][nftContract][tokenId] != msg.sender) revert NotItemOwner(); + if (ownerOf(toDungeonId) != msg.sender) revert NotDungeonOwner(); + + // Remove the NFT from the source dungeon + delete dungeonTokenOwners[fromDungeonId][nftContract][tokenId]; + + // Add the NFT to the destination dungeon + dungeonTokenOwners[toDungeonId][nftContract][tokenId] = msg.sender; + + emit ItemMovedBetweenDungeons(fromDungeonId, toDungeonId, nftContract, tokenId, msg.sender); + } + + // Function for NFT owners to set the linking charge for a specific dungeon + 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; + } + + // Function for NFT owners to sign terms for a specific dungeon with a custom validity period + function signTerms( + address nftContract, + uint256 tokenId, + uint256 dungeonId, + uint256 validityPeriod // in seconds + ) external { + console.log("entered"); + console.log("dhgfhg"); + IERC721 nft = IERC721(nftContract); + console.log(nft.ownerOf(tokenId)); + if (nft.ownerOf(tokenId) != msg.sender) revert NotItemOwner(); + 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." + )); + + // Store the hash of the terms, the timestamp, and the validity period + bytes32 termsHash = keccak256(abi.encodePacked(terms)); + nftTermsHashes[nftContract][tokenId][dungeonId] = termsHash; + nftTermsTimestamps[nftContract][tokenId][dungeonId] = block.timestamp; + nftTermsValidity[nftContract][tokenId][dungeonId] = validityPeriod; + + // Console logs + console.log("Terms signed for NFT contract: %s", nftContract); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/DungeonTest.t.sol b/test/DungeonTest.t.sol new file mode 100644 index 0000000..5b46626 --- /dev/null +++ b/test/DungeonTest.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +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"; + +contract DungeonTest is Test { + DungeonMOW dungeonMOW; + MyMockNFT mockNFT; + + address public USER = makeAddr("user"); + address public NFT_OWNER = makeAddr("nftOwner"); + 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); + } + + function testCreateDungeon() public { + string memory metadataURI = "ipfs://example"; + vm.prank(USER); + dungeonMOW.createDungeon(metadataURI); + + DungeonMOW.Dungeon memory dungeon = dungeonMOW.getDungeon(0); + assertEq(dungeon.owner, USER); + assertEq(dungeon.metadataURI, metadataURI); + } + + 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); + + uint256 storedCharge = dungeonMOW.nftLinkingCharges(address(mockNFT), tokenId, 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); + + bytes32 termsHash = dungeonMOW.nftTermsHashes(nftContract, tokenId, dungeonId); + assert(termsHash != bytes32(0)); + } +} \ No newline at end of file diff --git a/test/mocks/MockNFT.sol b/test/mocks/MockNFT.sol new file mode 100644 index 0000000..df31a88 --- /dev/null +++ b/test/mocks/MockNFT.sol @@ -0,0 +1,19 @@ +// 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