diff --git a/contracts/facets/LootboxFacet.sol b/contracts/facets/LootboxFacet.sol index 109ffd5..73f2f2b 100644 --- a/contracts/facets/LootboxFacet.sol +++ b/contracts/facets/LootboxFacet.sol @@ -1,15 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {AppStorage} from "../libraries/LibAppStorage.sol"; +import {AppStorage, Modifiers} from "../libraries/LibAppStorage.sol"; + +contract LootboxFacet is Modifiers { -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"); + /// @notice Open a lootbox + /// @dev This function throws for queries about the zero address and non-existing players. + /// @param playerAddress The player to query + function openLootbox(address playerAddress, uint32 lootboxId) external { + require(playerAddress != address(0), "Player address is not valid"); + require(s.players[playerAddress].createdAt > 0, "Player does not exist"); } } diff --git a/contracts/facets/OwnershipFacet.sol b/contracts/facets/OwnershipFacet.sol index 7b41963..45cba09 100644 --- a/contracts/facets/OwnershipFacet.sol +++ b/contracts/facets/OwnershipFacet.sol @@ -5,6 +5,11 @@ 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 index 3d22316..cbee459 100644 --- a/contracts/facets/PlayerFacet.sol +++ b/contracts/facets/PlayerFacet.sol @@ -1,20 +1,44 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.13; -import {AppStorage} from "../libraries/LibAppStorage.sol"; +import {AppStorage, Modifiers, Player} from "../libraries/LibAppStorage.sol"; -contract PlayerFacet { - AppStorage internal s; +contract PlayerFacet is Modifiers { 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++; + /// @notice Query all details relating to a player + /// @dev This function throws for queries about the zero address and non-existing players. + /// @param playerAddress The player to query + /// @return _player The player's details + function player(address playerAddress) public view returns (Player memory _player) { + require(playerAddress != address(0), "Player address is not valid"); + require(s.players[playerAddress].createdAt > 0, "Player does not exist"); + _player = s.players[playerAddress]; } - // for testing purposes - function supportsInterface(bytes4 _interfaceID) external view returns (bool) { - return false; + /// @notice Add a player + /// @dev This function throws for queries about the zero address and already existing players. + /// @param playerAddress Address of the player to add + function addPlayer(address playerAddress) external onlyGuildAdmin { + require(playerAddress != address(0), "PlayerFacet: Player address is not valid"); + require(!(s.players[playerAddress].createdAt > 0), "PlayerFacet: Player already exists"); + s.players[playerAddress] = Player(block.timestamp, 0); + } + + /// @notice Level-up a player + /// @dev This function throws for queries about the zero address and non-existing players. + /// @param playerAddress Address of the player to level-up + function levelUp(address playerAddress) external onlyGallion playerExists(playerAddress) { + require(playerAddress != address(0), "PlayerFacet: Player address is not valid"); + s.players[playerAddress].level++; + } + + /// @notice Remove a player + /// @dev This function throws for queries about the zero address and non-existing players. + /// @param playerAddress Address of the player to remove + function removePlayer(address playerAddress) external onlyGuildAdmin { + require(playerAddress != address(0), "PlayerFacet: Player address is not valid"); + require(s.players[playerAddress].createdAt > 0, "PlayerFacet: Player does not exist"); + delete s.players[playerAddress]; } } diff --git a/contracts/facets/Test1Facet.sol b/contracts/facets/Test1Facet.sol new file mode 100644 index 0000000..fbd5c5b --- /dev/null +++ b/contracts/facets/Test1Facet.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// For test purposes only. +contract Test1Facet { + + event TestEvent(address something); + + function test1Func1() external {} + + function test1Func2() external {} + + function test1Func3() external {} + + function test1Func4() external {} + + function test1Func5() external {} + + function test1Func6() external {} + + function test1Func7() external {} + + function test1Func8() external {} + + function test1Func9() external {} + + function test1Func10() external {} + + function test1Func11() external {} + + function test1Func12() external {} + + function test1Func13() external {} + + function test1Func14() external {} + + function test1Func15() external {} + + function test1Func16() external {} + + function test1Func17() external {} + + function test1Func18() external {} + + function test1Func19() external {} + + function test1Func20() external {} + + function supportsInterface(bytes4 _interfaceID) external view returns (bool) {} +} \ No newline at end of file diff --git a/contracts/facets/Test2Facet.sol b/contracts/facets/Test2Facet.sol new file mode 100644 index 0000000..7717c78 --- /dev/null +++ b/contracts/facets/Test2Facet.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// For test purposes only. +contract Test2Facet { + + function test2Func1() external {} + + function test2Func2() external {} + + function test2Func3() external {} + + function test2Func4() external {} + + function test2Func5() external {} + + function test2Func6() external {} + + function test2Func7() external {} + + function test2Func8() external {} + + function test2Func9() external {} + + function test2Func10() external {} + + function test2Func11() external {} + + function test2Func12() external {} + + function test2Func13() external {} + + function test2Func14() external {} + + function test2Func15() external {} + + function test2Func16() external {} + + function test2Func17() external {} + + function test2Func18() external {} + + function test2Func19() external {} + + function test2Func20() external {} +} \ No newline at end of file diff --git a/contracts/libraries/LibAppStorage.sol b/contracts/libraries/LibAppStorage.sol index dc35287..c60b3d2 100644 --- a/contracts/libraries/LibAppStorage.sol +++ b/contracts/libraries/LibAppStorage.sol @@ -1,28 +1,61 @@ // 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; +import {LibDiamond} from "./LibDiamond.sol"; +import {LibMeta} from "./LibMeta.sol"; + +struct AppStorage { + bytes32 domainSeparator; + address owner; + address gallionLabs; + address guildTokenContract; + address guildContract; + mapping(address => Admin) guildAdmins; + mapping(address => Player) players; + mapping(uint32 => Lootbox) lootboxes; +} + +struct Admin { + uint createdAt; +} + +struct Player { + uint createdAt; + uint16 level; +} + +struct Lootbox { + address owner; + Rarity rarity; +} + +enum Rarity { + common, + rare, + epic, + legendary +} + +contract Modifiers { + AppStorage internal s; + + modifier onlyGuildAdmin() { + require(s.guildAdmins[LibMeta.msgSender()].createdAt > 0, "LibAppStorage: Only guild admins can call this function"); + _; } - struct Player { - address owner; - uint16 level; + modifier playerExists(address player) { + require(s.players[player].createdAt > 0, "LibAppStorage: Player does not exist"); + _; } - struct Lootbox { - address owner; - Rarity rarity; + modifier onlyOwner() { + LibDiamond.enforceIsContractOwner(); + _; } - enum Rarity { - common, - rare, - epic, - legendary + modifier onlyGallion() { + require(LibMeta.msgSender() == s.gallionLabs, "LibAppStorage: Only Gallion can call this function"); + _; } +} diff --git a/contracts/libraries/LibMeta.sol b/contracts/libraries/LibMeta.sol new file mode 100644 index 0000000..7c2cba8 --- /dev/null +++ b/contracts/libraries/LibMeta.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +library LibMeta { + bytes32 internal constant EIP712_DOMAIN_TYPEHASH = + keccak256(bytes("EIP712Domain(string name,string version,uint256 salt,address verifyingContract)")); + + function domainSeparator(string memory name, string memory version) internal view returns (bytes32 domainSeparator_) { + domainSeparator_ = keccak256( + abi.encode(EIP712_DOMAIN_TYPEHASH, keccak256(bytes(name)), keccak256(bytes(version)), getChainID(), address(this)) + ); + } + + function getChainID() internal view returns (uint256 id) { + assembly { + id := chainid() + } + } + + function msgSender() internal view returns (address sender_) { + if (msg.sender == address(this)) { + bytes memory array = msg.data; + uint256 index = msg.data.length; + assembly { + // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. + sender_ := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff) + } + } else { + sender_ = msg.sender; + } + } +} diff --git a/contracts/upgradeInitializers/DiamondInit.sol b/contracts/upgradeInitializers/DiamondInit.sol index e2018ff..1a206d7 100644 --- a/contracts/upgradeInitializers/DiamondInit.sol +++ b/contracts/upgradeInitializers/DiamondInit.sol @@ -6,17 +6,26 @@ 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"; +import {AppStorage, Admin} from "../libraries/LibAppStorage.sol"; +import {LibMeta} from "../libraries/LibMeta.sol"; contract DiamondInit { AppStorage internal s; struct Args { address gallionLabs; + address[] guildAdmins; } function init(Args memory _args) external { s.gallionLabs = _args.gallionLabs; + + for(uint i = 0; i < _args.guildAdmins.length; i++) { + s.guildAdmins[_args.guildAdmins[i]] = Admin(block.timestamp); + } + + s.domainSeparator = LibMeta.domainSeparator("GallionDiamond", "V1"); + // adding ERC165 data LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); ds.supportedInterfaces[type(IERC165).interfaceId] = true; diff --git a/package.json b/package.json index ece7105..216f3fe 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "0.0.1", "description": "", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "npx hardhat test", "prettier": "prettier --write contracts/**/*.sol", - "solhint": "solhint 'contracts/**/*.sol'" + "solhint": "solhint 'contracts/**/*.sol'", + "compile": "npx hardhat compile --force" }, "repository": { "type": "git", diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 729bc04..9f69cfc 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -1,70 +1,76 @@ import { getSelectors, FacetCutAction } from './libraries/diamond'; import { ethers } from 'hardhat'; +// Comment to enable console output +console.log = () => {}; + async function deployDiamond() { - const accounts = await ethers.getSigners() - const contractOwner = accounts[0] + 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) + 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) + 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) + 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') + console.log(''); + console.log('Deploying facets'); const FacetNames = [ 'DiamondLoupeFacet', - 'OwnershipFacet' - ] - const cut = [] + 'OwnershipFacet', + 'PlayerFacet', + 'LootboxFacet', + ]; + 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 }`) + 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 + 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' + gallionLabs: accounts[9].address, + guildAdmins: [accounts[2].address] } - ]) - tx = await diamondCut.diamondCut(cut, diamondInit.address, functionCall) - console.log('Diamond cut tx: ', tx.hash) - receipt = await tx.wait() + ]); + 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 }`) + throw Error(`Diamond upgrade failed: ${ tx.hash }`); } - console.log('Completed diamond cut') - return diamond.address + console.log('Completed diamond cut'); + return diamond.address; } // We recommend this pattern to be able to use async/await everywhere @@ -73,9 +79,9 @@ if (require.main === module) { deployDiamond() .then(() => process.exit(0)) .catch(error => { - console.error(error) - process.exit(1) - }) + console.error(error); + process.exit(1); + }); } -exports.deployDiamond = deployDiamond +exports.deployDiamond = deployDiamond; diff --git a/test/PlayerTest.ts b/test/PlayerTest.ts new file mode 100644 index 0000000..bb4873d --- /dev/null +++ b/test/PlayerTest.ts @@ -0,0 +1,60 @@ +import {ethers} from 'hardhat'; +import {Address} from '../types'; +import {DiamondCutFacet, DiamondLoupeFacet, GuildDiamond, OwnershipFacet, PlayerFacet} from '../typechain-types'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; +import {PlayerStructOutput} from '../typechain-types/facets/PlayerFacet'; + +const {deployDiamond} = require('../scripts/deploy.ts'); +const {assert} = require('chai'); + +const Account = { + Owner: 0, + NewOwner: 1, + Admin1: 2, + Admin2: 3, + Player1: 4, + Player2: 5, + Gallion: 9 +}; + +describe('Player Facet test', async function () { + let accounts: SignerWithAddress[] = []; + let diamondAddress: Address; + let guildContract: GuildDiamond; + let diamondCutFacet: DiamondCutFacet; + let diamondLoupeFacet: DiamondLoupeFacet; + let ownershipFacet: OwnershipFacet; + let playerFacet: PlayerFacet; + + before(async function () { + accounts = await ethers.getSigners(); + diamondAddress = await deployDiamond(); + guildContract = (await ethers.getContractAt('GuildDiamond', diamondAddress) as GuildDiamond); + diamondCutFacet = (await ethers.getContractAt('DiamondCutFacet', diamondAddress) as DiamondCutFacet); + diamondLoupeFacet = (await ethers.getContractAt('DiamondLoupeFacet', diamondAddress) as DiamondLoupeFacet); + ownershipFacet = (await ethers.getContractAt('OwnershipFacet', diamondAddress) as OwnershipFacet); + playerFacet = (await ethers.getContractAt('PlayerFacet', diamondAddress) as PlayerFacet); + }); + + it('should add player 1', async () => { + const playerFacet: PlayerFacet = (await ethers.getContractAt('PlayerFacet', diamondAddress) as PlayerFacet); + await playerFacet + .connect(accounts[Account.Admin1]) + .addPlayer(accounts[Account.Player1].address); + const player: PlayerStructOutput = await playerFacet + .connect(accounts[Account.Player1]) + .player(accounts[Account.Player1].address); + assert.exists(player.createdAt); + }); + + it('should levelUp player 1', async () => { + const playerFacet: PlayerFacet = (await ethers.getContractAt('PlayerFacet', diamondAddress) as PlayerFacet); + await playerFacet + .connect(accounts[Account.Gallion]) + .levelUp(accounts[Account.Player1].address); + const player: PlayerStructOutput = await playerFacet + .connect(accounts[Account.Player1]) + .player(accounts[Account.Player1].address); + assert.equal(player.level, 1); + }); +}); diff --git a/test/cacheBugTest.ts b/test/cacheBugTest.ts index 3a42bf2..121f2fa 100644 --- a/test/cacheBugTest.ts +++ b/test/cacheBugTest.ts @@ -1,9 +1,9 @@ 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') +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] @@ -13,23 +13,23 @@ const { assert } = require('chai') describe('Cache bug test', async () => { let diamondLoupeFacet: DiamondLoupeFacet; let playerFacet: PlayerFacet; - const ownerSel = '0x8da5cb5b' + 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 + 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 tx; + let receipt; let selectors = [ sel0, @@ -43,14 +43,14 @@ describe('Cache bug test', async () => { 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() + const diamondAddress = await deployDiamond(); + const diamondCutFacet = await ethers.getContractAt('DiamondCutFacet', diamondAddress); + diamondLoupeFacet = (await ethers.getContractAt('DiamondLoupeFacet', diamondAddress) as DiamondLoupeFacet); + const PlayerFacet = await ethers.getContractFactory('PlayerFacet'); + playerFacet = (await PlayerFacet.deploy() as PlayerFacet); + await playerFacet.deployed(); // add functions tx = await diamondCutFacet.diamondCut([ @@ -59,8 +59,8 @@ describe('Cache bug test', async () => { action: FacetCutAction.Add, functionSelectors: selectors } - ], ethers.constants.AddressZero, '0x', { gasLimit: 800000 }) - receipt = await tx.wait() + ], ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + receipt = await tx.wait(); if (!receipt.status) { throw Error(`Diamond upgrade failed: ${tx.hash}`) } @@ -71,37 +71,37 @@ describe('Cache bug test', async () => { 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() + ], 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) + 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.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') - }) -}) + 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 index 909588a..357831e 100644 --- a/test/diamondTest.ts +++ b/test/diamondTest.ts @@ -1,20 +1,32 @@ -import { ethers } from 'hardhat'; -import { Address } from '../types'; -import { DiamondCutFacet, DiamondLoupeFacet, OwnershipFacet, PlayerFacet } from '../typechain-types'; +import {ethers} from 'hardhat'; +import {Address} from '../types'; +import {DiamondCutFacet, DiamondLoupeFacet, GuildDiamond, OwnershipFacet, Test1Facet} from '../typechain-types'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; const { getSelectors, FacetCutAction, removeSelectors, findAddressPositionInFacets -} = require('../scripts/libraries/diamond.ts') +} = require('../scripts/libraries/diamond.ts'); +const {deployDiamond} = require('../scripts/deploy.ts'); +const {assert} = require('chai'); -const { deployDiamond } = require('../scripts/deploy.ts') +const FacetCount = 5; +const Account = { + Owner: 0, + NewOwner: 1, + Admin1: 2, + Admin2: 3, + Player1: 4, + Player2: 5, + Gallion: 9 +}; -const { assert } = require('chai') - -describe('GuildDiamondTest', async function () { +describe('Diamond test', async function () { + let accounts: SignerWithAddress[] = []; let diamondAddress: Address; + let guildContract: GuildDiamond; let diamondCutFacet: DiamondCutFacet; let diamondLoupeFacet: DiamondLoupeFacet; let ownershipFacet: OwnershipFacet; @@ -24,18 +36,20 @@ describe('GuildDiamondTest', async function () { const addresses: Address[] = []; before(async function () { + accounts = await ethers.getSigners(); diamondAddress = await deployDiamond(); + guildContract = (await ethers.getContractAt('GuildDiamond', diamondAddress) as GuildDiamond); 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 () => { + it(`should have ${FacetCount} facets -- call to facetAddresses function`, async () => { for (const address of await diamondLoupeFacet.facetAddresses()) { addresses.push(address); } - assert.equal(addresses.length, 3); + assert.equal(addresses.length, FacetCount); }); it('facets should have the right function selectors -- call to facetFunctionSelectors function', async () => { @@ -69,140 +83,148 @@ describe('GuildDiamondTest', async function () { ); }); - 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( + it('should transfer ownership to "account 1"', async () => { + const ownershipFacet: OwnershipFacet = (await ethers.getContractAt('OwnershipFacet', diamondAddress)) as OwnershipFacet; + await ownershipFacet.transferOwnership(accounts[Account.NewOwner].address); + assert.equal(await ownershipFacet.owner(), accounts[Account.NewOwner].address); + }); + + it('should add test1 functions', async () => { + const Test1Facet = await ethers.getContractFactory('Test1Facet'); + const test1Facet = await Test1Facet.deploy(); + await test1Facet.deployed(); + addresses.push(test1Facet.address); + const selectors = getSelectors(test1Facet).remove(['supportsInterface(bytes4)']); + tx = await diamondCutFacet.connect(accounts[Account.NewOwner]).diamondCut( [{ - facetAddress: playerFacet.address, + facetAddress: test1Facet.address, action: FacetCutAction.Add, functionSelectors: selectors }], - ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + ethers.constants.AddressZero, '0x', {gasLimit: 800000}); receipt = await tx.wait(); if (!receipt.status) { - throw Error(`Diamond upgrade failed: ${ tx.hash }`); + throw Error(`Diamond upgrade failed: ${tx.hash}`); } - result = await diamondLoupeFacet.facetFunctionSelectors(playerFacet.address); + result = await diamondLoupeFacet.facetFunctionSelectors(test1Facet.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 test function call', async () => { + const test1Facet = await ethers.getContractAt('Test1Facet', diamondAddress); + await test1Facet.test1Func10(); }) 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( + const Test1Facet = await ethers.getContractFactory('Test1Facet'); + const selectors = getSelectors(Test1Facet).get(['supportsInterface(bytes4)']); + const testFacetAddress = addresses[FacetCount]; + result = await diamondLoupeFacet.facetFunctionSelectors(testFacetAddress); + console.log('result: ', result); + tx = await diamondCutFacet.connect(accounts[Account.NewOwner]).diamondCut( [{ facetAddress: testFacetAddress, action: FacetCutAction.Replace, functionSelectors: selectors }], - ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + ethers.constants.AddressZero, '0x', {gasLimit: 800000}); receipt = await tx.wait(); if (!receipt.status) { - throw Error(`Diamond upgrade failed: ${ tx.hash }`); + throw Error(`Diamond upgrade failed: ${tx.hash}`); } result = await diamondLoupeFacet.facetFunctionSelectors(testFacetAddress); - assert.sameMembers(result, getSelectors(PlayerFacet)); - }) + assert.sameMembers(result, getSelectors(Test1Facet)); + }); - 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( + it('should add test2 functions', async () => { + const Test2Facet = await ethers.getContractFactory('Test2Facet'); + const test2Facet = await Test2Facet.deploy(); + await test2Facet.deployed(); + addresses.push(test2Facet.address); + const selectors = getSelectors(test2Facet); + tx = await diamondCutFacet.connect(accounts[Account.NewOwner]).diamondCut( [{ - facetAddress: lootboxFacet.address, + facetAddress: test2Facet.address, action: FacetCutAction.Add, functionSelectors: selectors }], - ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + ethers.constants.AddressZero, '0x', {gasLimit: 800000}); receipt = await tx.wait(); if (!receipt.status) { - throw Error(`Diamond upgrade failed: ${ tx.hash }`); + throw Error(`Diamond upgrade failed: ${tx.hash}`); } - result = await diamondLoupeFacet.facetFunctionSelectors(lootboxFacet.address); + result = await diamondLoupeFacet.facetFunctionSelectors(test2Facet.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( + it('should remove some test2 functions', async () => { + const test2Facet = await ethers.getContractAt('Test2Facet', diamondAddress); + const functionsToKeep = ['test2Func1()', 'test2Func5()', 'test2Func6()', 'test2Func19()', 'test2Func20()']; + const selectors = getSelectors(test2Facet).remove(functionsToKeep); + tx = await diamondCutFacet.connect(accounts[Account.NewOwner]).diamondCut( [{ facetAddress: ethers.constants.AddressZero, action: FacetCutAction.Remove, functionSelectors: selectors }], - ethers.constants.AddressZero, '0x', { gasLimit: 800000 }); + ethers.constants.AddressZero, '0x', {gasLimit: 800000}); receipt = await tx.wait(); if (!receipt.status) { - throw Error(`Diamond upgrade failed: ${ tx.hash }`); + throw Error(`Diamond upgrade failed: ${tx.hash}`); } - result = await diamondLoupeFacet.facetFunctionSelectors(addresses[4]); - assert.sameMembers(result, getSelectors(lootboxFacet).get(functionsToKeep)); - }) + result = await diamondLoupeFacet.facetFunctionSelectors(addresses[FacetCount + 1]); + assert.sameMembers(result, getSelectors(test2Facet).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( + it('should remove some test1 functions', async () => { + const test1Facet = await ethers.getContractAt('Test1Facet', diamondAddress); + const functionsToKeep = ['test1Func2()', 'test1Func11()', 'test1Func12()']; + const selectors = getSelectors(test1Facet).remove(functionsToKeep); + tx = await diamondCutFacet.connect(accounts[Account.NewOwner]).diamondCut( [{ facetAddress: ethers.constants.AddressZero, action: FacetCutAction.Remove, functionSelectors: selectors }], - ethers.constants.AddressZero, '0x', { gasLimit: 800000 }) - receipt = await tx.wait() + ethers.constants.AddressZero, '0x', {gasLimit: 800000}); + receipt = await tx.wait(); if (!receipt.status) { - throw Error(`Diamond upgrade failed: ${ tx.hash }`) + throw Error(`Diamond upgrade failed: ${tx.hash}`); } - result = await diamondLoupeFacet.facetFunctionSelectors(addresses[3]) - assert.sameMembers(result, getSelectors(playerFacet).get(functionsToKeep)) - })*/ + result = await diamondLoupeFacet.facetFunctionSelectors(addresses[FacetCount]); + assert.sameMembers(result, getSelectors(test1Facet).get(functionsToKeep)); + }); it('remove all functions and facets accept \'diamondCut\' and \'facets\'', async () => { - let selectors = [] - let facets = await diamondLoupeFacet.facets() + let selectors = []; + let facets = await diamondLoupeFacet.facets(); for (let i = 0; i < facets.length; i++) { - selectors.push(...facets[i].functionSelectors) + selectors.push(...facets[i].functionSelectors); } - selectors = removeSelectors(selectors, ['facets()', 'diamondCut(tuple(address,uint8,bytes4[])[],address,bytes)']) - tx = await diamondCutFacet.diamondCut( + selectors = removeSelectors(selectors, ['facets()', 'diamondCut(tuple(address,uint8,bytes4[])[],address,bytes)']); + tx = await diamondCutFacet.connect(accounts[Account.NewOwner]).diamondCut( [{ facetAddress: ethers.constants.AddressZero, action: FacetCutAction.Remove, functionSelectors: selectors }], - ethers.constants.AddressZero, '0x', { gasLimit: 800000 }) - receipt = await tx.wait() + ethers.constants.AddressZero, '0x', {gasLimit: 800000}); + receipt = await tx.wait(); if (!receipt.status) { - throw Error(`Diamond upgrade failed: ${ tx.hash }`) + 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']) - }) + 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') + const diamondLoupeFacetSelectors = getSelectors(diamondLoupeFacet).remove(['supportsInterface(bytes4)']); + const Test1Facet = await ethers.getContractFactory('Test1Facet'); + const Test2Facet = await ethers.getContractFactory('Test2Facet'); // Any number of functions from any number of facets can be added/replaced/removed in a // single transaction const cut = [ @@ -217,35 +239,35 @@ describe('GuildDiamondTest', async function () { functionSelectors: getSelectors(ownershipFacet) }, { - facetAddress: addresses[3], + facetAddress: addresses[FacetCount], action: FacetCutAction.Add, - functionSelectors: getSelectors(PlayerFacet) + functionSelectors: getSelectors(Test1Facet) }, { - facetAddress: addresses[4], + facetAddress: addresses[FacetCount + 1], action: FacetCutAction.Add, - functionSelectors: getSelectors(LootboxFacet) + functionSelectors: getSelectors(Test2Facet) } - ] - tx = await diamondCutFacet.diamondCut(cut, ethers.constants.AddressZero, '0x', { gasLimit: 8000000 }) - receipt = await tx.wait() + ]; + tx = await diamondCutFacet.connect(accounts[Account.NewOwner]).diamondCut(cut, ethers.constants.AddressZero, '0x', {gasLimit: 8000000}); + receipt = await tx.wait(); if (!receipt.status) { - throw Error(`Diamond upgrade failed: ${ tx.hash }`) + 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)) - }) -}) + const facets = await diamondLoupeFacet.facets(); + const facetAddresses = await diamondLoupeFacet.facetAddresses(); + assert.equal(facetAddresses.length, FacetCount); + assert.equal(facets.length, FacetCount); + assert.includeMembers(addresses, facetAddresses); + 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[FacetCount], facets)][1], getSelectors(Test1Facet)); + assert.sameMembers(facets[findAddressPositionInFacets(addresses[FacetCount + 1], facets)][1], getSelectors(Test2Facet)); + }); +});