From 62a1d3c6977fd7b4f230f1709dbf212c9636764f Mon Sep 17 00:00:00 2001 From: dan13ram Date: Mon, 1 Apr 2024 10:00:41 +0530 Subject: [PATCH] fixed contracts --- .gas-snapshot | 90 +- addresses.json | 29 +- .../CharacterSheetsImplementation.sol | 1014 +++++++++-------- test/CharacterSheet.t.sol | 6 +- 4 files changed, 604 insertions(+), 535 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 101457c..70f1614 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,8 +1,8 @@ -CharacterAccountTest:testEquipAndUnequipViaMultiSendDelegateCall() (gas: 863322) -CharacterAccountTest:testEquipItemToCharacter() (gas: 876054) -CharacterAccountTest:testEquipViaMultiSendDelegateCall() (gas: 883491) -CharacterAccountTest:testUnequipItemToCharacter() (gas: 870850) -CharacterAccountTest:test_Owner() (gas: 591503) +CharacterAccountTest:testEquipAndUnequipViaMultiSendDelegateCall() (gas: 863855) +CharacterAccountTest:testEquipItemToCharacter() (gas: 876542) +CharacterAccountTest:testEquipViaMultiSendDelegateCall() (gas: 883979) +CharacterAccountTest:testUnequipItemToCharacter() (gas: 871537) +CharacterAccountTest:test_Owner() (gas: 591747) CharacterEligibilityAdaptorTest:testIsEligible() (gas: 53561) CharacterEligibilityAdaptorTest:testSupportsInterface() (gas: 13427) CharacterSheetsFactoryTest:testCreateAndInitialize() (gas: 3886107) @@ -19,76 +19,76 @@ CharacterSheetsFactoryTest:testInitializeContracts() (gas: 3839130) CharacterSheetsFactoryTest:testUpdateImplementationAddressStorage() (gas: 21541) CharacterSheetsTest:testChangeBaseUri() (gas: 71664) CharacterSheetsTest:testChangeBaseUriRevertNotAdmin() (gas: 71564) -CharacterSheetsTest:testEquipItemToCharacter() (gas: 331724) -CharacterSheetsTest:testEquipItemToCharacterReverts() (gas: 220816) -CharacterSheetsTest:testGetCharacterSheetByCharacterId() (gas: 23243) -CharacterSheetsTest:testGetPlayerIdFromAccountAddress() (gas: 28008) -CharacterSheetsTest:testRemovePlayer() (gas: 948542) +CharacterSheetsTest:testEquipItemToCharacter() (gas: 333968) +CharacterSheetsTest:testEquipItemToCharacterReverts() (gas: 218588) +CharacterSheetsTest:testGetCharacterSheetByCharacterId() (gas: 25397) +CharacterSheetsTest:testGetPlayerIdFromAccountAddress() (gas: 30207) +CharacterSheetsTest:testRemovePlayer() (gas: 948632) CharacterSheetsTest:testRenounceSheet() (gas: 73170) CharacterSheetsTest:testRenounceSheetReverts() (gas: 72911) -CharacterSheetsTest:testRestoreSheetAfterRemove() (gas: 788485) -CharacterSheetsTest:testRestoreSheetAfterRenounce() (gas: 745299) -CharacterSheetsTest:testRollCharacterSheet() (gas: 590052) +CharacterSheetsTest:testRestoreSheetAfterRemove() (gas: 788729) +CharacterSheetsTest:testRestoreSheetAfterRenounce() (gas: 745588) +CharacterSheetsTest:testRollCharacterSheet() (gas: 590142) CharacterSheetsTest:testRollCharacterSheetFailNonMember() (gas: 61385) CharacterSheetsTest:testRollCharacterSheetRevertAlreadyACharacter() (gas: 68037) CharacterSheetsTest:testRollFailsForRenouncedSheet() (gas: 116739) -CharacterSheetsTest:testSafeTransferFrom() (gas: 466677) -CharacterSheetsTest:testSafeTransferFromBackAndForth() (gas: 268314) -CharacterSheetsTest:testTransferFrom() (gas: 477649) -CharacterSheetsTest:testUnequipItemFromCharacter() (gas: 314874) +CharacterSheetsTest:testSafeTransferFrom() (gas: 469091) +CharacterSheetsTest:testSafeTransferFromBackAndForth() (gas: 268387) +CharacterSheetsTest:testTransferFrom() (gas: 480040) +CharacterSheetsTest:testUnequipItemFromCharacter() (gas: 317317) CharacterSheetsTest:testUpdateCharacterMetadata() (gas: 98927) -CharacterSheetsTest:testUpdateContractImplementation() (gas: 5129500) -ClassLevelAdaptorTest:testFuzz_GetCurrentLevel(uint256) (runs: 256, μ: 37180, ~: 32870) +CharacterSheetsTest:testUpdateContractImplementation() (gas: 5165408) +ClassLevelAdaptorTest:testFuzz_GetCurrentLevel(uint256) (runs: 256, μ: 37057, ~: 33214) ClassLevelAdaptorTest:testSupportsInterface() (gas: 13427) ClassesTest:testAssignClass() (gas: 281864) -ClassesTest:testClaimClass() (gas: 156790) -ClassesTest:testClassExp() (gas: 256849) +ClassesTest:testClaimClass() (gas: 156835) +ClassesTest:testClassExp() (gas: 256984) ClassesTest:testCreateClass() (gas: 134641) -ClassesTest:testFuzz_BalanceOf(uint256) (runs: 256, μ: 297234, ~: 306336) -ClassesTest:testRenounceClass() (gas: 240909) +ClassesTest:testFuzz_BalanceOf(uint256) (runs: 256, μ: 297203, ~: 306426) +ClassesTest:testRenounceClass() (gas: 240954) ClassesTest:testRevokeClass() (gas: 213123) -ClassesTest:testTransferClass() (gas: 762109) -ExperienceTest:testBurnExp() (gas: 195449) -ExperienceTest:testDropExp() (gas: 251928) +ClassesTest:testTransferClass() (gas: 762398) +ExperienceTest:testBurnExp() (gas: 195494) +ExperienceTest:testDropExp() (gas: 252018) ExperienceTest:testExperienceDeployment() (gas: 18173) HatsAdaptorTest:testAddGameMaster() (gas: 147549) HatsAdaptorTest:testHatsAdaptorDeployment() (gas: 116819) -HatsAdaptorTest:testIsCharacter() (gas: 83761) +HatsAdaptorTest:testIsCharacter() (gas: 87252) HatsAdaptorTest:testIsGameMaster() (gas: 63906) HatsAdaptorTest:testIsPlayer() (gas: 86846) -HatsAdaptorTest:testMintCharacterHat() (gas: 213240) -HatsAdaptorTest:testMintPlayerHat() (gas: 698228) -HatsAdaptorTest:test_CheckCharacterHatEligibility() (gas: 93093) +HatsAdaptorTest:testMintCharacterHat() (gas: 213375) +HatsAdaptorTest:testMintPlayerHat() (gas: 698318) +HatsAdaptorTest:test_CheckCharacterHatEligibility() (gas: 93138) HatsAdaptorTest:test_addValidGame() (gas: 211) -ItemsTest:testClaimItem() (gas: 877456) -ItemsTest:testClaimItemRevert() (gas: 1270449) -ItemsTest:testComplexRequirementsClaimRevert() (gas: 1257400) -ItemsTest:testComplexRequirementsClaimRevertWithTooMuchExp() (gas: 1286928) -ItemsTest:testComplexRequirementsClaimWithItem1() (gas: 1333957) -ItemsTest:testComplexRequirementsClaimWithItem1ForShallowNot() (gas: 1288021) -ItemsTest:testComplexRequirementsClaimWithItem2() (gas: 1338831) -ItemsTest:testCraftItem() (gas: 693200) -ItemsTest:testCraftItemRevert() (gas: 434002) +ItemsTest:testClaimItem() (gas: 877546) +ItemsTest:testClaimItemRevert() (gas: 1270719) +ItemsTest:testComplexRequirementsClaimRevert() (gas: 1257535) +ItemsTest:testComplexRequirementsClaimRevertWithTooMuchExp() (gas: 1287018) +ItemsTest:testComplexRequirementsClaimWithItem1() (gas: 1334047) +ItemsTest:testComplexRequirementsClaimWithItem1ForShallowNot() (gas: 1288111) +ItemsTest:testComplexRequirementsClaimWithItem2() (gas: 1338921) +ItemsTest:testCraftItem() (gas: 694968) +ItemsTest:testCraftItemRevert() (gas: 434092) ItemsTest:testCreateClaimableItem() (gas: 77741) ItemsTest:testCreateCraftableItem() (gas: 69511) ItemsTest:testCreateItemTypeRevert() (gas: 63965) ItemsTest:testDeleteItem() (gas: 240249) -ItemsTest:testDismantleItems() (gas: 1158284) -ItemsTest:testDismantleItemsRevert() (gas: 1216279) -ItemsTest:testDropLoot() (gas: 1247910) +ItemsTest:testDismantleItems() (gas: 1160135) +ItemsTest:testDismantleItemsRevert() (gas: 1218085) +ItemsTest:testDropLoot() (gas: 1248244) ItemsTest:testDropLootRevert() (gas: 258077) ItemsTest:testInvalidTreeAnd() (gas: 633183) ItemsTest:testInvalidTreeNot() (gas: 850493) ItemsTest:testInvalidTreeOr() (gas: 867837) -ItemsTest:testSimpleRequirementsClaimWithItem1() (gas: 628285) +ItemsTest:testSimpleRequirementsClaimWithItem1() (gas: 628330) ItemsTest:testURI() (gas: 20934) ItemsTest:testUpdateClaimableItemRequirements() (gas: 338153) ItemsTest:testUpdateCraftableItemRequirements() (gas: 196725) Test_AdminEligibilityModule:testAddNewAdmin() (gas: 165759) Test_AdminEligibilityModule:testRemoveAdmin() (gas: 154477) Test_ElderEligibilityModule:testAddClassToElderModule() (gas: 465413) -Test_ElderEligibilityModule:testCharacterSheetsLevelEligibilityModule() (gas: 659828) +Test_ElderEligibilityModule:testCharacterSheetsLevelEligibilityModule() (gas: 659711) Test_ElderEligibilityModule:testElderModuleNoCharacter() (gas: 471970) Test_GameMasterEligibilityModule:testAddNewGameMaster() (gas: 206808) -Test_GameMasterEligibilityModule:testNewModuleSetup() (gas: 135594) +Test_GameMasterEligibilityModule:testNewModuleSetup() (gas: 135639) Test_GameMasterEligibilityModule:testRemoveGameMaster() (gas: 192932) \ No newline at end of file diff --git a/addresses.json b/addresses.json index 2a6e3bb..957eeed 100644 --- a/addresses.json +++ b/addresses.json @@ -27,39 +27,12 @@ "MolochV3EligibilityAdaptor": "0x620646b1E1b27b56afDaf87eE13CA727F4E87cC8", "ClonesAddressStorageImplementation": "0x20C270dD8F5F8A341AA90fB8Eb0B3bc44237cc29" }, - "goerli": { - "chainId": "5", - "network": "goerli", - "Erc6551Registry": "0x02101dfB77FDE026414827Fdc604ddAF224F0921", - "HatsContract": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137", - "HatsModuleFactory": "0xfE661c01891172046feE16D3a57c3Cf456729efA", - "CharacterAccount": "0x3cD942A7902b955E0a5222d80376B29062Ea9DaA", - "ClassesImplementation": "0x62b97b232Ed8b11409ff8a7648214010426355ed", - "CharacterSheetsFactory": "0x570F219D3FA620E7Fd48ff4787F2Da169Ed12244", - "ExperienceImplementation": "0x8065d9c66d41bF35473B71D17aCBD535dd2a3488", - "ItemsManagerImplementation": "0xEC48EB7BE3B867cB9Af481533008752d64514d84", - "CloneAddressStorage": "0xbc0A467b8100c87282988c6ff71d4227C822bF2e", - "ImplementationAddressStorage": "0x07ECa0752E074C981fB0a5eE16E7114d809ed3ff", - "ItemsImplementation": "0x8117C81B65dF2d899E413F65dd1230Ccd92Ea80a", - "CharacterSheetsImplementation": "0x5d429668Cc7FAE3657359548B413Ced02398F030", - "CharacterEligibilityAdaptor": "0xc94eb020b474a8320a7500CcC0E29cb5Aa86BD34", - "CharacterEligibilityAdaptorV2": "", - "CharacterEligibilityAdaptorV3": "0xC557D0a20e0BfC06adFA2eD0503523dF011E850B", - "ClassLevelAdaptor": "0x1F4Bf13C531f4875fb5d2859dCD88591b8d471f1", - "HatsAdaptor": "0x163920D269760feB9eD161601648832d963e4143", - "AddressHatsEligibilityModule": "0x9AaF0df5657b634131784523F4F5e83459c61986", - "CharacterSheetsLevelEligibilityModule": "0x6f52b30a4730C5Cb765E7c762249dc4Ae02fB8eD", - "ERC721HatsEligibilityModule": "0x6bAa65D3E024D8ebA717085691dd8A985e9F267E", - "ERC6551HatsEligibilityModule": "0xa25B005B82209B2D2dC6E86006ae75649b1dAb5b", - "MultiERC6511HatsEligibilityModule": "", - "ClonesAddressStorageImplementation": "0xfFE77f170cEFcC2248e6697c32ef665FcF685E14" - }, "sepolia": { "chainId": "11155111", "network": "sepolia", "Erc6551Registry": "0x02101dfB77FDE026414827Fdc604ddAF224F0921", "CharacterAccount": "0x42D6c5A636f6359C36e37cBaA09568627B8F6Afc", - "CharacterSheetsImplementation": "0x62f81Be2545651e1a11a0eF7548Abf5248A41eDb", + "CharacterSheetsImplementation": "0x6639BD11fA55384184208ae8bC49d5235E5eA30B", "ClassesImplementation": "0xC75219c28E8E9CAaBa6c2928519f179543daee0F", "CharacterSheetsFactory": "0xAfFCc11509C4EBCfA78b72128eB1a2256504F135", "ClassLevelAdaptor": "0x12b097B25BE1Ca56fB9943e8725b1965CF0908c2", diff --git a/src/implementations/CharacterSheetsImplementation.sol b/src/implementations/CharacterSheetsImplementation.sol index 82bec05..69eed5f 100644 --- a/src/implementations/CharacterSheetsImplementation.sol +++ b/src/implementations/CharacterSheetsImplementation.sol @@ -2,22 +2,19 @@ pragma solidity ^0.8.20; // pragma abicoder v2; -import { - ERC721URIStorageUpgradeable, - ERC721Upgradeable -} from "openzeppelin-contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; -import {IERC721} from "openzeppelin-contracts/token/ERC721/IERC721.sol"; -import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {IERC1155} from "openzeppelin-contracts/token/ERC1155/IERC1155.sol"; -import {IERC6551Registry} from "../interfaces/IERC6551Registry.sol"; -import {ICharacterEligibilityAdaptor} from "../interfaces/ICharacterEligibilityAdaptor.sol"; -import {IHatsAdaptor} from "../interfaces/IHatsAdaptor.sol"; -import {IItems} from "../interfaces/IItems.sol"; -import {IClonesAddressStorage} from "../interfaces/IClonesAddressStorage.sol"; -import {IImplementationAddressStorage} from "../interfaces/IImplementationAddressStorage.sol"; - -import {CharacterSheet} from "../lib/Structs.sol"; -import {Errors} from "../lib/Errors.sol"; +import { ERC721URIStorageUpgradeable, ERC721Upgradeable } from "openzeppelin-contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; +import { IERC721 } from "openzeppelin-contracts/token/ERC721/IERC721.sol"; +import { UUPSUpgradeable } from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IERC1155 } from "openzeppelin-contracts/token/ERC1155/IERC1155.sol"; +import { IERC6551Registry } from "../interfaces/IERC6551Registry.sol"; +import { ICharacterEligibilityAdaptor } from "../interfaces/ICharacterEligibilityAdaptor.sol"; +import { IHatsAdaptor } from "../interfaces/IHatsAdaptor.sol"; +import { IItems } from "../interfaces/IItems.sol"; +import { IClonesAddressStorage } from "../interfaces/IClonesAddressStorage.sol"; +import { IImplementationAddressStorage } from "../interfaces/IImplementationAddressStorage.sol"; + +import { CharacterSheet } from "../lib/Structs.sol"; +import { Errors } from "../lib/Errors.sol"; /** * @title Character Sheets @@ -27,475 +24,574 @@ import {Errors} from "../lib/Errors.sol"; * a rpg themed reputation system with experience points awarded by a centralized authority the "GAME_MASTER" and items and classes that can be owned and equipped * by the base character account. */ -contract CharacterSheetsImplementation is ERC721URIStorageUpgradeable, UUPSUpgradeable { - string public baseTokenURI; - string public metadataURI; - - address public erc6551CharacterAccount; - address public erc6551Registry; - - IClonesAddressStorage public clones; - - // characterId => characterSheet - mapping(uint256 => CharacterSheet) private _sheets; - // playerAddress => characterId - mapping(address => uint256) private _playerSheets; - // characterAddress => characterId - mapping(address => uint256) private _characterSheets; - - mapping(address => bool) public jailed; - - uint256 public totalSheets; - - event NewCharacterSheetRolled(address player, address account, uint256 characterId); - event MetadataURIUpdated(string newURI); - event BaseURIUpdated(string newURI); - event Erc6551CharacterAccountUpdated(address newERC6551CharacterAccount); - event Erc6551RegistryUpdated(address newERC6551Registry); - event CharacterRemoved(uint256 characterId); - event ClonesAddressStorageUpdated(address newClonesStorageAddress); - event ItemEquipped(uint256 characterId, uint256 itemId); - event ItemUnequipped(uint256 characterId, uint256 itemId); - event CharacterUpdated(uint256 characterId); - event PlayerJailed(address playerAddress, bool thrownInJail); - event CharacterRestored(address player, address account, uint256 characterId); - event ExternalCharacterAdded(address player, address account, uint256 characterId); - - modifier onlyAdmin() { - if (!IHatsAdaptor(clones.hatsAdaptor()).isAdmin(msg.sender)) { - revert Errors.AdminOnly(); - } - _; - } - - modifier onlyGameMaster() { - if (!IHatsAdaptor(clones.hatsAdaptor()).isGameMaster(msg.sender)) { - revert Errors.GameMasterOnly(); - } - _; - } - - modifier onlyPlayer() { - if (!IHatsAdaptor(clones.hatsAdaptor()).isPlayer(msg.sender)) { - revert Errors.PlayerOnly(); - } - _; - } - - modifier onlyCharacter() { - if (!IHatsAdaptor(clones.hatsAdaptor()).isCharacter(msg.sender)) { - revert Errors.CharacterOnly(); - } - _; - } - - modifier validTransfer(address from, address to, uint256 characterId) { - if (balanceOf(to) != 0) { - revert Errors.TokenBalanceError(); - } - - _; - - _playerSheets[from] = 0; - _playerSheets[to] = characterId; - _sheets[characterId].playerAddress = to; - - _ifNotPlayerMintHat(to); - } - - constructor() { - _disableInitializers(); - } - - /** - * - * @param _encodedParameters encoded parameters must include: - * - address daoAddress: the address of the dao who's member list will be allowed to become players and who - * will be able to interact with this contract - * - address[] gameMasters: an array addresses of the person/persons who are authorized to issue player - * cards, classes, and items. - * - address owner: the account that will have the DEFAULT_ADMIN role - * - address CharacterAccountImplementation: the erc 4337 implementation of the Character account. - * - address erc6551Registry: the address of the deployed ERC6551 registry on whichever chain these - * contracts are on - * - string metadataURI: the metadata for the character sheets implementation - * - string baseURI: the default uri of the player card images, arbitrary a different uri can be set - * when the character sheet is minted. - * - address itemsImplementation: this is the address of the ERC1155 items contract associated - * with this contract. this is assigned at contract creation. - */ - function initialize(bytes calldata _encodedParameters) external initializer { - __ERC721_init_unchained("CharacterSheet", "CHAS"); - __UUPSUpgradeable_init(); - - address clonesStorage; - address implementationStorage; - - (clonesStorage, implementationStorage, metadataURI, baseTokenURI) = - abi.decode(_encodedParameters, (address, address, string, string)); - clones = IClonesAddressStorage(clonesStorage); - erc6551Registry = IImplementationAddressStorage(implementationStorage).erc6551Registry(); - erc6551CharacterAccount = IImplementationAddressStorage(implementationStorage).erc6551AccountImplementation(); - } - - /** - * - * @param _tokenURI the uri of the character sheet metadata - * if no uri is stored then it will revert to the base uri of the contract - */ - function rollCharacterSheet(address player, string calldata _tokenURI) external returns (uint256) { - _checkRollReverts(player); - - uint256 existingCharacterId = _playerSheets[player]; - - if (existingCharacterId != 0 || _sheets[existingCharacterId].playerAddress == player) { - // must restore sheet - revert Errors.PlayerError(); - } - - uint256 characterId = totalSheets + 1; - - // calculate ERC6551 account address - address characterAccount = IERC6551Registry(erc6551Registry).createAccount( - erc6551CharacterAccount, block.chainid, address(this), characterId, characterId, "" - ); - // setting salt as characterId - - _sheets[characterId] = - CharacterSheet({accountAddress: characterAccount, playerAddress: player, inventory: new uint256[](0)}); - - _mintSheet(player, characterAccount, characterId, _tokenURI); - - emit NewCharacterSheetRolled(player, characterAccount, characterId); - return characterId; - } - - /** - * unequips an item from the character sheet inventory - * @param characterId the player to have the item type from their inventory - * @param characterId the erc1155 token id of the item to be unequipped - */ - function unequipItemFromCharacter(uint256 characterId, uint256 itemId) external onlyCharacter { - if (msg.sender != _sheets[characterId].accountAddress) { - revert Errors.OwnershipError(); - } - - if (IERC1155(clones.items()).balanceOf(msg.sender, itemId) == 0) { - // TODO ensure that when items are transferred from a character sheet that they are unequipped - revert Errors.InventoryError(); - } - - uint256[] memory arr = _sheets[characterId].inventory; - - bool success; - 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; - } - } - - _sheets[characterId].inventory = arr; - _sheets[characterId].inventory.pop(); - - success = true; - break; - } - } - - if (success) { - emit ItemUnequipped(characterId, itemId); - } else { - revert Errors.InventoryError(); - } - } - - /** - * adds an item to the items in the character sheet inventory - * @param characterId the id of the player receiving the item - * @param itemId the itemId of the item - */ - function equipItemToCharacter(uint256 characterId, uint256 itemId) external onlyCharacter { - if (msg.sender != _sheets[characterId].accountAddress) { - revert Errors.OwnershipError(); - } - - if (IERC1155(clones.items()).balanceOf(msg.sender, itemId) < 1) { - revert Errors.InsufficientBalance(); - } - - if (isItemEquipped(characterId, itemId)) { - revert Errors.InventoryError(); - } - - _sheets[characterId].inventory.push(itemId); - emit ItemEquipped(characterId, itemId); - } - - /** - * this will burn the nft of the player. only a player can burn their own token. - */ - function renounceSheet() external onlyPlayer { - uint256 _characterId = _playerSheets[msg.sender]; - - if (_ownerOf(_characterId) != msg.sender) { - revert Errors.OwnershipError(); - } - - _burn(_characterId); - - emit CharacterRemoved(_characterId); - } - - /** - * restores a previously renounced sheet if called by the wrong player and incorrect address will be created that does not control any assets - * does not work with imported characters. must be done in original game. - * @return the ERC6551 account address - */ - function restoreSheet() external returns (address) { - uint256 characterId = _playerSheets[msg.sender]; - - if (_ownerOf(characterId) != address(0)) { - revert Errors.OwnershipError(); - } - if ( - clones.characterEligibilityAdaptor() != address(0) - && !ICharacterEligibilityAdaptor(clones.characterEligibilityAdaptor()).isEligible(msg.sender) - ) { - revert Errors.EligibilityError(); - } - if (jailed[msg.sender]) { - revert Errors.Jailed(); - } - address restoredAccount = IERC6551Registry(erc6551Registry).createAccount( - erc6551CharacterAccount, block.chainid, address(this), characterId, characterId, "" - ); - // setting salt as characterId - - if (_sheets[characterId].playerAddress != msg.sender) { - revert Errors.PlayerError(); - } - if (_sheets[characterId].accountAddress != restoredAccount) { - revert Errors.CharacterError(); - } - - _safeMint(msg.sender, characterId); - - emit CharacterRestored(msg.sender, restoredAccount, characterId); - - return restoredAccount; - } - - /** - * Burns a players characterSheet. can only be done if there is a passing guild kick proposal - * @param characterId the characterId of the player to be removed. - */ - function removeSheet(uint256 characterId) external onlyGameMaster { - address playerAddress = _ownerOf(characterId); - if (playerAddress == address(0)) { - revert Errors.CharacterError(); - } - - if ( - clones.characterEligibilityAdaptor() != address(0) - && ICharacterEligibilityAdaptor(clones.characterEligibilityAdaptor()).isEligible(playerAddress) - ) { - revert Errors.EligibilityError(); - } - - if (!jailed[playerAddress]) { - revert Errors.Jailed(); - } - - _burn(characterId); - - emit CharacterRemoved(characterId); +contract CharacterSheetsImplementation is + ERC721URIStorageUpgradeable, + UUPSUpgradeable +{ + string public baseTokenURI; + string public metadataURI; + + address public erc6551CharacterAccount; + address public erc6551Registry; + + IClonesAddressStorage public clones; + + // characterId => characterSheet + mapping(uint256 => CharacterSheet) private _sheets; + // playerAddress => characterId + mapping(address => uint256) private _playerSheets; + // characterAddress => characterId + mapping(address => uint256) private _characterSheets; + + mapping(address => bool) public jailed; + + uint256 public totalSheets; + + event NewCharacterSheetRolled( + address player, + address account, + uint256 characterId + ); + event MetadataURIUpdated(string newURI); + event BaseURIUpdated(string newURI); + event Erc6551CharacterAccountUpdated(address newERC6551CharacterAccount); + event Erc6551RegistryUpdated(address newERC6551Registry); + event CharacterRemoved(uint256 characterId); + event ClonesAddressStorageUpdated(address newClonesStorageAddress); + event ItemEquipped(uint256 characterId, uint256 itemId); + event ItemUnequipped(uint256 characterId, uint256 itemId); + event CharacterUpdated(uint256 characterId); + event PlayerJailed(address playerAddress, bool thrownInJail); + event CharacterRestored(address player, address account, uint256 characterId); + event ExternalCharacterAdded( + address player, + address account, + uint256 characterId + ); + + modifier onlyAdmin() { + if (!IHatsAdaptor(clones.hatsAdaptor()).isAdmin(msg.sender)) { + revert Errors.AdminOnly(); + } + _; + } + + modifier onlyGameMaster() { + if (!IHatsAdaptor(clones.hatsAdaptor()).isGameMaster(msg.sender)) { + revert Errors.GameMasterOnly(); + } + _; + } + + modifier onlyPlayer() { + if (!IHatsAdaptor(clones.hatsAdaptor()).isPlayer(msg.sender)) { + revert Errors.PlayerOnly(); + } + _; + } + + modifier onlyCharacter() { + if (!IHatsAdaptor(clones.hatsAdaptor()).isCharacter(msg.sender)) { + revert Errors.CharacterOnly(); + } + _; + } + + modifier validTransfer( + address from, + address to, + uint256 characterId + ) { + if (balanceOf(to) != 0) { + revert Errors.TokenBalanceError(); + } + + _; + + _playerSheets[from] = 0; + _playerSheets[to] = characterId; + _sheets[characterId].playerAddress = to; + + _ifNotPlayerMintHat(to); + } + + constructor() { + _disableInitializers(); + } + + /** + * + * @param _encodedParameters encoded parameters must include: + * - address daoAddress: the address of the dao who's member list will be allowed to become players and who + * will be able to interact with this contract + * - address[] gameMasters: an array addresses of the person/persons who are authorized to issue player + * cards, classes, and items. + * - address owner: the account that will have the DEFAULT_ADMIN role + * - address CharacterAccountImplementation: the erc 4337 implementation of the Character account. + * - address erc6551Registry: the address of the deployed ERC6551 registry on whichever chain these + * contracts are on + * - string metadataURI: the metadata for the character sheets implementation + * - string baseURI: the default uri of the player card images, arbitrary a different uri can be set + * when the character sheet is minted. + * - address itemsImplementation: this is the address of the ERC1155 items contract associated + * with this contract. this is assigned at contract creation. + */ + function initialize(bytes calldata _encodedParameters) external initializer { + __ERC721_init_unchained("CharacterSheet", "CHAS"); + __UUPSUpgradeable_init(); + + address clonesStorage; + address implementationStorage; + + (clonesStorage, implementationStorage, metadataURI, baseTokenURI) = abi + .decode(_encodedParameters, (address, address, string, string)); + clones = IClonesAddressStorage(clonesStorage); + erc6551Registry = IImplementationAddressStorage(implementationStorage) + .erc6551Registry(); + erc6551CharacterAccount = IImplementationAddressStorage( + implementationStorage + ).erc6551AccountImplementation(); + } + + /** + * + * @param _tokenURI the uri of the character sheet metadata + * if no uri is stored then it will revert to the base uri of the contract + */ + function rollCharacterSheet( + address player, + string calldata _tokenURI + ) external returns (uint256) { + _checkRollReverts(player); + + uint256 existingCharacterId = _playerSheets[player]; + + if ( + existingCharacterId != 0 || + _sheets[existingCharacterId].playerAddress == player + ) { + // must restore sheet + revert Errors.PlayerError(); + } + + uint256 characterId = totalSheets + 1; + + // calculate ERC6551 account address + address characterAccount = IERC6551Registry(erc6551Registry).createAccount( + erc6551CharacterAccount, + block.chainid, + address(this), + characterId, + characterId, + "" + ); + // setting salt as characterId + + _sheets[characterId] = CharacterSheet({ + accountAddress: characterAccount, + playerAddress: player, + inventory: new uint256[](0) + }); + + _mintSheet(player, characterAccount, characterId, _tokenURI); + + emit NewCharacterSheetRolled(player, characterAccount, characterId); + return characterId; + } + + /** + * unequips an item from the character sheet inventory + * @param characterId the player to have the item type from their inventory + * @param characterId the erc1155 token id of the item to be unequipped + */ + function unequipItemFromCharacter( + uint256 characterId, + uint256 itemId + ) external onlyCharacter { + if (msg.sender != _sheets[characterId].accountAddress) { + revert Errors.OwnershipError(); + } + + if (IERC1155(clones.items()).balanceOf(msg.sender, itemId) == 0) { + // TODO ensure that when items are transferred from a character sheet that they are unequipped + revert Errors.InventoryError(); + } + + uint256[] memory arr = _sheets[characterId].inventory; + + bool success; + 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; + } + } + + _sheets[characterId].inventory = arr; + _sheets[characterId].inventory.pop(); + + success = true; + break; + } + } + + if (success) { + emit ItemUnequipped(characterId, itemId); + } else { + revert Errors.InventoryError(); + } + } + + /** + * adds an item to the items in the character sheet inventory + * @param characterId the id of the player receiving the item + * @param itemId the itemId of the item + */ + function equipItemToCharacter( + uint256 characterId, + uint256 itemId + ) external onlyCharacter { + if (msg.sender != _sheets[characterId].accountAddress) { + revert Errors.OwnershipError(); } - /** - * allows a player to update the character metadata in the contract - * @param newCid the new metadata URI - */ - function updateCharacterMetadata(string calldata newCid) external onlyPlayer { - uint256 characterId = _playerSheets[msg.sender]; - - if (_ownerOf(characterId) != msg.sender) { - revert Errors.OwnershipError(); - } - - _setTokenURI(characterId, newCid); - - emit CharacterUpdated(characterId); + if (IERC1155(clones.items()).balanceOf(msg.sender, itemId) < 1) { + revert Errors.InsufficientBalance(); } - function jailPlayer(address playerAddress, bool throwInJail) external onlyGameMaster { - jailed[playerAddress] = throwInJail; - emit PlayerJailed(playerAddress, throwInJail); + if (isItemEquipped(characterId, itemId)) { + revert Errors.InventoryError(); } - function updateClones(address clonesStorage) public onlyAdmin { - clones = IClonesAddressStorage(clonesStorage); - emit ClonesAddressStorageUpdated(clonesStorage); - } + _sheets[characterId].inventory.push(itemId); + emit ItemEquipped(characterId, itemId); + } - function updateErc6551Registry(address newErc6551Storage) public onlyAdmin { - erc6551Registry = newErc6551Storage; - emit Erc6551RegistryUpdated(newErc6551Storage); - } + /** + * this will burn the nft of the player. only a player can burn their own token. + */ + function renounceSheet() external onlyPlayer { + uint256 _characterId = _playerSheets[msg.sender]; - function updateErc6551CharacterAccount(address newERC6551CharacterAccount) public onlyAdmin { - erc6551CharacterAccount = newERC6551CharacterAccount; - emit Erc6551CharacterAccountUpdated(newERC6551CharacterAccount); + if (_ownerOf(_characterId) != msg.sender) { + revert Errors.OwnershipError(); } - function updateBaseUri(string memory _uri) public onlyAdmin { - baseTokenURI = _uri; - emit BaseURIUpdated(_uri); - } + _burn(_characterId); - function updateMetadataUri(string memory _uri) public onlyAdmin { - metadataURI = _uri; - emit MetadataURIUpdated(_uri); - } + emit CharacterRemoved(_characterId); + } - // transfer overrides since these tokens should be soulbound or only transferable by the gameMaster + /** + * restores a previously renounced sheet if called by the wrong player and incorrect address will be created that does not control any assets + * does not work with imported characters. must be done in original game. + * @return the ERC6551 account address + */ + function restoreSheet() external returns (address) { + uint256 characterId = _playerSheets[msg.sender]; - /** - * @dev See {IERC721-approve}. - */ - function approve(address to, uint256 characterId) public virtual override(ERC721Upgradeable, IERC721) { - return super.approve(to, characterId); + if (_ownerOf(characterId) != address(0)) { + revert Errors.OwnershipError(); } - - /** - * @dev See {IERC721-setApprovalForAll}. - */ - function setApprovalForAll(address operator, bool approved) public virtual override(ERC721Upgradeable, IERC721) { - return super.setApprovalForAll(operator, approved); + if ( + clones.characterEligibilityAdaptor() != address(0) && + !ICharacterEligibilityAdaptor(clones.characterEligibilityAdaptor()) + .isEligible(msg.sender) + ) { + revert Errors.EligibilityError(); } - - /** - * @dev See {IERC721-transferFrom}. - */ - function transferFrom(address from, address to, uint256 characterId) - public - virtual - override(ERC721Upgradeable, IERC721) - onlyGameMaster - validTransfer(from, to, characterId) - { - super.transferFrom(from, to, characterId); + if (jailed[msg.sender]) { + revert Errors.Jailed(); } + address restoredAccount = IERC6551Registry(erc6551Registry).createAccount( + erc6551CharacterAccount, + block.chainid, + address(this), + characterId, + characterId, + "" + ); + // setting salt as characterId - /** - * @dev See {IERC721-safeTransferFrom}. - */ - function safeTransferFrom(address from, address to, uint256 characterId, bytes memory) - public - virtual - override(ERC721Upgradeable, IERC721) - onlyGameMaster - validTransfer(from, to, characterId) - { - super.transferFrom(from, to, characterId); + if (_sheets[characterId].playerAddress != msg.sender) { + revert Errors.PlayerError(); } - - function getCharacterSheetByCharacterId(uint256 characterId) public view returns (CharacterSheet memory) { - return _sheets[characterId]; + if (_sheets[characterId].accountAddress != restoredAccount) { + revert Errors.CharacterError(); } - function getCharacterIdByAccountAddress(address _account) public view returns (uint256 id) { - return _characterSheets[_account]; - } + _safeMint(msg.sender, characterId); - function getCharacterIdByPlayerAddress(address _player) public view returns (uint256) { - uint256 characterId = _playerSheets[_player]; - if (_ownerOf(characterId) != _player) { - revert Errors.CharacterError(); - } - return characterId; - } + emit CharacterRestored(msg.sender, restoredAccount, characterId); - function isItemEquipped(uint256 characterId, uint256 itemId) public view returns (bool) { - CharacterSheet storage sheet = _sheets[characterId]; - if (sheet.inventory.length == 0) { - return false; - } - uint256 supply = IItems(clones.items()).getItem(itemId).supply; - if (supply == 0) { - revert Errors.ItemError(); - } + return restoredAccount; + } - for (uint256 i; i < sheet.inventory.length; i++) { - if (sheet.inventory[i] == itemId) { - return true; - } - } - return false; + /** + * Burns a players characterSheet. can only be done if there is a passing guild kick proposal + * @param characterId the characterId of the player to be removed. + */ + function removeSheet(uint256 characterId) external onlyGameMaster { + address playerAddress = _ownerOf(characterId); + if (playerAddress == address(0)) { + revert Errors.CharacterError(); + } + + if ( + clones.characterEligibilityAdaptor() != address(0) && + ICharacterEligibilityAdaptor(clones.characterEligibilityAdaptor()) + .isEligible(playerAddress) + ) { + revert Errors.EligibilityError(); + } + + if (!jailed[playerAddress]) { + revert Errors.Jailed(); + } + + _burn(characterId); + + emit CharacterRemoved(characterId); + } + + /** + * allows a player to update the character metadata in the contract + * @param newCid the new metadata URI + */ + function updateCharacterMetadata(string calldata newCid) external onlyPlayer { + uint256 characterId = _playerSheets[msg.sender]; + + if (_ownerOf(characterId) != msg.sender) { + revert Errors.OwnershipError(); + } + + _setTokenURI(characterId, newCid); + + emit CharacterUpdated(characterId); + } + + function jailPlayer( + address playerAddress, + bool throwInJail + ) external onlyGameMaster { + jailed[playerAddress] = throwInJail; + emit PlayerJailed(playerAddress, throwInJail); + } + + function updateClones(address clonesStorage) public onlyAdmin { + clones = IClonesAddressStorage(clonesStorage); + emit ClonesAddressStorageUpdated(clonesStorage); + } + + function updateErc6551Registry(address newErc6551Storage) public onlyAdmin { + erc6551Registry = newErc6551Storage; + emit Erc6551RegistryUpdated(newErc6551Storage); + } + + function updateErc6551CharacterAccount( + address newERC6551CharacterAccount + ) public onlyAdmin { + erc6551CharacterAccount = newERC6551CharacterAccount; + emit Erc6551CharacterAccountUpdated(newERC6551CharacterAccount); + } + + function updateBaseUri(string memory _uri) public onlyAdmin { + baseTokenURI = _uri; + emit BaseURIUpdated(_uri); + } + + function updateMetadataUri(string memory _uri) public onlyAdmin { + metadataURI = _uri; + emit MetadataURIUpdated(_uri); + } + + // transfer overrides since these tokens should be soulbound or only transferable by the gameMaster + + /** + * @dev See {IERC721-approve}. + */ + function approve( + address to, + uint256 characterId + ) public virtual override(ERC721Upgradeable, IERC721) { + return super.approve(to, characterId); + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll( + address operator, + bool approved + ) public virtual override(ERC721Upgradeable, IERC721) { + return super.setApprovalForAll(operator, approved); + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom( + address from, + address to, + uint256 characterId + ) + public + virtual + override(ERC721Upgradeable, IERC721) + onlyGameMaster + validTransfer(from, to, characterId) + { + if (characterId == 0) { + revert Errors.CharacterError(); + } + super.transferFrom(from, to, characterId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom( + address from, + address to, + uint256 characterId, + bytes memory + ) + public + virtual + override(ERC721Upgradeable, IERC721) + onlyGameMaster + validTransfer(from, to, characterId) + { + if (characterId == 0) { + revert Errors.CharacterError(); + } + super.transferFrom(from, to, characterId); + } + + function getCharacterSheetByCharacterId( + uint256 characterId + ) public view returns (CharacterSheet memory) { + if (characterId == 0 || characterId > totalSheets) { + revert Errors.CharacterError(); + } + return _sheets[characterId]; + } + + function getCharacterIdByAccountAddress( + address _account + ) public view returns (uint256 id) { + uint256 characterId = _characterSheets[_account]; + if (characterId == 0) { + revert Errors.CharacterError(); + } + return characterId; + } + + function getCharacterIdByPlayerAddress( + address _player + ) public view returns (uint256) { + uint256 characterId = _playerSheets[_player]; + if (characterId == 0) { + revert Errors.CharacterError(); + } + return characterId; + } + + function isItemEquipped( + uint256 characterId, + uint256 itemId + ) public view returns (bool) { + CharacterSheet storage sheet = _sheets[characterId]; + if (sheet.inventory.length == 0) { + return false; + } + uint256 supply = IItems(clones.items()).getItem(itemId).supply; + if (supply == 0) { + revert Errors.ItemError(); + } + + for (uint256 i; i < sheet.inventory.length; i++) { + if (sheet.inventory[i] == itemId) { + return true; + } + } + return false; + } + + function tokenURI( + uint256 characterId + ) public view override returns (string memory) { + return super.tokenURI(characterId); + } + + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721URIStorageUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function _ifNotPlayerMintHat(address wearer) internal { + if (!IHatsAdaptor(clones.hatsAdaptor()).isPlayer(wearer)) { + IHatsAdaptor(clones.hatsAdaptor()).mintPlayerHat(wearer); + } + } + + function _mintSheet( + address playerAddress, + address characterAccount, + uint256 characterId, + string memory _tokenURI + ) internal { + _safeMint(playerAddress, characterId); + _setTokenURI(characterId, _tokenURI); + _playerSheets[playerAddress] = characterId; + _characterSheets[characterAccount] = characterId; + + _ifNotPlayerMintHat(playerAddress); + + IHatsAdaptor(clones.hatsAdaptor()).mintCharacterHat(characterAccount); + + totalSheets++; + } + + //solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade( + address newImplementation + ) internal override onlyAdmin {} + + function _baseURI() internal view virtual override returns (string memory) { + return baseTokenURI; + } + + function _checkRollReverts(address player) internal view { + if ( + erc6551CharacterAccount == address(0) || erc6551Registry == address(0) + ) { + revert Errors.NotInitialized(); + } + + // check the eligibility adaptor to see if the player is eligible to roll a character sheet + if ( + clones.characterEligibilityAdaptor() != address(0) && + !ICharacterEligibilityAdaptor(clones.characterEligibilityAdaptor()) + .isEligible(player) + ) { + revert Errors.EligibilityError(); + } + // a character cannot be a character + if (_characterSheets[player] != 0) { + revert Errors.CharacterError(); } - function tokenURI(uint256 characterId) public view override returns (string memory) { - return super.tokenURI(characterId); + if (jailed[player]) { + revert Errors.Jailed(); } - function supportsInterface(bytes4 interfaceId) public view override(ERC721URIStorageUpgradeable) returns (bool) { - return super.supportsInterface(interfaceId); - } - - function _ifNotPlayerMintHat(address wearer) internal { - if (!IHatsAdaptor(clones.hatsAdaptor()).isPlayer(wearer)) { - IHatsAdaptor(clones.hatsAdaptor()).mintPlayerHat(wearer); - } - } - - function _mintSheet(address playerAddress, address characterAccount, uint256 characterId, string memory _tokenURI) - internal - { - _safeMint(playerAddress, characterId); - _setTokenURI(characterId, _tokenURI); - _playerSheets[playerAddress] = characterId; - _characterSheets[characterAccount] = characterId; - - _ifNotPlayerMintHat(playerAddress); - - IHatsAdaptor(clones.hatsAdaptor()).mintCharacterHat(characterAccount); - - totalSheets++; - } - - //solhint-disable-next-line no-empty-blocks - function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {} - - function _baseURI() internal view virtual override returns (string memory) { - return baseTokenURI; - } - - function _checkRollReverts(address player) internal view { - if (erc6551CharacterAccount == address(0) || erc6551Registry == address(0)) { - revert Errors.NotInitialized(); - } - - // check the eligibility adaptor to see if the player is eligible to roll a character sheet - if ( - clones.characterEligibilityAdaptor() != address(0) - && !ICharacterEligibilityAdaptor(clones.characterEligibilityAdaptor()).isEligible(player) - ) { - revert Errors.EligibilityError(); - } - // a character cannot be a character - if (_characterSheets[player] != 0) { - revert Errors.CharacterError(); - } - - if (jailed[player]) { - revert Errors.Jailed(); - } - - if (balanceOf(player) != 0) { - revert Errors.TokenBalanceError(); - } + if (balanceOf(player) != 0) { + revert Errors.TokenBalanceError(); } + } } diff --git a/test/CharacterSheet.t.sol b/test/CharacterSheet.t.sol index be5da39..0bf6c89 100644 --- a/test/CharacterSheet.t.sol +++ b/test/CharacterSheet.t.sol @@ -304,21 +304,21 @@ contract CharacterSheetsTest is SetUp { vm.expectRevert(Errors.CharacterOnly.selector); deployments.characterSheets.equipItemToCharacter(1, 1); - CharacterSheet memory sheet = deployments.characterSheets.getCharacterSheetByCharacterId(0); + CharacterSheet memory sheet = deployments.characterSheets.getCharacterSheetByCharacterId(1); assertEq(sheet.inventory.length, 0, "item should not be assigned"); vm.prank(accounts.character2); vm.expectRevert(Errors.OwnershipError.selector); deployments.characterSheets.equipItemToCharacter(1, 1); - sheet = deployments.characterSheets.getCharacterSheetByCharacterId(0); + sheet = deployments.characterSheets.getCharacterSheetByCharacterId(1); assertEq(sheet.inventory.length, 0, "item should not be assigned"); vm.prank(accounts.character1); vm.expectRevert(Errors.InsufficientBalance.selector); deployments.characterSheets.equipItemToCharacter(1, 1); - sheet = deployments.characterSheets.getCharacterSheetByCharacterId(0); + sheet = deployments.characterSheets.getCharacterSheetByCharacterId(1); assertEq(sheet.inventory.length, 0, "item should not be assigned"); }