diff --git a/foundry.toml b/foundry.toml index e375182..ba1a8e1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,5 +3,6 @@ solc = "0.8.15" bytecode_hash = "none" optimizer_runs = 1000000 fs_permissions = [{ access = "read-write", path = "./"}] +ignored_error_codes = ["license", "code-size", "unused-var"] [profile.intense.fuzz] runs = 10000 diff --git a/src/factories/CharacterSheetsFactory.sol b/src/factories/CharacterSheetsFactory.sol index 2e6fd1f..d74f0c4 100644 --- a/src/factories/CharacterSheetsFactory.sol +++ b/src/factories/CharacterSheetsFactory.sol @@ -39,6 +39,7 @@ contract CharacterSheetsFactory is OwnableUpgradeable { function create( address[] calldata dungeonMasters, address dao, + address default_admin, string calldata experienceBaseuri, string calldata characterSheetsBaseUri ) external returns (address, address) { @@ -51,9 +52,9 @@ contract CharacterSheetsFactory is OwnableUpgradeable { address experienceClone = ClonesUpgradeable.cloneDeterministic(experienceAndItemsImplementation, 0); bytes memory encodedCharacterSheetParameters = - abi.encode(dao, dungeonMasters, experienceClone, characterSheetsBaseUri); + abi.encode(dao, dungeonMasters, default_admin, experienceClone, characterSheetsBaseUri); bytes memory encodedExperienceParameters = - abi.encode(dao, dungeonMasters, characterSheetsClone, hatsAddress, experienceBaseuri); + abi.encode(dao, default_admin, characterSheetsClone, hatsAddress, experienceBaseuri); CharacterSheetsImplementation(characterSheetsClone).initialize(encodedCharacterSheetParameters); ExperienceAndItemsImplementation(experienceClone).initialize(encodedExperienceParameters); diff --git a/src/implementations/CharacterSheetsImplementation.sol b/src/implementations/CharacterSheetsImplementation.sol index c1f3a42..34a5311 100644 --- a/src/implementations/CharacterSheetsImplementation.sol +++ b/src/implementations/CharacterSheetsImplementation.sol @@ -69,17 +69,17 @@ contract CharacterSheetsImplementation is Initializable, IMolochDAO, ERC721, ERC address daoAddress; address[] memory dungeonMasters; + address owner; string memory baseUri; address experienceImplementation; - (daoAddress, dungeonMasters, experienceImplementation, baseUri) = - abi.decode(_encodedParameters, (address, address[], address, string)); + (daoAddress, dungeonMasters, owner, experienceImplementation, baseUri) = + abi.decode(_encodedParameters, (address, address[], address, address, string)); for (uint256 i = 0; i < dungeonMasters.length; i++) { _grantRole(DUNGEON_MASTER, dungeonMasters[i]); - _grantRole(DEFAULT_ADMIN_ROLE, dungeonMasters[i]); } - + _grantRole(DEFAULT_ADMIN_ROLE, owner); setBaseUri(baseUri); _experience = ExperienceAndItemsImplementation(experienceImplementation); _dao = IMolochDAO(daoAddress); @@ -140,20 +140,53 @@ contract CharacterSheetsImplementation is Initializable, IMolochDAO, ERC721, ERC players[playerId].classes.push(classTokenId); emit classAdded(playerId, classTokenId); } - - function removeClassFromPlayer(uint256 playerId, uint256 classTokenId) external onlyExpContract returns(bool success){ + /** + * removes a class from a character Sheet + * @param playerId the id of the character sheet to be modified + * @param classId the class Id to be removed + */ + function removeClassFromPlayer(uint256 playerId, uint256 classId) external onlyExpContract returns (bool success) { uint256[] memory arr = players[playerId].classes; for (uint256 i = 0; i < arr.length; i++) { - if (arr[i] == classTokenId) { - for (uint256 j = i; j < arr.length - 1; j++) { - arr[j] = arr[j + 1]; + if (arr[i] == classId) { + for (uint256 j = i; j < arr.length; j++) { + if (j + 1 < arr.length) { + arr[j] = arr[j + 1]; + } else if (j + 1 >= arr.length) { + arr[j] = 0; + } } - arr[arr.length - 1] = 0; players[playerId].classes = arr; - success = true; + players[playerId].classes.pop(); + + return success = true; + } + } + return success = false; + } + /** + * removes an itemtype from a character sheet inventory + * @param playerId the player to have the item type from their inventory + * @param itemId the itemId of the item to be removed + */ + function removeitemFromPlayer(uint256 playerId, uint256 itemId) external onlyExpContract returns (bool success) { + uint256[] memory arr = players[playerId].items; + for (uint256 i = 0; i < arr.length; i++) { + if (arr[i] == itemId) { + for (uint256 j = i; j < arr.length; j++) { + if (j + 1 < arr.length) { + arr[j] = arr[j + 1]; + } else if (j + 1 >= arr.length) { + arr[j] = 0; + } + } + players[playerId].items = arr; + players[playerId].items.pop(); + + return success = true; } } - success = false; + return success = false; } function addItemToPlayer(uint256 playerId, uint256 itemTokenId) external onlyExpContract { @@ -175,11 +208,11 @@ contract CharacterSheetsImplementation is Initializable, IMolochDAO, ERC721, ERC revert("This is not the address of an npc"); } - function getClassIndex(uint256 playerId, uint256 classId)public view returns(uint256 indexOfClass){ + function getClassIndex(uint256 playerId, uint256 classId) public view returns (uint256 indexOfClass) { CharacterSheet memory sheet = players[playerId]; uint256 length = sheet.classes.length; - for(uint256 i =0; i 0, "This class does not exist."); require(balanceOf(player.ERC6551TokenAddress, newClass.tokenId) == 0, "Can only assign a class once."); _mint(player.ERC6551TokenAddress, newClass.tokenId, 1, bytes(newClass.cid)); - characterSheets.addClassToPlayer(playerId, newClass.tokenId); + characterSheets.addClassToPlayer(playerId, newClass.classId); classes[classId].supply++; - emit classAssigned(player.ERC6551TokenAddress, classId); + emit classAssigned(player.ERC6551TokenAddress, classId, newClass.tokenId); } function assignClasses(uint256 playerId, uint256[] calldata _classIds) external onlyDungeonMaster { for (uint256 i = 0; i < _classIds.length; i++) { - console2.log("CLASS IDS: ", _classIds[i], i); assignClass(playerId, _classIds[i]); } } @@ -292,11 +293,11 @@ contract ExperienceAndItemsImplementation is ERC1155Holder, Initializable, ERC11 CharacterSheet memory sheet = characterSheets.getCharacterSheetByPlayerId(playerId); uint256 tokenId = classes[classId].tokenId; if(characterSheets.hasRole(DUNGEON_MASTER, msg.sender)){ - require(characterSheets.removeClassFromPlayer(playerId, tokenId), "Player does not have that class"); + require(characterSheets.removeClassFromPlayer(playerId, classId), "Player does not have that class"); _burn(sheet.ERC6551TokenAddress, tokenId, 1); } else { require(sheet.memberAddress == msg.sender, "Must be the player to remove a class"); - require(characterSheets.removeClassFromPlayer(playerId, tokenId), "You do not have that class"); + require(characterSheets.removeClassFromPlayer(playerId, classId), "You do not have that class"); _burn(sheet.ERC6551TokenAddress, tokenId, 1); } success = true; diff --git a/test/ExperienceAndItems.t.sol b/test/ExperienceAndItems.t.sol index e1e013a..cad74ce 100644 --- a/test/ExperienceAndItems.t.sol +++ b/test/ExperienceAndItems.t.sol @@ -12,12 +12,13 @@ contract ExperienceAndItemsTest is Test, SetUp { function testCreateClass() public { vm.prank(admin); (uint256 _tokenId, uint256 _classId) = experience.createClassType(createNewClass("Ballerina")); - (uint256 tokenId, string memory name, uint256 supply, string memory cid) = experience.classes(_classId); + (uint256 tokenId, uint256 classId, string memory name, uint256 supply, string memory cid) = experience.classes(_classId); assertEq(experience.totalClasses(), 2); assertEq(tokenId, 3); assertEq(_tokenId, 3); assertEq(_classId, 2); + assertEq(classId, 2); assertEq(keccak256(abi.encode(name)), keccak256(abi.encode("Ballerina"))); assertEq(supply, 0); assertEq(keccak256(abi.encode(cid)), keccak256(abi.encode("test_class_cid/"))); @@ -37,7 +38,7 @@ contract ExperienceAndItemsTest is Test, SetUp { assertEq(experience.balanceOf(player.ERC6551TokenAddress, tokenId), 1); assertEq(player.classes.length, 1); - assertEq(player.classes[0], tokenId); + assertEq(player.classes[0], classId); //add second class vm.prank(admin); @@ -47,7 +48,7 @@ contract ExperienceAndItemsTest is Test, SetUp { assertEq(experience.balanceOf(secondPlayer.ERC6551TokenAddress, 2), 1, "does not own second class"); assertEq(secondPlayer.classes.length, 2, "not enough classes"); - assertEq(secondPlayer.classes[1], 2, 'second class not in player classes array'); + assertEq(secondPlayer.classes[1], 1, 'second class not in player classes array'); } function testAssignClasses()public { @@ -58,33 +59,40 @@ contract ExperienceAndItemsTest is Test, SetUp { Class[] memory allClasses = experience.getAllClasses(); uint256[] memory classes = new uint256[](2); - classes[0] = 1; - classes[1] = 2; + classes[0] = allClasses[0].classId; + classes[1] = allClasses[1].classId; experience.assignClasses(playerId, classes); vm.stopPrank(); CharacterSheet memory player = characterSheets.getCharacterSheetByPlayerId(playerId); assertEq(player.classes.length, 2, "not enough classes assigned"); - assertEq(player.classes[0], classId); - assertEq(player.classes[1], 1); + assertEq(player.classes[0], allClasses[0].classId, "wrong classId"); + assertEq(player.classes[1], allClasses[1].classId, "wrong classid 2"); } - // function testRemoveClass() public { - // uint256 playerId = characterSheets.memberAddressToTokenId(player1); + function testRevokeClass() public { + uint256 playerId = characterSheets.memberAddressToTokenId(player1); - // vm.startPrank(admin); - // (uint256 tokenId, uint256 classId) = experience.createClassType(createNewClass("Ballerina")); + vm.startPrank(admin); + (uint256 tokenId, uint256 classId) = experience.createClassType(createNewClass("Ballerina")); - // experience.assignClass(playerId, classId); - // vm.stopPrank(); + Class[] memory allClasses = experience.getAllClasses(); - // vm.prank(player1); - // experience.revokeClass(playerId, classId); + uint256[] memory classes = new uint256[](2); + classes[0] = allClasses[0].classId; + classes[1] = allClasses[1].classId; - // CharacterSheet memory sheet = characterSheets.getCharacterSheetByPlayerId(playerId); + experience.assignClasses(playerId, classes); + vm.stopPrank(); + + vm.prank(player1); + experience.revokeClass(playerId, allClasses[0].classId); + + CharacterSheet memory sheet = characterSheets.getCharacterSheetByPlayerId(playerId); - // assertEq(experience.balanceOf(sheet.ERC6551TokenAddress, tokenId), 0); - // assertEq(sheet.classes.length, 0); - // } + assertEq(experience.balanceOf(sheet.ERC6551TokenAddress, tokenId), 1, "Incorrect class balance"); + assertEq(sheet.classes.length, 1, "classes array wrong length."); + assertEq(sheet.classes[0], allClasses[1].classId, "wrong remaining id"); + } function testCreateItemType() public { Item memory newItem = createNewItem("Pirate_Hat", false, bytes32(0)); @@ -93,6 +101,7 @@ contract ExperienceAndItemsTest is Test, SetUp { ( uint256 tokenId, + uint256 itemId, string memory name, uint256 supply, uint256 supplied, diff --git a/test/helpers/SetUp.sol b/test/helpers/SetUp.sol index f239768..f49a885 100644 --- a/test/helpers/SetUp.sol +++ b/test/helpers/SetUp.sol @@ -16,6 +16,7 @@ import {ERC6551Registry} from "../../src/mocks/ERC6551Registry.sol"; import {SimpleERC6551Account} from "../../src/mocks/ERC6551Implementation.sol"; contract SetUp is Test { + ExperienceAndItemsImplementation experienceAndItemsImplementation; ExperienceAndItemsImplementation experience; CharacterSheetsFactory characterSheetsFactory; @@ -45,7 +46,7 @@ contract SetUp is Test { function setUp() public { - Item memory newItem = createNewItem("test_item", false, bytes32(0)); + vm.startPrank(admin); dao = new Moloch(); @@ -70,8 +71,10 @@ contract SetUp is Test { characterSheetsFactory.updateHats(address(hats)); address[] memory dungeonMasters = new address[](1); dungeonMasters[0] = admin; - (characterSheetsAddress, experienceAddress) = characterSheetsFactory.create(dungeonMasters, address(dao), 'test_base_uri_experience/', 'test_base_uri_character_sheets/'); + (characterSheetsAddress, experienceAddress) = characterSheetsFactory.create(dungeonMasters, address(dao), admin,'test_base_uri_experience/', 'test_base_uri_character_sheets/'); characterSheets = CharacterSheetsImplementation(characterSheetsAddress); + experience = ExperienceAndItemsImplementation(experienceAddress); + characterSheets.setERC6551Registry(address(erc6551Registry)); characterSheets.setERC6551Implementation(address(erc6551Implementation)); @@ -84,18 +87,18 @@ contract SetUp is Test { erc6551Registry = new ERC6551Registry(); erc6551Implementation = new SimpleERC6551Account(); - experience = ExperienceAndItemsImplementation(experienceAddress); - experience.createItemType(newItem); + + experience.createItemType(createNewItem("test_item", false, bytes32(0))); experience.createClassType(createNewClass('test_class')); vm.stopPrank(); } function createNewItem(string memory _name, bool _soulbound, bytes32 _claimable)public pure returns(Item memory){ - return Item({tokenId: 0, name: _name, supply: 10**18, supplied: 0, experienceCost: 100, hatId: 0, soulbound: _soulbound, claimable: _claimable, cid: 'test_item_cid/'}); + return Item({tokenId: 0, itemId: 0, name: _name, supply: 10**18, supplied: 0, experienceCost: 100, hatId: 0, soulbound: _soulbound, claimable: _claimable, cid: 'test_item_cid/'}); } function createNewClass(string memory _name)public pure returns(Class memory){ - return Class({tokenId: 0, name: _name, supply: 0, cid: 'test_class_cid/'}); + return Class({tokenId: 0, classId: 0, name: _name, supply: 0, cid: 'test_class_cid/'}); } function dropExp(address player, uint256 amount)public{ @@ -112,7 +115,6 @@ contract SetUp is Test { function generateMerkleRootAndProof(uint256[] memory itemIds, address[] memory claimers, uint256[] memory amounts, uint256 indexOfProof) public view returns(bytes32[] memory proof, bytes32 root) { bytes32[] memory leaves = new bytes32[](itemIds.length); for(uint256 i =0; i< itemIds.length; i++){ - leaves[i] = keccak256(bytes.concat(keccak256(abi.encodePacked(itemIds[i], claimers[i], amounts[i])))); } proof = merkle.getProof(leaves, indexOfProof);