diff --git a/contracts/GuildDiamond.sol b/contracts/GuildDiamond.sol new file mode 100644 index 0000000..1b71b8c --- /dev/null +++ b/contracts/GuildDiamond.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +* +* Implementation of a diamond. +/******************************************************************************/ + +import { LibDiamond } from "./libraries/LibDiamond.sol"; +import { IDiamondCut } from "./interfaces/IDiamondCut.sol"; +import { AppStorage } from "./libraries/LibAppStorage.sol"; + +contract GuildDiamond { + AppStorage s; + + constructor(address _contractOwner, address _diamondCutFacet) payable { + LibDiamond.setContractOwner(_contractOwner); + + // Add the diamondCut external function from the diamondCutFacet + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = IDiamondCut.diamondCut.selector; + cut[0] = IDiamondCut.FacetCut({ + facetAddress: _diamondCutFacet, + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: functionSelectors + }); + LibDiamond.diamondCut(cut, address(0), ""); + } + + // Find facet for function that is called and execute the + // function if a facet is found and return any value. + fallback() external payable { + LibDiamond.DiamondStorage storage ds; + bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION; + // get diamond storage + assembly { + ds.slot := position + } + // get facet from function selector + address facet = ds.facetAddressAndSelectorPosition[msg.sig].facetAddress; + require(facet != address(0), "Diamond: Function does not exist"); + // Execute external function from facet using delegatecall and return any value. + assembly { + // copy function selector and any arguments + calldatacopy(0, 0, calldatasize()) + // execute function call using the facet + let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) + // get any return value + returndatacopy(0, 0, returndatasize()) + // return any return value or error back to the caller + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + + receive() external payable {} +} diff --git a/contracts/facets/DiamondCutFacet.sol b/contracts/facets/DiamondCutFacet.sol new file mode 100644 index 0000000..bc0a69a --- /dev/null +++ b/contracts/facets/DiamondCutFacet.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +import { IDiamondCut } from "../interfaces/IDiamondCut.sol"; +import { LibDiamond } from "../libraries/LibDiamond.sol"; + +contract DiamondCutFacet is IDiamondCut { + /// @notice Add/replace/remove any number of functions and optionally execute + /// a function with delegatecall + /// @param _diamondCut Contains the facet addresses and function selectors + /// @param _init The address of the contract or facet to execute _calldata + /// @param _calldata A function call, including function selector and arguments + /// _calldata is executed with delegatecall on _init + function diamondCut( + FacetCut[] calldata _diamondCut, + address _init, + bytes calldata _calldata + ) external override { + LibDiamond.enforceIsContractOwner(); + LibDiamond.diamondCut(_diamondCut, _init, _calldata); + } +} diff --git a/contracts/facets/DiamondLoupeFacet.sol b/contracts/facets/DiamondLoupeFacet.sol new file mode 100644 index 0000000..81d2c02 --- /dev/null +++ b/contracts/facets/DiamondLoupeFacet.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +// The functions in DiamondLoupeFacet MUST be added to a diamond. +// The EIP-2535 Diamond standard requires these functions. + +import { LibDiamond } from "../libraries/LibDiamond.sol"; +import { IDiamondLoupe } from "../interfaces/IDiamondLoupe.sol"; +import { IERC165 } from "../interfaces/IERC165.sol"; + +contract DiamondLoupeFacet is IDiamondLoupe, IERC165 { + // Diamond Loupe Functions + //////////////////////////////////////////////////////////////////// + /// These functions are expected to be called frequently by tools. + // + // struct Facet { + // address facetAddress; + // bytes4[] functionSelectors; + // } + /// @notice Gets all facets and their selectors. + /// @return facets_ Facet + function facets() external override view returns (Facet[] memory facets_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + uint256 selectorCount = ds.selectors.length; + // create an array set to the maximum size possible + facets_ = new Facet[](selectorCount); + // create an array for counting the number of selectors for each facet + uint8[] memory numFacetSelectors = new uint8[](selectorCount); + // total number of facets + uint256 numFacets; + // loop through function selectors + for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) { + bytes4 selector = ds.selectors[selectorIndex]; + address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress; + bool continueLoop = false; + // find the functionSelectors array for selector and add selector to it + for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) { + if (facets_[facetIndex].facetAddress == facetAddress_) { + facets_[facetIndex].functionSelectors[numFacetSelectors[facetIndex]] = selector; + // probably will never have more than 256 functions from one facet contract + require(numFacetSelectors[facetIndex] < 255); + numFacetSelectors[facetIndex]++; + continueLoop = true; + break; + } + } + // if functionSelectors array exists for selector then continue loop + if (continueLoop) { + continueLoop = false; + continue; + } + // create a new functionSelectors array for selector + facets_[numFacets].facetAddress = facetAddress_; + facets_[numFacets].functionSelectors = new bytes4[](selectorCount); + facets_[numFacets].functionSelectors[0] = selector; + numFacetSelectors[numFacets] = 1; + numFacets++; + } + for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) { + uint256 numSelectors = numFacetSelectors[facetIndex]; + bytes4[] memory selectors = facets_[facetIndex].functionSelectors; + // setting the number of selectors + assembly { + mstore(selectors, numSelectors) + } + } + // setting the number of facets + assembly { + mstore(facets_, numFacets) + } + } + + /// @notice Gets all the function selectors supported by a specific facet. + /// @param _facet The facet address. + /// @return _facetFunctionSelectors The selectors associated with a facet address. + function facetFunctionSelectors(address _facet) external override view returns (bytes4[] memory _facetFunctionSelectors) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + uint256 selectorCount = ds.selectors.length; + uint256 numSelectors; + _facetFunctionSelectors = new bytes4[](selectorCount); + // loop through function selectors + for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) { + bytes4 selector = ds.selectors[selectorIndex]; + address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress; + if (_facet == facetAddress_) { + _facetFunctionSelectors[numSelectors] = selector; + numSelectors++; + } + } + // Set the number of selectors in the array + assembly { + mstore(_facetFunctionSelectors, numSelectors) + } + } + + /// @notice Get all the facet addresses used by a diamond. + /// @return facetAddresses_ + function facetAddresses() external override view returns (address[] memory facetAddresses_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + uint256 selectorCount = ds.selectors.length; + // create an array set to the maximum size possible + facetAddresses_ = new address[](selectorCount); + uint256 numFacets; + // loop through function selectors + for (uint256 selectorIndex; selectorIndex < selectorCount; selectorIndex++) { + bytes4 selector = ds.selectors[selectorIndex]; + address facetAddress_ = ds.facetAddressAndSelectorPosition[selector].facetAddress; + bool continueLoop = false; + // see if we have collected the address already and break out of loop if we have + for (uint256 facetIndex; facetIndex < numFacets; facetIndex++) { + if (facetAddress_ == facetAddresses_[facetIndex]) { + continueLoop = true; + break; + } + } + // continue loop if we already have the address + if (continueLoop) { + continueLoop = false; + continue; + } + // include address + facetAddresses_[numFacets] = facetAddress_; + numFacets++; + } + // Set the number of facet addresses in the array + assembly { + mstore(facetAddresses_, numFacets) + } + } + + /// @notice Gets the facet address that supports the given selector. + /// @dev If facet is not found return address(0). + /// @param _functionSelector The function selector. + /// @return facetAddress_ The facet address. + function facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress; + } + + // This implements ERC-165. + function supportsInterface(bytes4 _interfaceId) external override view returns (bool) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + return ds.supportedInterfaces[_interfaceId]; + } +} diff --git a/contracts/facets/LootboxFacet.sol b/contracts/facets/LootboxFacet.sol new file mode 100644 index 0000000..109ffd5 --- /dev/null +++ b/contracts/facets/LootboxFacet.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AppStorage} from "../libraries/LibAppStorage.sol"; + +contract LootboxFacet { + AppStorage internal s; + event OpenLootboxEvent(address player, uint32 lootboxId); + + function openLootbox(address player, uint32 lootboxId) external { + require(player != address(0), "Player address is not valid"); + require(s.playersExists[player], "Player does not exist"); + } + +} diff --git a/contracts/facets/OwnershipFacet.sol b/contracts/facets/OwnershipFacet.sol new file mode 100644 index 0000000..45cba09 --- /dev/null +++ b/contracts/facets/OwnershipFacet.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { LibDiamond } from "../libraries/LibDiamond.sol"; +import { IERC173 } from "../interfaces/IERC173.sol"; + +contract OwnershipFacet is IERC173 { + function transferOwnership(address _newOwner) external override { + LibDiamond.enforceIsContractOwner(); + LibDiamond.setContractOwner(_newOwner); + } + + function owner() external override view returns (address owner_) { + owner_ = LibDiamond.contractOwner(); + } +} diff --git a/contracts/facets/PlayerFacet.sol b/contracts/facets/PlayerFacet.sol new file mode 100644 index 0000000..3d22316 --- /dev/null +++ b/contracts/facets/PlayerFacet.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AppStorage} from "../libraries/LibAppStorage.sol"; + +contract PlayerFacet { + AppStorage internal s; + event LevelUpEvent(address player, uint16 level); + + function levelUp(address player) external { + require(player != address(0), "Player address is not valid"); + //require(s.playersExists[player], "Player does not exist"); + //s.players[player].level++; + } + + // for testing purposes + function supportsInterface(bytes4 _interfaceID) external view returns (bool) { + return false; + } +} diff --git a/contracts/facets/RulesFacet.sol b/contracts/facets/RulesFacet.sol new file mode 100644 index 0000000..d6081ae --- /dev/null +++ b/contracts/facets/RulesFacet.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AppStorage} from "../libraries/LibAppStorage.sol"; + +contract RulesFacet { + AppStorage internal s; + +} diff --git a/contracts/facets/TreasuryFacet.sol b/contracts/facets/TreasuryFacet.sol new file mode 100644 index 0000000..541b72e --- /dev/null +++ b/contracts/facets/TreasuryFacet.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AppStorage} from "../libraries/LibAppStorage.sol"; + +contract TreasuryFacet { + AppStorage internal s; + +} diff --git a/contracts/interfaces/IDiamondCut.sol b/contracts/interfaces/IDiamondCut.sol new file mode 100644 index 0000000..2972f69 --- /dev/null +++ b/contracts/interfaces/IDiamondCut.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +interface IDiamondCut { + enum FacetCutAction {Add, Replace, Remove} + // Add=0, Replace=1, Remove=2 + + struct FacetCut { + address facetAddress; + FacetCutAction action; + bytes4[] functionSelectors; + } + + /// @notice Add/replace/remove any number of functions and optionally execute + /// a function with delegatecall + /// @param _diamondCut Contains the facet addresses and function selectors + /// @param _init The address of the contract or facet to execute _calldata + /// @param _calldata A function call, including function selector and arguments + /// _calldata is executed with delegatecall on _init + function diamondCut( + FacetCut[] calldata _diamondCut, + address _init, + bytes calldata _calldata + ) external; + + event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata); +} diff --git a/contracts/interfaces/IDiamondLoupe.sol b/contracts/interfaces/IDiamondLoupe.sol new file mode 100644 index 0000000..c3b2570 --- /dev/null +++ b/contracts/interfaces/IDiamondLoupe.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +// A loupe is a small magnifying glass used to look at diamonds. +// These functions look at diamonds +interface IDiamondLoupe { + /// These functions are expected to be called frequently + /// by tools. + + struct Facet { + address facetAddress; + bytes4[] functionSelectors; + } + + /// @notice Gets all facet addresses and their four byte function selectors. + /// @return facets_ Facet + function facets() external view returns (Facet[] memory facets_); + + /// @notice Gets all the function selectors supported by a specific facet. + /// @param _facet The facet address. + /// @return facetFunctionSelectors_ + function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_); + + /// @notice Get all the facet addresses used by a diamond. + /// @return facetAddresses_ + function facetAddresses() external view returns (address[] memory facetAddresses_); + + /// @notice Gets the facet that supports the given selector. + /// @dev If facet is not found return address(0). + /// @param _functionSelector The function selector. + /// @return facetAddress_ The facet address. + function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_); +} diff --git a/contracts/interfaces/IERC165.sol b/contracts/interfaces/IERC165.sol new file mode 100644 index 0000000..04b7bcc --- /dev/null +++ b/contracts/interfaces/IERC165.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceId The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/interfaces/IERC173.sol b/contracts/interfaces/IERC173.sol new file mode 100644 index 0000000..a708048 --- /dev/null +++ b/contracts/interfaces/IERC173.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ERC-173 Contract Ownership Standard +/// Note: the ERC-165 identifier for this interface is 0x7f5828d0 +/* is ERC165 */ +interface IERC173 { + /// @dev This emits when ownership of a contract changes. + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @notice Get the address of the owner + /// @return owner_ The address of the owner. + function owner() external view returns (address owner_); + + /// @notice Set the address of the new owner of the contract + /// @dev Set _newOwner to address(0) to renounce any ownership. + /// @param _newOwner The address of the new owner of the contract + function transferOwnership(address _newOwner) external; +} diff --git a/contracts/libraries/LibAppStorage.sol b/contracts/libraries/LibAppStorage.sol new file mode 100644 index 0000000..dc35287 --- /dev/null +++ b/contracts/libraries/LibAppStorage.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + + struct AppStorage { + address gallionLabs; + address guildTokenContract; + address guildContract; + mapping(address => bool) playersExists; + mapping(address => Player) players; + mapping(uint32 => Lootbox) lootboxes; + } + + struct Player { + address owner; + uint16 level; + } + + struct Lootbox { + address owner; + Rarity rarity; + } + + enum Rarity { + common, + rare, + epic, + legendary + } diff --git a/contracts/libraries/LibDiamond.sol b/contracts/libraries/LibDiamond.sol new file mode 100644 index 0000000..16ea39c --- /dev/null +++ b/contracts/libraries/LibDiamond.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* Author: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ +import { IDiamondCut } from "../interfaces/IDiamondCut.sol"; + +// Remember to add the loupe functions from DiamondLoupeFacet to the diamond. +// The loupe functions are required by the EIP2535 Diamonds standard + +library LibDiamond { + bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage"); + + struct FacetAddressAndSelectorPosition { + address facetAddress; + uint16 selectorPosition; + } + + struct DiamondStorage { + // function selector => facet address and selector position in selectors array + mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition; + bytes4[] selectors; + mapping(bytes4 => bool) supportedInterfaces; + // owner of the contract + address contractOwner; + } + + function diamondStorage() internal pure returns (DiamondStorage storage ds) { + bytes32 position = DIAMOND_STORAGE_POSITION; + assembly { + ds.slot := position + } + } + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + function setContractOwner(address _newOwner) internal { + DiamondStorage storage ds = diamondStorage(); + address previousOwner = ds.contractOwner; + ds.contractOwner = _newOwner; + emit OwnershipTransferred(previousOwner, _newOwner); + } + + function contractOwner() internal view returns (address contractOwner_) { + contractOwner_ = diamondStorage().contractOwner; + } + + function enforceIsContractOwner() internal view { + require(msg.sender == diamondStorage().contractOwner, "LibDiamond: Must be contract owner"); + } + + event DiamondCut(IDiamondCut.FacetCut[] _diamondCut, address _init, bytes _calldata); + + // Internal function version of diamondCut + function diamondCut( + IDiamondCut.FacetCut[] memory _diamondCut, + address _init, + bytes memory _calldata + ) internal { + for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) { + IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action; + if (action == IDiamondCut.FacetCutAction.Add) { + addFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else if (action == IDiamondCut.FacetCutAction.Replace) { + replaceFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else if (action == IDiamondCut.FacetCutAction.Remove) { + removeFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else { + revert("LibDiamondCut: Incorrect FacetCutAction"); + } + } + emit DiamondCut(_diamondCut, _init, _calldata); + initializeDiamondCut(_init, _calldata); + } + + function addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut"); + DiamondStorage storage ds = diamondStorage(); + uint16 selectorCount = uint16(ds.selectors.length); + require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)"); + enforceHasContractCode(_facetAddress, "LibDiamondCut: Add facet has no code"); + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + address oldFacetAddress = ds.facetAddressAndSelectorPosition[selector].facetAddress; + require(oldFacetAddress == address(0), "LibDiamondCut: Can't add function that already exists"); + ds.facetAddressAndSelectorPosition[selector] = FacetAddressAndSelectorPosition(_facetAddress, selectorCount); + ds.selectors.push(selector); + selectorCount++; + } + } + + function replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut"); + DiamondStorage storage ds = diamondStorage(); + require(_facetAddress != address(0), "LibDiamondCut: Replace facet can't be address(0)"); + enforceHasContractCode(_facetAddress, "LibDiamondCut: Replace facet has no code"); + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + address oldFacetAddress = ds.facetAddressAndSelectorPosition[selector].facetAddress; + // can't replace immutable functions -- functions defined directly in the diamond + require(oldFacetAddress != address(this), "LibDiamondCut: Can't replace immutable function"); + require(oldFacetAddress != _facetAddress, "LibDiamondCut: Can't replace function with same function"); + require(oldFacetAddress != address(0), "LibDiamondCut: Can't replace function that doesn't exist"); + // replace old facet address + ds.facetAddressAndSelectorPosition[selector].facetAddress = _facetAddress; + } + } + + function removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut"); + DiamondStorage storage ds = diamondStorage(); + uint256 selectorCount = ds.selectors.length; + require(_facetAddress == address(0), "LibDiamondCut: Remove facet address must be address(0)"); + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + FacetAddressAndSelectorPosition memory oldFacetAddressAndSelectorPosition = ds.facetAddressAndSelectorPosition[selector]; + require(oldFacetAddressAndSelectorPosition.facetAddress != address(0), "LibDiamondCut: Can't remove function that doesn't exist"); + // can't remove immutable functions -- functions defined directly in the diamond + require(oldFacetAddressAndSelectorPosition.facetAddress != address(this), "LibDiamondCut: Can't remove immutable function."); + // replace selector with last selector + selectorCount--; + if (oldFacetAddressAndSelectorPosition.selectorPosition != selectorCount) { + bytes4 lastSelector = ds.selectors[selectorCount]; + ds.selectors[oldFacetAddressAndSelectorPosition.selectorPosition] = lastSelector; + ds.facetAddressAndSelectorPosition[lastSelector].selectorPosition = oldFacetAddressAndSelectorPosition.selectorPosition; + } + // delete last selector + ds.selectors.pop(); + delete ds.facetAddressAndSelectorPosition[selector]; + } + } + + function initializeDiamondCut(address _init, bytes memory _calldata) internal { + if (_init == address(0)) { + require(_calldata.length == 0, "LibDiamondCut: _init is address(0) but_calldata is not empty"); + } else { + require(_calldata.length > 0, "LibDiamondCut: _calldata is empty but _init is not address(0)"); + if (_init != address(this)) { + enforceHasContractCode(_init, "LibDiamondCut: _init address has no code"); + } + (bool success, bytes memory error) = _init.delegatecall(_calldata); + if (!success) { + if (error.length > 0) { + // bubble up the error + revert(string(error)); + } else { + revert("LibDiamondCut: _init function reverted"); + } + } + } + } + + function enforceHasContractCode(address _contract, string memory _errorMessage) internal view { + uint256 contractSize; + assembly { + contractSize := extcodesize(_contract) + } + require(contractSize > 0, _errorMessage); + } +} diff --git a/contracts/upgradeInitializers/DiamondInit.sol b/contracts/upgradeInitializers/DiamondInit.sol new file mode 100644 index 0000000..e2018ff --- /dev/null +++ b/contracts/upgradeInitializers/DiamondInit.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import {LibDiamond} from "../libraries/LibDiamond.sol"; +import {IDiamondLoupe} from "../interfaces/IDiamondLoupe.sol"; +import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; +import {IERC173} from "../interfaces/IERC173.sol"; +import {IERC165} from "../interfaces/IERC165.sol"; +import {AppStorage} from "../libraries/LibAppStorage.sol"; + +contract DiamondInit { + AppStorage internal s; + + struct Args { + address gallionLabs; + } + + function init(Args memory _args) external { + s.gallionLabs = _args.gallionLabs; + // adding ERC165 data + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + ds.supportedInterfaces[type(IERC165).interfaceId] = true; + ds.supportedInterfaces[type(IDiamondCut).interfaceId] = true; + ds.supportedInterfaces[type(IDiamondLoupe).interfaceId] = true; + ds.supportedInterfaces[type(IERC173).interfaceId] = true; + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 0000000..dc4bc72 --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,33 @@ + +/* global ethers task */ +import { task } from 'hardhat/config'; +import "@nomiclabs/hardhat-waffle"; +import "@typechain/hardhat"; + +require('@nomiclabs/hardhat-waffle'); + +// This is a sample Hardhat task. To learn how to create your own go to +// https://hardhat.org/guides/create-task.html +task('accounts', 'Prints the list of accounts', async (args, hre) => { + const accounts = await hre.ethers.getSigners() + + for (const account of accounts) { + console.log(account.address) + } +}) + +// You need to export an object to set up your config +// Go to https://hardhat.org/config/ to learn more + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +module.exports = { + solidity: '0.8.13', + settings: { + optimizer: { + enabled: true, + runs: 200 + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ece7105 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "gallion-contract", + "version": "0.0.1", + "description": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "prettier": "prettier --write contracts/**/*.sol", + "solhint": "solhint 'contracts/**/*.sol'" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Gallion-labs/gallion-contracts" + }, + "author": "Florian Léger & Luc Labbé", + "license": "MIT", + "bugs": { + "url": "https://github.com/Gallion-labs/gallion-contracts/issues" + }, + "homepage": "https://github.com/Gallion-labs/gallion-contract#readme", + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.6", + "@nomiclabs/hardhat-waffle": "^2.0.3", + "@typechain/hardhat": "^6.1.0", + "@typechain/ethers-v5": "^10.1.0", + "chai": "^4.3.6", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.1.1", + "dotenv": "^8.6.0", + "ethereum-waffle": "^3.4.4", + "ethers": "^5.6.8", + "hardhat": "^2.9.9", + "prettier": "^2.6.2", + "prettier-plugin-solidity": "^1.0.0-beta.19", + "standard": "^16.0.3", + "solhint": "^3.3.7", + "solc": "^0.8.14-fixed", + "ts-node": "^10.8.1", + "@types/node": "^17.0.42", + "typechain": "^8.1.0", + "typescript": "^4.7.3", + "@openzeppelin/contracts": "^4.6.0" + } +} diff --git a/scripts/deploy.ts b/scripts/deploy.ts new file mode 100644 index 0000000..729bc04 --- /dev/null +++ b/scripts/deploy.ts @@ -0,0 +1,81 @@ +import { getSelectors, FacetCutAction } from './libraries/diamond'; +import { ethers } from 'hardhat'; + +async function deployDiamond() { + const accounts = await ethers.getSigners() + const contractOwner = accounts[0] + + // deploy DiamondCutFacet + const DiamondCutFacet = await ethers.getContractFactory('DiamondCutFacet') + const diamondCutFacet = await DiamondCutFacet.deploy() + await diamondCutFacet.deployed() + console.log('DiamondCutFacet deployed:', diamondCutFacet.address) + + // deploy Guild Diamond + const GuildDiamond = await ethers.getContractFactory('GuildDiamond') + const diamond = await GuildDiamond.deploy(contractOwner.address, diamondCutFacet.address) + await diamond.deployed() + console.log('Diamond deployed:', diamond.address) + + // deploy DiamondInit + // DiamondInit provides a function that is called when the diamond is upgraded to initialize state variables + // Read about how the diamondCut function works here: https://eips.ethereum.org/EIPS/eip-2535#addingreplacingremoving-functions + const DiamondInit = await ethers.getContractFactory('DiamondInit') + const diamondInit = await DiamondInit.deploy() + await diamondInit.deployed() + console.log('DiamondInit deployed:', diamondInit.address) + + // deploy facets + console.log('') + console.log('Deploying facets') + const FacetNames = [ + 'DiamondLoupeFacet', + 'OwnershipFacet' + ] + const cut = [] + for (const FacetName of FacetNames) { + const Facet = await ethers.getContractFactory(FacetName) + const facet = await Facet.deploy() + await facet.deployed() + console.log(`${ FacetName } deployed: ${ facet.address }`) + cut.push({ + facetAddress: facet.address, + action: FacetCutAction.Add, + functionSelectors: getSelectors(facet) + }) + } + + // upgrade diamond with facets + console.log('') + console.log('Diamond Cut:', cut) + const diamondCut = await ethers.getContractAt('IDiamondCut', diamond.address) + let tx + let receipt + // call to init function + let functionCall = diamondInit.interface.encodeFunctionData('init', [ + { + gallionLabs: '0x8F7d7E9Adfa6da73273391C57bab0eF22651c7Bb' + } + ]) + tx = await diamondCut.diamondCut(cut, diamondInit.address, functionCall) + console.log('Diamond cut tx: ', tx.hash) + receipt = await tx.wait() + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${ tx.hash }`) + } + console.log('Completed diamond cut') + return diamond.address +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +if (require.main === module) { + deployDiamond() + .then(() => process.exit(0)) + .catch(error => { + console.error(error) + process.exit(1) + }) +} + +exports.deployDiamond = deployDiamond diff --git a/scripts/libraries/diamond.ts b/scripts/libraries/diamond.ts new file mode 100644 index 0000000..e692ffe --- /dev/null +++ b/scripts/libraries/diamond.ts @@ -0,0 +1,87 @@ +/* global ethers */ + +import { Contract } from 'ethers'; +import { ethers } from 'hardhat'; +import { Address } from '../../types'; +import { IDiamondLoupe } from '../../typechain-types/facets/DiamondLoupeFacet'; +import FacetStruct = IDiamondLoupe.FacetStruct; + +export const FacetCutAction = { Add: 0, Replace: 1, Remove: 2 }; + +class Selectors extends Array { + public contract: Contract; + + public constructor(contract: Contract, ...selectors: string[]) { + super(...selectors); + this.contract = contract; + } + + /** + * Used with getSelectors to remove selectors from an array of selectors. + * @param functionNames - an array of function signatures + */ + public remove = (functionNames: string[]): Selectors => { + const _signatures = this.filter((v) => { + for (const functionName of functionNames) { + if (v === this.contract.interface.getSighash(functionName)) { + return false; + } + } + return true; + }) as Selectors; + + return new Selectors(this.contract, ..._signatures); + } + + /** + * Used with getSelectors to get selectors from an array of selectors. + * @param functionNames - an array of function signatures + */ + public get = (functionNames: string[]): Selectors => { + const _signatures = this.filter((v) => { + for (const functionName of functionNames) { + if (v === this.contract.interface.getSighash(functionName)) { + return true; + } + } + return false; + }); + + return new Selectors(this.contract, ..._signatures); + } +} + +// get function selectors from ABI +export function getSelectors(contract: Contract): Selectors { + const signatures = Object.keys(contract.interface.functions); + const _signatures = signatures.reduce((acc: string[], val) => { + if (val !== 'init(bytes)') { + acc.push(contract.interface.getSighash(val)); + } + return acc; + }, []); + + return new Selectors(contract, ..._signatures); +} + +// get function selector from function signature +export function getSelector(func: string): string { + const abiInterface = new ethers.utils.Interface([func]); + return abiInterface.getSighash(ethers.utils.Fragment.from(func)); +} + +// remove selectors using an array of signatures +export function removeSelectors(selectors: Selectors, signatures: string[]): Selectors { + const _interface = new ethers.utils.Interface(signatures.map(v => 'function ' + v)); + const _signatures = signatures.map(v => _interface.getSighash(v)); + return selectors.filter(v => !_signatures.includes(v)) as Selectors; +} + +// find a particular address position in the return value of diamondLoupeFacet.facets() +export function findAddressPositionInFacets(facetAddress: Address, facets: FacetStruct[]): number | undefined { + for (let i = 0; i < facets.length; i++) { + if (facets[i].facetAddress === facetAddress) { + return i; + } + } +} diff --git a/test/cacheBugTest.ts b/test/cacheBugTest.ts new file mode 100644 index 0000000..f0b031c --- /dev/null +++ b/test/cacheBugTest.ts @@ -0,0 +1,112 @@ +/* global ethers describe before it */ +/* eslint-disable prefer-const */ + +import { DiamondLoupeFacet, PlayerFacet } from '../typechain-types'; +import { ethers } from 'hardhat'; + +const { deployDiamond } = require('../scripts/deploy.ts') + +const { FacetCutAction } = require('../scripts/libraries/diamond.ts') + +const { assert } = require('chai') + +// The diamond example comes with 8 function selectors +// [cut, loupe, loupe, loupe, loupe, erc165, transferOwnership, owner] +// This bug manifests if you delete something from the final +// selector slot array, so we'll fill up a new slot with +// things, and have a fresh row to work with. +describe('Cache bug test', async () => { + let diamondLoupeFacet: DiamondLoupeFacet; + let playerFacet: PlayerFacet; + const ownerSel = '0x8da5cb5b' + + const sel0 = '0x19e3b533' // fills up slot 1 + const sel1 = '0x0716c2ae' // fills up slot 1 + const sel2 = '0x11046047' // fills up slot 1 + const sel3 = '0xcf3bbe18' // fills up slot 1 + const sel4 = '0x24c1d5a7' // fills up slot 1 + const sel5 = '0xcbb835f6' // fills up slot 1 + const sel6 = '0xcbb835f7' // fills up slot 1 + const sel7 = '0xcbb835f8' // fills up slot 2 + const sel8 = '0xcbb835f9' // fills up slot 2 + const sel9 = '0xcbb835fa' // fills up slot 2 + const sel10 = '0xcbb835fb' // fills up slot 2 + + before(async function () { + let tx + let receipt + + let selectors = [ + sel0, + sel1, + sel2, + sel3, + sel4, + sel5, + sel6, + sel7, + sel8, + sel9, + sel10 + ] + + let diamondAddress = await deployDiamond() + let diamondCutFacet = await ethers.getContractAt('DiamondCutFacet', diamondAddress) + diamondLoupeFacet = await ethers.getContractAt('DiamondLoupeFacet', diamondAddress) + const PlayerFacet = await ethers.getContractFactory('PlayerFacet') + playerFacet = await PlayerFacet.deploy() + await playerFacet.deployed() + + // add functions + tx = await diamondCutFacet.diamondCut([ + { + facetAddress: playerFacet.address, + action: FacetCutAction.Add, + functionSelectors: selectors + } + ], ethers.constants.AddressZero, '0x', { gasLimit: 800000 }) + receipt = await tx.wait() + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${tx.hash}`) + } + + // Remove function selectors + // Function selector for the owner function in slot 0 + selectors = [ + ownerSel, // owner selector + sel5, + sel10 + ] + tx = await diamondCutFacet.diamondCut([ + { + facetAddress: ethers.constants.AddressZero, + action: FacetCutAction.Remove, + functionSelectors: selectors + } + ], ethers.constants.AddressZero, '0x', { gasLimit: 800000 }) + receipt = await tx.wait() + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${tx.hash}`) + } + }) + + it('should not exhibit the cache bug', async () => { + // Get the playerFacet's registered functions + let selectors = await diamondLoupeFacet.facetFunctionSelectors(playerFacet.address) + + // Check individual correctness + assert.isTrue(selectors.includes(sel0), 'Does not contain sel0') + assert.isTrue(selectors.includes(sel1), 'Does not contain sel1') + assert.isTrue(selectors.includes(sel2), 'Does not contain sel2') + assert.isTrue(selectors.includes(sel3), 'Does not contain sel3') + assert.isTrue(selectors.includes(sel4), 'Does not contain sel4') + assert.isTrue(selectors.includes(sel6), 'Does not contain sel6') + assert.isTrue(selectors.includes(sel7), 'Does not contain sel7') + assert.isTrue(selectors.includes(sel8), 'Does not contain sel8') + assert.isTrue(selectors.includes(sel9), 'Does not contain sel9') + + assert.isFalse(selectors.includes(ownerSel), 'Contains ownerSel') + assert.isFalse(selectors.includes(sel10), 'Contains sel10') + assert.isFalse(selectors.includes(sel5), 'Contains sel5') + }) +}) diff --git a/test/diamondTest.ts b/test/diamondTest.ts new file mode 100644 index 0000000..909588a --- /dev/null +++ b/test/diamondTest.ts @@ -0,0 +1,251 @@ +import { ethers } from 'hardhat'; +import { Address } from '../types'; +import { DiamondCutFacet, DiamondLoupeFacet, OwnershipFacet, PlayerFacet } from '../typechain-types'; + +const { + getSelectors, + FacetCutAction, + removeSelectors, + findAddressPositionInFacets +} = require('../scripts/libraries/diamond.ts') + +const { deployDiamond } = require('../scripts/deploy.ts') + +const { assert } = require('chai') + +describe('GuildDiamondTest', async function () { + let diamondAddress: Address; + let diamondCutFacet: DiamondCutFacet; + let diamondLoupeFacet: DiamondLoupeFacet; + let ownershipFacet: OwnershipFacet; + let tx; + let receipt; + let result; + const addresses: Address[] = []; + + before(async function () { + diamondAddress = await deployDiamond(); + diamondCutFacet = (await ethers.getContractAt('DiamondCutFacet', diamondAddress) as DiamondCutFacet); + diamondLoupeFacet = (await ethers.getContractAt('DiamondLoupeFacet', diamondAddress) as DiamondLoupeFacet); + ownershipFacet = (await ethers.getContractAt('OwnershipFacet', diamondAddress) as OwnershipFacet); + }); + + it('should have three facets -- call to facetAddresses function', async () => { + for (const address of await diamondLoupeFacet.facetAddresses()) { + addresses.push(address); + } + + assert.equal(addresses.length, 3); + }); + + it('facets should have the right function selectors -- call to facetFunctionSelectors function', async () => { + let selectors = getSelectors(diamondCutFacet); + result = await diamondLoupeFacet.facetFunctionSelectors(addresses[0]); + assert.sameMembers(result, selectors); + selectors = getSelectors(diamondLoupeFacet); + result = await diamondLoupeFacet.facetFunctionSelectors(addresses[1]); + assert.sameMembers(result, selectors); + selectors = getSelectors(ownershipFacet); + result = await diamondLoupeFacet.facetFunctionSelectors(addresses[2]); + assert.sameMembers(result, selectors); + }); + + it('selectors should be associated to facets correctly -- multiple calls to facetAddress function', async () => { + assert.equal( + addresses[0], + await diamondLoupeFacet.facetAddress('0x1f931c1c') + ); + assert.equal( + addresses[1], + await diamondLoupeFacet.facetAddress('0xcdffacc6') + ); + assert.equal( + addresses[1], + await diamondLoupeFacet.facetAddress('0x01ffc9a7') + ); + assert.equal( + addresses[2], + await diamondLoupeFacet.facetAddress('0xf2fde38b') + ); + }); + + it('should add levelUp functions', async () => { + const PlayerFacet = await ethers.getContractFactory('PlayerFacet'); + const playerFacet = await PlayerFacet.deploy(); + await playerFacet.deployed(); + addresses.push(playerFacet.address); + const selectors = getSelectors(playerFacet).remove(['supportsInterface(bytes4)']); + tx = await diamondCutFacet.diamondCut( + [{ + facetAddress: playerFacet.address, + action: FacetCutAction.Add, + functionSelectors: selectors + }], + ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + receipt = await tx.wait(); + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${ tx.hash }`); + } + result = await diamondLoupeFacet.facetFunctionSelectors(playerFacet.address); + assert.sameMembers(result, selectors); + }) + + it('should call levelUp function', async () => { + const playerFacet: PlayerFacet = await ethers.getContractAt('PlayerFacet', diamondAddress); + await playerFacet.levelUp('0x8F7d7E9Adfa6da73273391C57bab0eF22651c7Bb'); + }) + + it('should replace supportsInterface function', async () => { + const PlayerFacet = await ethers.getContractFactory('PlayerFacet'); + const selectors = getSelectors(PlayerFacet).get(['supportsInterface(bytes4)']); + const testFacetAddress = addresses[3]; + tx = await diamondCutFacet.diamondCut( + [{ + facetAddress: testFacetAddress, + action: FacetCutAction.Replace, + functionSelectors: selectors + }], + ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + receipt = await tx.wait(); + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${ tx.hash }`); + } + result = await diamondLoupeFacet.facetFunctionSelectors(testFacetAddress); + assert.sameMembers(result, getSelectors(PlayerFacet)); + }) + + it('should add openLootbox function', async () => { + const LootboxFacet = await ethers.getContractFactory('LootboxFacet'); + const lootboxFacet = await LootboxFacet.deploy(); + await lootboxFacet.deployed(); + addresses.push(lootboxFacet.address); + const selectors = getSelectors(lootboxFacet); + tx = await diamondCutFacet.diamondCut( + [{ + facetAddress: lootboxFacet.address, + action: FacetCutAction.Add, + functionSelectors: selectors + }], + ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + receipt = await tx.wait(); + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${ tx.hash }`); + } + result = await diamondLoupeFacet.facetFunctionSelectors(lootboxFacet.address); + assert.sameMembers(result, selectors); + }) + + it('should remove openLootbox function', async () => { + const lootboxFacet = await ethers.getContractAt('LootboxFacet', diamondAddress); + const functionsToKeep: string[] = []; + const selectors = getSelectors(lootboxFacet).remove(functionsToKeep); + tx = await diamondCutFacet.diamondCut( + [{ + facetAddress: ethers.constants.AddressZero, + action: FacetCutAction.Remove, + functionSelectors: selectors + }], + ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + receipt = await tx.wait(); + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${ tx.hash }`); + } + result = await diamondLoupeFacet.facetFunctionSelectors(addresses[4]); + assert.sameMembers(result, getSelectors(lootboxFacet).get(functionsToKeep)); + }) + + /*it('should remove some test1 functions', async () => { + const playerFacet = await ethers.getContractAt('PlayerFacet', diamondAddress) + const functionsToKeep = ['test1Func2()', 'test1Func11()', 'test1Func12()'] + const selectors = getSelectors(playerFacet).remove(functionsToKeep) + tx = await diamondCutFacet.diamondCut( + [{ + facetAddress: ethers.constants.AddressZero, + action: FacetCutAction.Remove, + functionSelectors: selectors + }], + ethers.constants.AddressZero, '0x', { gasLimit: 800000 }) + receipt = await tx.wait() + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${ tx.hash }`) + } + result = await diamondLoupeFacet.facetFunctionSelectors(addresses[3]) + assert.sameMembers(result, getSelectors(playerFacet).get(functionsToKeep)) + })*/ + + it('remove all functions and facets accept \'diamondCut\' and \'facets\'', async () => { + let selectors = [] + let facets = await diamondLoupeFacet.facets() + for (let i = 0; i < facets.length; i++) { + selectors.push(...facets[i].functionSelectors) + } + selectors = removeSelectors(selectors, ['facets()', 'diamondCut(tuple(address,uint8,bytes4[])[],address,bytes)']) + tx = await diamondCutFacet.diamondCut( + [{ + facetAddress: ethers.constants.AddressZero, + action: FacetCutAction.Remove, + functionSelectors: selectors + }], + ethers.constants.AddressZero, '0x', { gasLimit: 800000 }) + receipt = await tx.wait() + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${ tx.hash }`) + } + facets = await diamondLoupeFacet.facets() + assert.equal(facets.length, 2) + assert.equal(facets[0][0], addresses[0]) + assert.sameMembers(facets[0][1], ['0x1f931c1c']) + assert.equal(facets[1][0], addresses[1]) + assert.sameMembers(facets[1][1], ['0x7a0ed627']) + }) + + it('add most functions and facets', async () => { + const diamondLoupeFacetSelectors = getSelectors(diamondLoupeFacet).remove(['supportsInterface(bytes4)']) + const PlayerFacet = await ethers.getContractFactory('PlayerFacet') + const LootboxFacet = await ethers.getContractFactory('LootboxFacet') + // Any number of functions from any number of facets can be added/replaced/removed in a + // single transaction + const cut = [ + { + facetAddress: addresses[1], + action: FacetCutAction.Add, + functionSelectors: diamondLoupeFacetSelectors.remove(['facets()']) + }, + { + facetAddress: addresses[2], + action: FacetCutAction.Add, + functionSelectors: getSelectors(ownershipFacet) + }, + { + facetAddress: addresses[3], + action: FacetCutAction.Add, + functionSelectors: getSelectors(PlayerFacet) + }, + { + facetAddress: addresses[4], + action: FacetCutAction.Add, + functionSelectors: getSelectors(LootboxFacet) + } + ] + tx = await diamondCutFacet.diamondCut(cut, ethers.constants.AddressZero, '0x', { gasLimit: 8000000 }) + receipt = await tx.wait() + if (!receipt.status) { + throw Error(`Diamond upgrade failed: ${ tx.hash }`) + } + const facets = await diamondLoupeFacet.facets() + const facetAddresses = await diamondLoupeFacet.facetAddresses() + assert.equal(facetAddresses.length, 5) + assert.equal(facets.length, 5) + assert.sameMembers(facetAddresses, addresses) + assert.equal(facets[0][0], facetAddresses[0], 'first facet') + assert.equal(facets[1][0], facetAddresses[1], 'second facet') + assert.equal(facets[2][0], facetAddresses[2], 'third facet') + assert.equal(facets[3][0], facetAddresses[3], 'fourth facet') + assert.equal(facets[4][0], facetAddresses[4], 'fifth facet') + assert.sameMembers(facets[findAddressPositionInFacets(addresses[0], facets)][1], getSelectors(diamondCutFacet)) + assert.sameMembers(facets[findAddressPositionInFacets(addresses[1], facets)][1], diamondLoupeFacetSelectors) + assert.sameMembers(facets[findAddressPositionInFacets(addresses[2], facets)][1], getSelectors(ownershipFacet)) + assert.sameMembers(facets[findAddressPositionInFacets(addresses[3], facets)][1], getSelectors(PlayerFacet)) + assert.sameMembers(facets[findAddressPositionInFacets(addresses[4], facets)][1], getSelectors(LootboxFacet)) + }) +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..539e9d8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "CommonJS", + "strict": true, + "allowJs": true, + "esModuleInterop": true, + "outDir": "dist", + "resolveJsonModule": true, + "noImplicitAny": true + }, + "include": ["./scripts/**/*", "./test/**/*", "*.ts", "**/*.ts", "./tasks"], + "files": ["./hardhat.config.ts"] +} diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..5058103 --- /dev/null +++ b/types.ts @@ -0,0 +1,10 @@ +export interface AppStorage { + gallionLabs: Address; +} + +export type Address = string; + + +export function isAddress(address: string): address is Address { + return address.length === 42 && !!address.match(/^0x[0-9a-fA-F]{40}$/); +}