diff --git a/README.md b/README.md index 38dd936..c241cbc 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,12 @@ Deploy NetworkRegistry using a UUPS Upgradeable Proxy $ pnpm hardhat --network deploy --tags UpgradeablePGNetworkRegistry ``` +Deploy GuildRegistry using a UUPS Upgradeable Proxy + +```sh +$ pnpm hardhat --network deploy --tags UpgradeablePGuildRegistry +``` + ### Verify contracts ```sh diff --git a/contracts/GuildRegistry.sol b/contracts/GuildRegistry.sol new file mode 100644 index 0000000..6c4ea0d --- /dev/null +++ b/contracts/GuildRegistry.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import { ISplitMain } from "./interfaces/ISplitMain.sol"; +import { ISplitManager } from "./interfaces/ISplitManager.sol"; +import { DataTypes } from "./libraries/DataTypes.sol"; +import { PGContribCalculator } from "./libraries/PGContribCalculator.sol"; +import { IMemberRegistry, MemberRegistry } from "./registry/MemberRegistry.sol"; +import { + Registry__ParamsSizeMismatch, + Registry__UnauthorizedToUpgrade, + Split__ControlNotHandedOver, + Split_InvalidAddress, + Split__InvalidOrImmutable +} from "./utils/Errors.sol"; + +/** + * @title A guild registry to distribute funds escrowed in 0xSplit based on member activity + * @author DAOHaus + * @notice Manage a time-weighted member registry to distribute funds hold in 0xSplit based on member activity + * @dev Features and important things to consider: + * - There are methods for adding/updating members, update registry activity & split funds + * based on a time-weighted formula. + * - Funds are escrowed in a 0xSplit contract so in order to split funds the GuildRegistry contract must be set + * as the controller. + * - A main GuildRegistry should be owned by the community (i.e. Safe or a DAO), + */ +contract GuildRegistry is ISplitManager, UUPSUpgradeable, OwnableUpgradeable, MemberRegistry { + using PGContribCalculator for DataTypes.Members; + + /// @notice 0xSplit proxy contract + /// @dev 0xSplitMain contract + ISplitMain public splitMain; + /// @notice 0xSplit contract where funds are hold + /// @dev 0xSplitWallet contract + address public split; + + /** + * EVENTS + */ + + /** + * @notice emitted when the 0xSplit contract is updated + * @param _splitMain new 0xSplitMain contract address + * @param _split new 0xSplitWallet contract address + */ + event SplitUpdated(address _splitMain, address _split); + /** + * @notice emitted when a new split distribution is registered on the 0xSplit contract + * @param _split 0xSplit contract address + * @param _splitHash hash of the split distribution parameters + * @param _splitDistributorFee split fee set at reward for the address that executes the distribution + */ + event SplitsDistributionUpdated(address _split, bytes32 _splitHash, uint32 _splitDistributorFee); + + constructor() { + // disable initialization on singleton contract + _disableInitializers(); + } + + /** + * @dev Setup the 0xSplit contracts settings. + * @param _splitMain 0xSplit proxy contract + * @param _split 0xSplit contract address + */ + // solhint-disable-next-line func-name-mixedcase + function __GuildRegistry_init_unchained(address _splitMain, address _split) internal onlyInitializing { + splitMain = ISplitMain(_splitMain); + split = _split; + } + + /** + * @dev Executes initializers from parent contracts + * @param _splitMain 0xSplit proxy contract + * @param _split 0xSplit contract address + * @param _owner Account address that will own the registry contract + */ + // solhint-disable-next-line func-name-mixedcase + function __GuildRegistry_init(address _splitMain, address _split, address _owner) internal onlyInitializing { + if (_splitMain == address(0) || _split == address(0)) revert Split_InvalidAddress(); + __UUPSUpgradeable_init(); + __Ownable_init(_owner); + __MemberRegistry_init(); + __GuildRegistry_init_unchained(_splitMain, _split); + } + + /** + * @notice Initializes the registry contract + * @dev Initialization parameters are abi-encoded + * @param _initializationParams abi-encoded parameters + */ + function initialize(bytes memory _initializationParams) external virtual initializer { + (address _splitMain, address _split, address _owner) = abi.decode( + _initializationParams, + (address, address, address) + ); + __GuildRegistry_init(_splitMain, _split, _owner); + } + + /** + * @notice Adds a new set of members to the registry + * @dev _activityMultipliers values must be > 0 + * @inheritdoc IMemberRegistry + */ + function batchNewMembers( + address[] memory _members, + uint32[] memory _activityMultipliers, + uint32[] memory _startDates + ) external onlyOwner { + _batchNewMembers(_members, _activityMultipliers, _startDates); + } + + /** + * @notice Updates the activity multiplier for a set of existing members + * @dev If a member's activityMultiplier is zero, the record is automatically removed from the registry. + * @inheritdoc IMemberRegistry + */ + function batchUpdateMembersActivity( + address[] memory _members, + uint32[] memory _activityMultipliers + ) external onlyOwner { + uint256 batchSize = _members.length; + if (_activityMultipliers.length != batchSize) revert Registry__ParamsSizeMismatch(); + for (uint256 i = 0; i < batchSize; ++i) { + if (_activityMultipliers[i] > 0) _updateMemberActivity(_members[i], _activityMultipliers[i]); + else _removeMember(_members[i]); + } + } + + /** + * @notice Remove a set of members from the registry + * @inheritdoc IMemberRegistry + */ + function batchRemoveMembers(address[] memory _members) external onlyOwner { + _batchRemoveMembers(_members); + } + + /** + * @dev Updates registry activity since the last update epoch. Overrides MemberRegistry implementation + * to check whether if _cutoffDate is zero its value will be overridden with the current block.timestamp + */ + function _updateSecondsActive(uint32 _cutoffDate) internal override(MemberRegistry) { + if (_cutoffDate == 0) _cutoffDate = uint32(block.timestamp); + super._updateSecondsActive(_cutoffDate); + } + + /** + * @notice Updates seconds active since the last update epoch for every member in the registry. + * If _cutoffDate is zero its value will be overridden with the current block.timestamp + * @inheritdoc IMemberRegistry + */ + function updateSecondsActive(uint32 _cutoffDate) external { + _updateSecondsActive(_cutoffDate); + } + + /** + * @notice Updates the 0xSplit distribution based on member activity during the last epoch + * @param _sortedList sorted list (ascending order) of members to be considered in the 0xSplit distribution + * @param _splitDistributorFee split fee set as reward for the address that executes the distribution + */ + function _updateSplitDistribution(address[] memory _sortedList, uint32 _splitDistributorFee) internal { + (address[] memory _receivers, uint32[] memory _percentAllocations) = calculate(_sortedList); + splitMain.updateSplit(split, _receivers, _percentAllocations, _splitDistributorFee); + bytes32 splitHash = keccak256(abi.encodePacked(_receivers, _percentAllocations, _splitDistributorFee)); + emit SplitsDistributionUpdated(split, splitHash, _splitDistributorFee); + } + + /** + * @notice Updates the 0xSplit distribution based on member activity during the last epoch + * Consider calling {updateSecondsActive} prior triggering a 0xSplit distribution update + * @inheritdoc ISplitManager + */ + function updateSplits(address[] memory _sortedList, uint32 _splitDistributorFee) external { + _updateSplitDistribution(_sortedList, _splitDistributorFee); + } + + /** + * @notice Executes both {updateSecondsActive} to update registry member's activity and {updateSplits} + * for split distribution. If _cutoffDate is zero its value will be overridden with the current block.timestamp + * @inheritdoc ISplitManager + */ + function updateAll(uint32 _cutoffDate, address[] memory _sortedList, uint32 _splitDistributorFee) external { + _updateSecondsActive(_cutoffDate); + _updateSplitDistribution(_sortedList, _splitDistributorFee); + } + + /** + * @notice Calculate 0xSplit distribution allocations + * @dev It uses the PGContribCalculator library to calculate member allocations + * @inheritdoc ISplitManager + */ + function calculate( + address[] memory _sortedList + ) public view virtual returns (address[] memory _receivers, uint32[] memory _percentAllocations) { + (_receivers, _percentAllocations) = members.calculate(_sortedList); + } + + /** + * @notice Calculates a member individual contribution + * @dev It uses the PGContribCalculator library + * @inheritdoc ISplitManager + */ + function calculateContributionOf(address _memberAddress) external view returns (uint256) { + DataTypes.Member memory member = getMember(_memberAddress); + return members.calculateContributionOf(member); + } + + /** + * @notice Calculates the sum of all member contributions + * @dev omit members with activityMultiplier == 0 + * @inheritdoc ISplitManager + */ + function calculateTotalContributions() external view returns (uint256 total) { + uint256 totalRegistryMembers = totalMembers(); + for (uint256 i = 0; i < totalRegistryMembers; ++i) { + DataTypes.Member memory member = _getMemberByIndex(i); + total += members.calculateContributionOf(member); + } + } + + /** + * @notice Updates the the 0xSplitMain proxy and 0xSplit contract addresses + * @dev Callable on both main and replica registries + * @inheritdoc ISplitManager + */ + function setSplit(address _splitMain, address _split) external onlyOwner { + splitMain = ISplitMain(_splitMain); + address currentController = splitMain.getController(_split); + if (currentController == address(0)) revert Split__InvalidOrImmutable(); + address newController = splitMain.getNewPotentialController(_split); + if (currentController != address(this) && newController != address(this)) revert Split__ControlNotHandedOver(); + split = _split; + emit SplitUpdated(_splitMain, split); + acceptSplitControl(); + } + + /** + * @notice Transfer control of the current 0xSplit contract to `_newController` + * @dev Callable on both main and replica registries + * @inheritdoc ISplitManager + */ + function transferSplitControl(address _newController) external onlyOwner { + splitMain.transferControl(split, _newController); + } + + /** + * @notice Accepts control of the current 0xSplit contract + * @dev Callable on both main and replica registries + * @inheritdoc ISplitManager + */ + function acceptSplitControl() public onlyOwner { + splitMain.acceptControl(split); + } + + /** + * @notice Cancel controller transfer of the current 0xSplit contract + * @dev Callable on both main and replica registries + * @inheritdoc ISplitManager + */ + function cancelSplitControlTransfer() external onlyOwner { + splitMain.cancelControlTransfer(split); + } + + /** + * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. + */ + function _authorizeUpgrade(address /*newImplementation*/) internal view override { + if (_msgSender() != owner()) revert Registry__UnauthorizedToUpgrade(); + } + + // solhint-disable-next-line state-visibility, var-name-mixedcase + uint256[49] __gap_gr; +} diff --git a/contracts/mocks/GuildRegistryHarness.sol b/contracts/mocks/GuildRegistryHarness.sol new file mode 100644 index 0000000..f8feb66 --- /dev/null +++ b/contracts/mocks/GuildRegistryHarness.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { GuildRegistry } from "../GuildRegistry.sol"; + +contract GuildRegistryHarness is GuildRegistry { + function exposed__GuildRegistry_init_unchained(address _splitMain, address _split) external { + super.__GuildRegistry_init_unchained(_splitMain, _split); + } + + function exposed__GuildRegistry_init(address _splitMain, address _split, address _owner) external { + super.__GuildRegistry_init(_splitMain, _split, _owner); + } + + function exposed__MemberRegistry_init_unchained() external { + super.___MemberRegistry_init_unchained(); + } + + function exposed__MemberRegistry_init() external { + super.__MemberRegistry_init(); + } +} diff --git a/contracts/mocks/GuildRegistryV2Mock.sol b/contracts/mocks/GuildRegistryV2Mock.sol new file mode 100644 index 0000000..2f70a23 --- /dev/null +++ b/contracts/mocks/GuildRegistryV2Mock.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { IConnext } from "@connext/interfaces/core/IConnext.sol"; + +import { ISplitMain } from "../interfaces/ISplitMain.sol"; +import { GuildRegistry } from "../GuildRegistry.sol"; + +contract GuildRegistryV2Mock is GuildRegistry { + function initialize(bytes memory _initializationParams) external virtual override reinitializer(2) { + (address _splitMain, address _split, ) = abi.decode(_initializationParams, (address, address, address)); + splitMain = ISplitMain(_splitMain); + split = _split; + } +} diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index 12385ca..72a41a0 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -40,6 +40,8 @@ error MemberRegistry__StartDateInTheFuture(address _memberAddress, uint32 _start * 0xSplit related errors */ +/// @notice Invalid 0xSplit contract addresses +error Split_InvalidAddress(); /// @notice Control of 0xSplit contract hasn't been transferred to the registry error Split__ControlNotHandedOver(); /// @notice 0xSplit doesn't exists or is immutable diff --git a/deploy/003_deploy_PGRegistry_UUPS.ts b/deploy/003_deploy_PGRegistry_UUPS.ts index 49a5158..1c0f170 100644 --- a/deploy/003_deploy_PGRegistry_UUPS.ts +++ b/deploy/003_deploy_PGRegistry_UUPS.ts @@ -16,71 +16,60 @@ const deployFn: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { // uncomment if you get gas-related errors and need current network fee data to update params // console.log("Feedata", await ethers.provider.getFeeData()); - if (Object.keys(deploymentConfig).includes(chainId)) { - const networkConfig = deploymentConfig[chainId]; - const parentChainId = companionNetworks.l1 && (await companionNetworks.l1.getChainId()); - console.log("Is L2?", networkConfig.l2, parentChainId); + const networkConfig = deploymentConfig[chainId]; - console.log("networkConfig", networkConfig); + console.log("networkConfig", networkConfig); - let safeAddress = networkConfig.safe; + let safeAddress = networkConfig.safe; if (networkConfig.moloch && !networkConfig.safe) { const baal = (await ethers.getContractAt("Baal", networkConfig.moloch, signer)) as Baal; safeAddress = await baal.avatar(); } - const owner = networkConfig.l2 - ? networkConfig.registryOwner || ethers.constants.AddressZero - : safeAddress || deployer; + const owner = safeAddress || deployer; - console.log("Registry will be owned by", owner, "Is L2?", networkConfig.l2, "Is Safe?", owner === safeAddress); + console.log("Registry will be owned by", owner, "Is Safe?", owner === safeAddress); - const initializationParams = ethers.utils.defaultAbiCoder.encode( - ["address", "uint32", "address", "address", "address", "address"], - [ - networkConfig.connext, - networkConfig.l2 ? deploymentConfig[parentChainId].domainId : 0, - networkConfig.l2 ? deploymentConfig[parentChainId].pgRegistry : ethers.constants.AddressZero, - networkConfig.splitMain, - networkConfig.split, - owner, - ], - ); + const initializationParams = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "address"], + [ + networkConfig.splitMain, + networkConfig.split, + owner, + ], + ); - const calculatorLibraryDeployed = await deploy("PGContribCalculator", { - contract: "PGContribCalculator", - from: deployer, - args: [], - log: true, - }); + const calculatorLibraryDeployed = await deploy("PGContribCalculator", { + contract: "PGContribCalculator", + from: deployer, + args: [], + log: true, + }); - const registryDeployed = await deploy("NetworkRegistry", { - contract: "NetworkRegistry", - from: deployer, - args: [], - libraries: { - PGContribCalculator: calculatorLibraryDeployed.address, - }, - proxy: { - execute: { - init: { - methodName: "initialize", - args: [initializationParams], - }, + const registryDeployed = await deploy("GuildRegistry", { + contract: "GuildRegistry", + from: deployer, + args: [], + libraries: { + PGContribCalculator: calculatorLibraryDeployed.address, + }, + proxy: { + execute: { + init: { + methodName: "initialize", + args: [initializationParams], }, - owner, - proxyContract: "ERC1967Proxy", - proxyArgs: ["{implementation}", "{data}"], }, - log: true, - }); - const registryAddress = registryDeployed.address; + owner, + proxyContract: "ERC1967Proxy", + proxyArgs: ["{implementation}", "{data}"], + }, + log: true, + }); + const registryAddress = registryDeployed.address; - console.log(`PG NetworkRegistry deployed on ${network.name} chain at ${registryAddress}`); + console.log(`PG GuildRegistry deployed on ${network.name} chain at ${registryAddress}`); - return; - } - console.error("PGRegistry: Not supported Network!"); }; export default deployFn; -deployFn.tags = ["NetworkRegistry", "UpgradeablePGNetworkRegistry"]; +deployFn.tags = ["GuildRegistry", "UpgradeablePGuildRegistry"]; diff --git a/deploy/004_deploy_PGNetworkRegistry_UUPS.ts b/deploy/004_deploy_PGNetworkRegistry_UUPS.ts new file mode 100644 index 0000000..49a5158 --- /dev/null +++ b/deploy/004_deploy_PGNetworkRegistry_UUPS.ts @@ -0,0 +1,86 @@ +import { Baal } from "@daohaus/baal-contracts"; +// import { ethers } from "hardhat"; +import { DeployFunction } from "hardhat-deploy/types"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +import { deploymentConfig } from "../constants"; + +const deployFn: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { companionNetworks, deployments, ethers, getChainId, getNamedAccounts, network } = hre; + const { deployer } = await getNamedAccounts(); + const signer = await ethers.getSigner(deployer); + const chainId = network.name === "hardhat" ? "5" : await getChainId(); // hardhat -> Forking mode + + const { deploy } = deployments; + + // uncomment if you get gas-related errors and need current network fee data to update params + // console.log("Feedata", await ethers.provider.getFeeData()); + + if (Object.keys(deploymentConfig).includes(chainId)) { + const networkConfig = deploymentConfig[chainId]; + const parentChainId = companionNetworks.l1 && (await companionNetworks.l1.getChainId()); + console.log("Is L2?", networkConfig.l2, parentChainId); + + console.log("networkConfig", networkConfig); + + let safeAddress = networkConfig.safe; + if (networkConfig.moloch && !networkConfig.safe) { + const baal = (await ethers.getContractAt("Baal", networkConfig.moloch, signer)) as Baal; + safeAddress = await baal.avatar(); + } + const owner = networkConfig.l2 + ? networkConfig.registryOwner || ethers.constants.AddressZero + : safeAddress || deployer; + + console.log("Registry will be owned by", owner, "Is L2?", networkConfig.l2, "Is Safe?", owner === safeAddress); + + const initializationParams = ethers.utils.defaultAbiCoder.encode( + ["address", "uint32", "address", "address", "address", "address"], + [ + networkConfig.connext, + networkConfig.l2 ? deploymentConfig[parentChainId].domainId : 0, + networkConfig.l2 ? deploymentConfig[parentChainId].pgRegistry : ethers.constants.AddressZero, + networkConfig.splitMain, + networkConfig.split, + owner, + ], + ); + + const calculatorLibraryDeployed = await deploy("PGContribCalculator", { + contract: "PGContribCalculator", + from: deployer, + args: [], + log: true, + }); + + const registryDeployed = await deploy("NetworkRegistry", { + contract: "NetworkRegistry", + from: deployer, + args: [], + libraries: { + PGContribCalculator: calculatorLibraryDeployed.address, + }, + proxy: { + execute: { + init: { + methodName: "initialize", + args: [initializationParams], + }, + }, + owner, + proxyContract: "ERC1967Proxy", + proxyArgs: ["{implementation}", "{data}"], + }, + log: true, + }); + const registryAddress = registryDeployed.address; + + console.log(`PG NetworkRegistry deployed on ${network.name} chain at ${registryAddress}`); + + return; + } + console.error("PGRegistry: Not supported Network!"); +}; + +export default deployFn; +deployFn.tags = ["NetworkRegistry", "UpgradeablePGNetworkRegistry"]; diff --git a/test/guildRegistry/GuildRegistry.ts b/test/guildRegistry/GuildRegistry.ts new file mode 100644 index 0000000..80a29cc --- /dev/null +++ b/test/guildRegistry/GuildRegistry.ts @@ -0,0 +1,1261 @@ +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { BigNumber } from "ethers"; +import { deployments, ethers, getNamedAccounts, getUnnamedAccounts } from "hardhat"; + +import { PERCENTAGE_SCALE } from "../../constants"; +import { GuildRegistry, GuildRegistryHarness, GuildRegistryV2Mock, PGContribCalculator, SplitMain } from "../../types"; +import { User, registryFixture } from "../networkRegistry/NetworkRegistry.fixture"; +import { Member } from "../types"; +import { deploySplit, generateMemberBatch, hashSplit, summonGuildRegistryProxy } from "../utils"; + +describe("GuildRegistry", function () { + let l1CalculatorLibrary: PGContribCalculator; + let l1SplitMain: SplitMain; + let l1SplitAddress: string; + let users: { [key: string]: User }; + let members: Array; + const splitConfig = { + percentAllocations: [400_000, 300_000, 300_000], + distributorFee: 0, + }; + + let guildRegistry: GuildRegistry; + + beforeEach(async function () { + const setup = await registryFixture({}); + l1CalculatorLibrary = setup.calculatorLibrary; + l1SplitMain = setup.splitMain; + users = setup.users; + + const signer = await ethers.getSigner(users.owner.address); + const accounts = await getUnnamedAccounts(); + members = accounts + .slice(0, splitConfig.percentAllocations.length) + .sort((a: string, b: string) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1)); + + // Deploy Split on L1 + l1SplitAddress = await deploySplit( + l1SplitMain, + members, + splitConfig.percentAllocations, + splitConfig.distributorFee, + users.owner.address, + ); + + // Summon Registry + const registryAddress = await summonGuildRegistryProxy( + l1CalculatorLibrary.address, + { + splitMain: l1SplitMain.address, + split: l1SplitAddress, + owner: users.owner.address, + }, + "GuildRegistry", + ); + guildRegistry = (await ethers.getContractAt("GuildRegistry", registryAddress, signer)) as GuildRegistry; + + // Transfer Split control to GuildRegistry + const tx_controller_l1 = await l1SplitMain.transferControl(l1SplitAddress, registryAddress); + await tx_controller_l1.wait(); + await guildRegistry.acceptSplitControl(); + }); + + // ############################################################################################################## + // ################################## ###################################################### + // ################################## GuildRegistry Config ###################################################### + // ################################## ###################################################### + // ############################################################################################################## + // ############################################################################################################## + // ############################################################################################################## + + describe("GuildRegistry Config", function () { + it("Should be not be able to initialize proxy with wrong parameters", async () => { + const { deployer } = await getNamedAccounts(); + let initializationParams = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "address"], + [ + ethers.constants.AddressZero, // splitMain address + l1SplitAddress, // split address + ethers.constants.AddressZero, // owner + ], + ); + + await expect( + deployments.deploy("Guild Registry", { + contract: "GuildRegistry", + from: deployer, + args: [], + libraries: { + PGContribCalculator: l1CalculatorLibrary.address, + }, + proxy: { + execute: { + methodName: "initialize", + args: [initializationParams], + }, + proxyContract: "ERC1967Proxy", + proxyArgs: ["{implementation}", "{data}"], + }, + log: true, + }), + ).to.be.revertedWithCustomError(guildRegistry, "Split_InvalidAddress"); + + initializationParams = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "address"], + [ + l1SplitMain.address, // splitMain address + ethers.constants.AddressZero, // split address + ethers.constants.AddressZero, // owner + ], + ); + + await expect( + deployments.deploy("Guild Registry", { + contract: "GuildRegistry", + from: deployer, + args: [], + libraries: { + PGContribCalculator: l1CalculatorLibrary.address, + }, + proxy: { + execute: { + methodName: "initialize", + args: [initializationParams], + }, + proxyContract: "ERC1967Proxy", + proxyArgs: ["{implementation}", "{data}"], + }, + log: true, + }), + ).to.be.revertedWithCustomError(guildRegistry, "Split_InvalidAddress"); + + initializationParams = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "address"], + [ + l1SplitMain.address, // splitMain address + l1SplitAddress, // split address + ethers.constants.AddressZero, // owner address + ], + ); + + await expect( + deployments.deploy("Guild Registry", { + contract: "GuildRegistry", + from: deployer, + args: [], + libraries: { + PGContribCalculator: l1CalculatorLibrary.address, + }, + proxy: { + execute: { + methodName: "initialize", + args: [initializationParams], + }, + proxyContract: "ERC1967Proxy", + proxyArgs: ["{implementation}", "{data}"], + }, + log: true, + }), + ) + .to.be.revertedWithCustomError(guildRegistry, "OwnableInvalidOwner") + .withArgs(ethers.constants.AddressZero); + }); + + it("Should not be able to initialize the implementation contract", async () => { + const signer = await ethers.getSigner(users.owner.address); + const l1InitializationParams = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "address"], + [l1SplitMain.address, l1SplitAddress, users.owner.address], + ); + const implSlot = BigNumber.from("0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + const slotValue = await ethers.provider.getStorageAt(guildRegistry.address, implSlot); + const implementationAddress = `0x${slotValue.substring(26, 66)}`; + const implementation = (await ethers.getContractAt( + "GuildRegistry", + implementationAddress, + signer, + )) as GuildRegistry; + await expect(implementation.initialize(l1InitializationParams)).to.be.revertedWithCustomError( + implementation, + "InvalidInitialization", + ); + }); + + it("Should not be able to call init functions if contract is not initializing", async () => { + const { deployer } = await getNamedAccounts(); + const signer = await ethers.getSigner(deployer); + const implDeployed = await deployments.deploy("GuildRegistryHarness", { + contract: "GuildRegistryHarness", + from: deployer, + args: [], + libraries: { + PGContribCalculator: l1CalculatorLibrary.address, + }, + log: true, + }); + const registry = (await ethers.getContractAt( + "GuildRegistryHarness", + implDeployed.address, + signer, + )) as GuildRegistryHarness; + + await expect(registry.exposed__MemberRegistry_init_unchained()).to.be.revertedWithCustomError( + registry, + "NotInitializing", + ); + + await expect(registry.exposed__MemberRegistry_init()).to.be.revertedWithCustomError(registry, "NotInitializing"); + + await expect( + registry.exposed__GuildRegistry_init_unchained(ethers.constants.AddressZero, ethers.constants.AddressZero), + ).to.be.revertedWithCustomError(registry, "NotInitializing"); + + await expect( + registry.exposed__GuildRegistry_init( + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ), + ).to.be.revertedWithCustomError(registry, "NotInitializing"); + }); + + it("Should have owner on L1", async () => { + expect(await guildRegistry.owner()).to.equal(users.owner.address); + }); + + it("Should not be able to transferOwnership to zero address", async () => { + await expect(guildRegistry.transferOwnership(ethers.constants.AddressZero)).to.revertedWithCustomError( + guildRegistry, + "OwnableInvalidOwner", + ); + }); + + it("Should not be able to call config methods if not owner", async () => { + const signer = await ethers.getSigner(users.applicant.address); + const applicantRegistry = guildRegistry.connect(signer); + + await expect(applicantRegistry.setSplit(l1SplitMain.address, l1SplitAddress)).to.be.revertedWithCustomError( + guildRegistry, + "OwnableUnauthorizedAccount", + ); + await expect(applicantRegistry.transferSplitControl(users.applicant.address)).to.be.revertedWithCustomError( + guildRegistry, + "OwnableUnauthorizedAccount", + ); + await expect(applicantRegistry.acceptSplitControl()).to.be.revertedWithCustomError( + guildRegistry, + "OwnableUnauthorizedAccount", + ); + await expect(applicantRegistry.cancelSplitControlTransfer()).to.be.revertedWithCustomError( + guildRegistry, + "OwnableUnauthorizedAccount", + ); + }); + + it("Should control the 0xSplit contract", async () => { + const signer = await ethers.getSigner(users.owner.address); + const l1SplitMainAddress = await guildRegistry.splitMain(); + const l1SplitAddress = await guildRegistry.split(); + const splitMain = (await ethers.getContractAt("SplitMain", l1SplitMainAddress, signer)) as SplitMain; + expect(await splitMain.getController(l1SplitAddress)).to.equal(guildRegistry.address); + }); + + it("Should not be able to set a non-existent 0xSplit contract", async () => { + const dummySplitAddress = users.applicant.address; + await expect(guildRegistry.setSplit(l1SplitMain.address, dummySplitAddress)).to.be.revertedWithCustomError( + guildRegistry, + "Split__InvalidOrImmutable", + ); + + const newSplitAddress = await deploySplit( + l1SplitMain, + members, + splitConfig.percentAllocations, + splitConfig.distributorFee, + ethers.constants.AddressZero, // immutable + ); + await expect(guildRegistry.setSplit(l1SplitMain.address, newSplitAddress)).to.be.revertedWithCustomError( + guildRegistry, + "Split__InvalidOrImmutable", + ); + }); + + it("Should not be able to update 0xSplit contract if control is not handed over first", async () => { + const newSplitAddress = await deploySplit( + l1SplitMain, + members, + splitConfig.percentAllocations, + splitConfig.distributorFee, + users.owner.address, + ); + + await expect(guildRegistry.setSplit(l1SplitMain.address, newSplitAddress)).to.be.revertedWithCustomError( + guildRegistry, + "Split__ControlNotHandedOver", + ); + }); + + it("Should be able to update 0xSplit contract and get control over it", async () => { + const newSplitAddress = await deploySplit( + l1SplitMain, + members, + splitConfig.percentAllocations, + splitConfig.distributorFee, + users.owner.address, + ); + const txTransfer = await l1SplitMain.transferControl(newSplitAddress, guildRegistry.address); + await txTransfer.wait(); + + const tx = await guildRegistry.setSplit(l1SplitMain.address, newSplitAddress); + + await expect(tx).to.emit(guildRegistry, "SplitUpdated").withArgs(l1SplitMain.address, newSplitAddress); + await expect(tx) + .to.emit(l1SplitMain, "ControlTransfer") + .withArgs(newSplitAddress, users.owner.address, guildRegistry.address); + }); + + it("Should be able to transfer 0xSplit control", async () => { + const newController = users.applicant.address; + const tx = await guildRegistry.transferSplitControl(newController); + await tx.wait(); + expect(await l1SplitMain.getNewPotentialController(await guildRegistry.split())).to.equal(newController); + }); + + it("Should be able to accept 0xSplit control", async () => { + const signer = await ethers.getSigner(users.owner.address); + const newL1SplitAddress = await deploySplit( + l1SplitMain, + members, + splitConfig.percentAllocations, + splitConfig.distributorFee, + users.owner.address, + ); + const l1RegistryAddress = await summonGuildRegistryProxy( + l1CalculatorLibrary.address, + { + splitMain: l1SplitMain.address, + split: newL1SplitAddress, + owner: users.owner.address, + }, + "Guild Registry", + ); + const txTransfer = await l1SplitMain.transferControl(newL1SplitAddress, l1RegistryAddress); + await expect(txTransfer) + .to.emit(l1SplitMain, "InitiateControlTransfer") + .withArgs(newL1SplitAddress, l1RegistryAddress); + const registry = (await ethers.getContractAt("GuildRegistry", l1RegistryAddress, signer)) as GuildRegistry; + const tx = await registry.acceptSplitControl(); + await expect(tx) + .to.emit(l1SplitMain, "ControlTransfer") + .withArgs(newL1SplitAddress, users.owner.address, l1RegistryAddress); + }); + + it("Should be able to cancel 0xSplit control", async () => { + const signer = await ethers.getSigner(users.owner.address); + const newL1SplitAddress = await deploySplit( + l1SplitMain, + members, + splitConfig.percentAllocations, + splitConfig.distributorFee, + users.owner.address, + ); + const l1RegistryAddress = await summonGuildRegistryProxy( + l1CalculatorLibrary.address, + { + splitMain: l1SplitMain.address, + split: newL1SplitAddress, + owner: users.owner.address, + }, + "Guild Registry", + ); + const txTransfer = await l1SplitMain.transferControl(newL1SplitAddress, l1RegistryAddress); + await expect(txTransfer) + .to.emit(l1SplitMain, "InitiateControlTransfer") + .withArgs(newL1SplitAddress, l1RegistryAddress); + + const registry = (await ethers.getContractAt("GuildRegistry", l1RegistryAddress, signer)) as GuildRegistry; + const txAccept = await registry.acceptSplitControl(); + await expect(txAccept) + .to.emit(l1SplitMain, "ControlTransfer") + .withArgs(newL1SplitAddress, users.owner.address, l1RegistryAddress); + + const txTransfer2 = await registry.transferSplitControl(users.applicant.address); + await expect(txTransfer2) + .to.emit(l1SplitMain, "InitiateControlTransfer") + .withArgs(newL1SplitAddress, users.applicant.address); + + const tx = await registry.cancelSplitControlTransfer(); + await expect(tx).to.emit(l1SplitMain, "CancelControlTransfer").withArgs(newL1SplitAddress); + }); + }); + + // ############################################################################################################## + // ################################## ##################################################### + // ################################## GuildRegistry Actions ##################################################### + // ################################## ##################################################### + // ############################################################################################################## + // ############################################################################################################## + // ############################################################################################################## + + describe("GuildRegistry Actions", function () { + it("Should not be able to update a main registry if not the owner", async () => { + const signer = await ethers.getSigner(users.applicant.address); + const applicantRegistry = guildRegistry.connect(signer); + await expect( + applicantRegistry.batchNewMembers([users.applicant.address], [100], [0]), + ).to.be.revertedWithCustomError(guildRegistry, "OwnableUnauthorizedAccount"); + await expect( + applicantRegistry.batchUpdateMembersActivity([users.applicant.address], [100]), + ).to.be.revertedWithCustomError(guildRegistry, "OwnableUnauthorizedAccount"); + await expect(applicantRegistry.batchRemoveMembers([users.applicant.address])).to.be.revertedWithCustomError( + guildRegistry, + "OwnableUnauthorizedAccount", + ); + }); + + it("Should not be able to add new members in batch if param sizes mismatch", async () => { + const startDate = await time.latest(); + + await expect(guildRegistry.batchNewMembers([], [10], [Number(startDate)])).to.be.revertedWithCustomError( + guildRegistry, + "Registry__ParamsSizeMismatch", + ); + + await expect( + guildRegistry.batchNewMembers([ethers.constants.AddressZero], [10], []), + ).to.be.revertedWithCustomError(guildRegistry, "Registry__ParamsSizeMismatch"); + + await expect( + guildRegistry.batchNewMembers([ethers.constants.AddressZero], [], [Number(startDate)]), + ).to.be.revertedWithCustomError(guildRegistry, "Registry__ParamsSizeMismatch"); + }); + + it("Should not be able to add new members in batch if activityMultiplier=0", async () => { + const newMembers = await generateMemberBatch(10); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + activityMultipliers[0] = 0; + const startDates = newMembers.map((m: Member) => m.startDate); + await expect(guildRegistry.batchNewMembers(members, activityMultipliers, startDates)) + .to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__InvalidActivityMultiplier") + .withArgs(members[0], 0); + }); + + it("Should be able to add a new member with correct parameters", async () => { + const [, , , member1, member2] = await getUnnamedAccounts(); + const activityMultiplier = 100; + const startDate = await time.latest(); + + await expect( + guildRegistry.batchNewMembers([ethers.constants.AddressZero], [activityMultiplier], [startDate]), + ).to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__InvalidAddress"); + + await expect(guildRegistry.batchNewMembers([member1], [activityMultiplier + 1], [startDate])) + .to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__InvalidActivityMultiplier") + .withArgs(member1, activityMultiplier + 1); + + await expect(guildRegistry.batchNewMembers([member1], [0], [startDate])) + .to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__InvalidActivityMultiplier") + .withArgs(member1, 0); + + await expect( + guildRegistry.batchNewMembers([member1], [activityMultiplier], [(await time.latest()) + 1e6]), + ).to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__StartDateInTheFuture"); + + // tx success + const tx = await guildRegistry.batchNewMembers([member1], [activityMultiplier], [startDate]); + await expect(tx).to.emit(guildRegistry, "NewMember").withArgs(member1, Number(startDate), activityMultiplier); + + await expect( + guildRegistry.batchNewMembers([member1], [activityMultiplier], [startDate]), + ).to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__AlreadyRegistered"); + + const members = await guildRegistry.getMembers(); + const totalMembers = await guildRegistry.totalMembers(); + const totalActiveMembers = await guildRegistry.totalActiveMembers(); + expect(members.length).to.be.equal(totalMembers); + expect(totalMembers).to.be.equal(totalActiveMembers); + expect(members[0]).to.have.ordered.members([member1, 0, Number(startDate), activityMultiplier]); + + const member = await guildRegistry.getMember(member1); + expect(member).to.have.ordered.members([member1, 0, Number(startDate), activityMultiplier]); + + await expect(guildRegistry.getMember(member2)).to.be.revertedWithCustomError( + guildRegistry, + "MemberRegistry__NotRegistered", + ); + }); + + it("Should be able to add new members in batch", async () => { + const newMembers = await generateMemberBatch(10); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + const tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + for (let i = 0; i < newMembers.length; i++) { + await expect(tx) + .to.emit(guildRegistry, "NewMember") + .withArgs(newMembers[i].account, Number(newMembers[i].startDate), newMembers[i].activityMultiplier); + } + const totalActiveMembers = await guildRegistry.totalActiveMembers(); + expect(totalActiveMembers).to.be.equal(members.length); + }); + + it("Should not be able to update members in batch if param sizes mismatch", async () => { + await expect( + guildRegistry.batchUpdateMembersActivity(members.slice(0, 1), []), + // ).to.revertedWithPanic("0x32"); // Array accessed at an out-of-bounds or negative index + ).to.be.revertedWithCustomError(guildRegistry, "Registry__ParamsSizeMismatch"); + + await expect( + guildRegistry.batchUpdateMembersActivity([], [100]), + // ).to.revertedWithPanic("0x32"); // Array accessed at an out-of-bounds or negative index + ).to.be.revertedWithCustomError(guildRegistry, "Registry__ParamsSizeMismatch"); + }); + + it("Should be able to update an existing member with correct parameters", async () => { + const [, , , member1, member2] = await getUnnamedAccounts(); + const activityMultiplier = 100; + const modActivityMultiplier = activityMultiplier / 2; + const startDate = await time.latest(); + + await expect( + guildRegistry.batchUpdateMembersActivity([member2], [activityMultiplier]), + ).to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__NotRegistered"); + + const newTx = await guildRegistry.batchNewMembers([member1], [activityMultiplier], [startDate]); + await newTx.wait(); + const totalMembersBefore = await guildRegistry.totalMembers(); + + await expect( + guildRegistry.batchUpdateMembersActivity([member1], [activityMultiplier + 1]), + ).to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__InvalidActivityMultiplier"); + + // does not happen as member with activityMultiplier=0 is directly removed from the registry + // // should revert if member.secondsActive = 0 + // await expect( + // guildRegistry.batchUpdateMembersActivity([member1], [0]), + // ).to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__InvalidActivityMultiplier"); + + let member = await guildRegistry.getMember(member1); + + let tx = await guildRegistry.batchUpdateMembersActivity([member1], [modActivityMultiplier]); + await expect(tx) + .to.emit(guildRegistry, "UpdateMember") + .withArgs(member1, modActivityMultiplier, member.startDate, member.secondsActive); + + member = await guildRegistry.getMember(member1); + let totalMembersAfter = await guildRegistry.totalMembers(); + let totalActiveMembers = await guildRegistry.totalActiveMembers(); + expect(totalMembersBefore).to.be.equal(totalMembersAfter); + expect(totalMembersAfter).to.be.equal(totalActiveMembers); + + expect(member).to.have.ordered.members([member1, 0, Number(startDate), modActivityMultiplier]); + + // update registry activity + tx = await guildRegistry.updateSecondsActive(0); + await tx.wait(); + + // deactivate member at next epoch + tx = await guildRegistry.batchUpdateMembersActivity([member1], [0]); + await expect(tx).to.emit(guildRegistry, "RemoveMember").withArgs(member1); + totalMembersAfter = await guildRegistry.totalMembers(); + totalActiveMembers = await guildRegistry.totalActiveMembers(); + expect(totalMembersAfter).to.be.equal(totalActiveMembers); + expect(totalMembersAfter).to.be.equal(0); + expect(totalActiveMembers).to.be.equal(0); + }); + + it("Should be able to update members in batch", async () => { + const newMembers = await generateMemberBatch(10); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const modActivityMultipliers = newMembers.map((_, i) => (i % 2 === 0 ? 100 : 0)); + const startDates = newMembers.map((m: Member) => m.startDate); + const batchTx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batchTx.wait(); + + const updateTx = await guildRegistry.updateSecondsActive(0); + await updateTx.wait(); + + const tx = await guildRegistry.batchUpdateMembersActivity(members, modActivityMultipliers); + for (let i = 0; i < newMembers.length; i++) { + if (modActivityMultipliers[i] > 0) + await expect(tx) + .to.emit(guildRegistry, "UpdateMember") + .withArgs(newMembers[i].account, modActivityMultipliers[i], newMembers[i].startDate, anyValue); + } + // TODO: check members with activityMuliplier=0 were removed from the registry + const totalActiveMembers = await guildRegistry.totalActiveMembers(); + expect(totalActiveMembers).to.be.equal(modActivityMultipliers.filter((v) => v === 0).length); + }); + + it("Should not be able to remove an unregistered member", async () => { + const [, , , member] = await getUnnamedAccounts(); + await expect(guildRegistry.batchRemoveMembers([member])).to.be.revertedWithCustomError( + guildRegistry, + "MemberRegistry__NotRegistered", + ); + }); + + it("Should be able to remove members from the registry", async () => { + const batchSize = 5; + const newMembers: Member[] = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + const batchAddTx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batchAddTx.wait(); + + const updateTx = await guildRegistry.updateSecondsActive(0); + await updateTx.wait(); + + // const batchUpdateTx = await guildRegistry.batchUpdateMembersActivity(members.slice(0, 2), [0, 0]); + // await batchUpdateTx.wait(); + + const toBeMembers = [members[1], members[3]]; + + const totalMembersBefore = await guildRegistry.totalMembers(); + const totalActiveMembersBefore = await guildRegistry.totalActiveMembers(); + expect(totalMembersBefore).to.be.equal(totalActiveMembersBefore); + + const removeMembers = members.filter((_, i) => i % 2 === 0); + const tx = await guildRegistry.batchRemoveMembers(removeMembers); + for (let i = 1; i < removeMembers.length; i++) { + await expect(tx).to.emit(guildRegistry, "RemoveMember").withArgs(removeMembers[i]); + } + const totalMembersAfter = await guildRegistry.totalMembers(); + const totalActiveMembersAfter = await guildRegistry.totalActiveMembers(); + expect(totalMembersAfter).to.be.equal(totalActiveMembersAfter); + expect(totalMembersAfter).to.be.equal(totalMembersBefore.sub(removeMembers.length)); + expect(totalActiveMembersAfter).to.be.equal(totalActiveMembersBefore.sub(removeMembers.length)); + + const memberList = await guildRegistry.getMembers(); + expect(memberList.map((m) => m.account).every((m) => toBeMembers.includes(m))).to.be.true; + expect( + ( + await Promise.all( + toBeMembers.map(async (address) => (await guildRegistry.getMember(address)).account === address), + ) + ).every((v) => v), + ).to.be.true; + }); + + it("Should no tbe able to update registry activity using invalid cutoffDate", async () => { + const batchSize = 5; + const newMembers: Member[] = await generateMemberBatch(batchSize * 2); + const batch1 = newMembers.slice(0, batchSize); + let members = batch1.map((m: Member) => m.account); + let activityMultipliers = batch1.map((m: Member) => m.activityMultiplier); + let startDates = batch1.map((m: Member) => m.startDate); + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + const previousTimestamp = await time.latest(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + ///////// BATCH 2 + + const batch2 = newMembers.slice(batchSize, batchSize * 2); + members = batch2.map((m: Member) => m.account); + activityMultipliers = batch2.map((m: Member) => m.activityMultiplier); + startDates = batch1.map((m: Member) => Number(m.startDate) + 3600 * 24 * 15); // 15 days later + const batch2Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch2Tx.wait(); + + const lastBlockTimestamp = await time.latest(); + + await expect(guildRegistry.updateSecondsActive(previousTimestamp)).to.be.revertedWithCustomError( + guildRegistry, + "MemberRegistry__InvalidCutoffDate", + ); + + await expect( + guildRegistry.updateSecondsActive(lastBlockTimestamp + 3600 * 24), // one day ahead + ).to.be.revertedWithCustomError(guildRegistry, "MemberRegistry__InvalidCutoffDate"); + }); + + it("Should be able to update registry activity", async () => { + const batchSize = 5; + const newMembers: Member[] = await generateMemberBatch(batchSize * 3); + const batch1 = newMembers.slice(0, batchSize); + let members = batch1.map((m: Member) => m.account); + let activityMultipliers = batch1.map((m: Member) => m.activityMultiplier); + let startDates = batch1.map((m: Member) => m.startDate); + const batch1Date = Number(startDates[0]); + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + ///////// BATCH 2 + + const batch2 = newMembers.slice(batchSize, batchSize * 2); + members = batch2.map((m: Member) => m.account); + activityMultipliers = batch2.map((m: Member) => m.activityMultiplier); + startDates = batch1.map((m: Member) => Number(m.startDate) + 3600 * 24 * 15); // 15 days later + const batch2Date = Number(startDates[0]); + const batch2Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch2Tx.wait(); + + let lastBlockTimestamp = (await time.latest()) + 1; + + let tx = await guildRegistry.updateSecondsActive(lastBlockTimestamp); + + for (let i = 0; i < batchSize; i++) { + await expect(tx) + .to.emit(guildRegistry, "UpdateMemberSeconds") + .withArgs( + batch1[i].account, + Math.floor(((lastBlockTimestamp - batch1Date) * Number(batch1[i].activityMultiplier)) / 100), + ); + await expect(tx) + .to.emit(guildRegistry, "UpdateMemberSeconds") + .withArgs( + batch2[i].account, + Math.floor(((lastBlockTimestamp - batch2Date) * Number(batch2[i].activityMultiplier)) / 100), + ); + } + let totalMembers = await guildRegistry.totalMembers(); + await expect(tx).to.emit(guildRegistry, "RegistryActivityUpdate").withArgs(lastBlockTimestamp, totalMembers); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + ///////// BATCH 3 + + const batch3 = newMembers.slice(batchSize * 2, batchSize * 3); + members = batch3.map((m: Member) => m.account); + activityMultipliers = batch3.map(() => 100); // make sure all new members are active + startDates = batch3.map((m: Member) => Number(m.startDate) + 3600 * 24 * 45); // 45 days later + const batch3Date = Number(startDates[0]); + const batch3Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch3Tx.wait(); + + const lastActivityUpdate = await guildRegistry.lastActivityUpdate(); + + tx = await guildRegistry.updateSecondsActive(0); + + lastBlockTimestamp = await time.latest(); + + for (let i = 0; i < batchSize; i++) { + await expect(tx) + .to.emit(guildRegistry, "UpdateMemberSeconds") + .withArgs( + batch1[i].account, + Math.floor(((lastBlockTimestamp - lastActivityUpdate) * Number(batch1[i].activityMultiplier)) / 100), + ); + await expect(tx) + .to.emit(guildRegistry, "UpdateMemberSeconds") + .withArgs( + batch2[i].account, + Math.floor(((lastBlockTimestamp - lastActivityUpdate) * Number(batch2[i].activityMultiplier)) / 100), + ); + await expect(tx) + .to.emit(guildRegistry, "UpdateMemberSeconds") + .withArgs( + batch3[i].account, + Math.floor(((lastBlockTimestamp - batch3Date) * Number(activityMultipliers[i])) / 100), + ); + } + totalMembers = await guildRegistry.totalMembers(); + await expect(tx).to.emit(guildRegistry, "RegistryActivityUpdate").withArgs(lastBlockTimestamp, totalMembers); + }); + + it("Should not be able to update Split distribution if submitted member list is invalid", async () => { + const batchSize = 10; + const newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + const txUpdate = await guildRegistry.updateSecondsActive(0); + await txUpdate.wait(); + + const splitDistributorFee = splitConfig.distributorFee; + + newMembers.sort((a: Member, b: Member) => (a.account.toLowerCase() > b.account.toLowerCase() ? 1 : -1)); + const sortedMembers = newMembers.map((m: Member) => m.account); + + // first member in sortedList becomes inactive + const batch2Tx = await guildRegistry.batchUpdateMembersActivity(sortedMembers.slice(0, 1), [0]); + await batch2Tx.wait(); + + await expect(guildRegistry.updateSplits(members, splitDistributorFee)).to.be.revertedWithCustomError( + l1CalculatorLibrary, + "SplitDistribution__MemberListSizeMismatch", + ); + await expect(guildRegistry.updateSplits(members.slice(1), splitDistributorFee)).to.be.revertedWithCustomError( + l1CalculatorLibrary, + "SplitDistribution__AccountsOutOfOrder", + ); + + sortedMembers.pop(); // remove the last member in sortedList + // should not happen as inactive members are immediately removed from the registry + // // try to execute a split distribution with first member in sortedList as inactive + // await expect(guildRegistry.updateSplits(sortedMembers, splitDistributorFee)) + // .to.be.revertedWithCustomError(l1CalculatorLibrary, "SplitDistribution__InactiveMember") + // .withArgs(sortedMembers[0]); + + const activeMembers = sortedMembers.slice(1); // remove inactive member from sortedList + const unregisteredMemberAddr = ethers.utils.getAddress(`0x${"f".repeat(40)}`); // replace last member in sortedList + await expect(guildRegistry.updateSplits([...activeMembers, unregisteredMemberAddr], splitDistributorFee)) + .to.be.revertedWithCustomError(l1CalculatorLibrary, "MemberRegistry__NotRegistered") + .withArgs(unregisteredMemberAddr); + }); + + it("Should not be able to update a Split distribution if there is no active members", async () => { + const batchSize = 10; + const newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + let activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + + const splitDistributorFee = splitConfig.distributorFee; + + // no updates applied + let txUpdate = await guildRegistry.updateSecondsActive(0); + await txUpdate.wait(); + + newMembers.sort((a: Member, b: Member) => (a.account.toLowerCase() > b.account.toLowerCase() ? 1 : -1)); + const sortedMembers = newMembers.map((m: Member) => m.account); + + await expect(guildRegistry.updateSplits(sortedMembers, splitDistributorFee)).to.be.revertedWithCustomError( + l1CalculatorLibrary, + "SplitDistribution__NoActiveMembers", + ); + + // add some members to the registry + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + txUpdate = await guildRegistry.updateSecondsActive(0); + await txUpdate.wait(); + + // now all members become inactive + activityMultipliers = newMembers.map(() => 0); + const batch2Tx = await guildRegistry.batchUpdateMembersActivity(members, activityMultipliers); + await batch2Tx.wait(); + + expect(await guildRegistry.totalActiveMembers()).to.be.equal(0); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + // no updates applied + txUpdate = await guildRegistry.updateSecondsActive(0); + await txUpdate.wait(); + + await expect(guildRegistry.updateSplits(sortedMembers, splitDistributorFee)).to.be.revertedWithCustomError( + l1CalculatorLibrary, + "SplitDistribution__NoActiveMembers", + ); + }); + + it("Should be able to calculate Split allocations that sum up to PERCENTAGE_SCALE", async () => { + const batchSize = 10; + const newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + // same activityMultipliers and startDates to enforce allocations to sum up to PERCENTAGE_SCALE + const activityMultipliers = newMembers.map(() => 100); + const startDates = newMembers.map(() => newMembers[0].startDate); + + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + const txUpdate = await guildRegistry.updateSecondsActive(0); + await txUpdate.wait(); + + newMembers.sort((a: Member, b: Member) => (a.account.toLowerCase() > b.account.toLowerCase() ? 1 : -1)); + const sortedMembers = newMembers.map((m: Member) => m.account); + + const { _receivers, _percentAllocations } = await guildRegistry.calculate(sortedMembers); + + // fetch last calculated contributions on registry + const contributions = await Promise.all( + newMembers.map(async (member: Member) => await guildRegistry["calculateContributionOf"](member.account)), + ); + const totalContributions = contributions.reduce((a: BigNumber, b: BigNumber) => a.add(b), BigNumber.from(0)); + + // calculate allocations on active members + const calculatedAllocations = contributions.map((contr: BigNumber) => + contr.mul(PERCENTAGE_SCALE).div(totalContributions), + ); + + expect(_receivers).to.be.eql(newMembers.map((m: Member) => m.account)); + expect(_percentAllocations).to.be.eql(calculatedAllocations.map((v: BigNumber) => v.toNumber())); + }); + + it("Should be able to calculate Split allocations", async () => { + const batchSize = 10; + const newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + const txUpdate = await guildRegistry.updateSecondsActive(0); + await txUpdate.wait(); + + newMembers.sort((a: Member, b: Member) => (a.account.toLowerCase() > b.account.toLowerCase() ? 1 : -1)); + const sortedMembers = newMembers.map((m: Member) => m.account); + + const { _receivers, _percentAllocations } = await guildRegistry.calculate(sortedMembers); + + // filter active members + const activeMembers = newMembers.filter((member: Member) => Number(member.activityMultiplier) > 0); + // fetch last calculated contributions on registry + const contributions = await Promise.all( + activeMembers.map(async (member: Member) => await guildRegistry["calculateContributionOf"](member.account)), + ); + const totalContributions = contributions.reduce((a: BigNumber, b: BigNumber) => a.add(b), BigNumber.from(0)); + + // calculate allocations on active members + const calculatedAllocations = contributions.map((contr: BigNumber) => + contr.mul(PERCENTAGE_SCALE).div(totalContributions), + ); + const totalAllocations = calculatedAllocations.reduce( + (a: BigNumber, b: BigNumber) => a.add(b), + BigNumber.from(0), + ); + // NOTICE: dust (remainder) should be added to the member with the lowest allocation + if (totalAllocations.lt(PERCENTAGE_SCALE)) { + const contribAsNumber: number[] = contributions.map((c) => c.toNumber()); + const minValue = Math.min(...contribAsNumber); + const minIndex = contribAsNumber.indexOf(minValue); + calculatedAllocations[minIndex] = calculatedAllocations[minIndex].add(PERCENTAGE_SCALE.sub(totalAllocations)); + } + + expect(_receivers).to.be.eql(activeMembers.map((m: Member) => m.account)); + expect(_percentAllocations).to.be.eql(calculatedAllocations.map((v: BigNumber) => v.toNumber())); + }); + + it("Should not be able to produce an empty Split distribution", async () => { + const batchSize = 10; + const newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + members.sort((a: string, b: string) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1)); + + await expect(guildRegistry.calculate(members)).to.be.revertedWithCustomError( + l1CalculatorLibrary, + "SplitDistribution__EmptyDistribution", + ); + + const splitDistributorFee = splitConfig.distributorFee; + + await expect(guildRegistry.updateSplits(members, splitDistributorFee)).to.be.revertedWithCustomError( + l1CalculatorLibrary, + "SplitDistribution__EmptyDistribution", + ); + }); + + it("Should be able to update Split values from last update", async () => { + const batchSize = 10; + const newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + const txUpdate = await guildRegistry.updateSecondsActive(0); + await txUpdate.wait(); + + members.sort((a: string, b: string) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1)); + const splitDistributorFee = splitConfig.distributorFee; + + // pre-calculate to get split hash + const { _receivers, _percentAllocations } = await guildRegistry.calculate(members); + + const splitHash = hashSplit(_receivers, _percentAllocations, splitDistributorFee); + + const tx = await guildRegistry.updateSplits(members, splitDistributorFee); + + await expect(tx).to.emit(l1SplitMain, "UpdateSplit").withArgs(l1SplitAddress); + await expect(tx) + .to.emit(guildRegistry, "SplitsDistributionUpdated") + .withArgs(l1SplitAddress, splitHash, splitDistributorFee); + }); + + it("Should not be able to update all if submitted member list is invalid", async () => { + const batchSize = 10; + const newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + const splitDistributorFee = splitConfig.distributorFee; + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + newMembers.sort((a: Member, b: Member) => (a.account.toLowerCase() > b.account.toLowerCase() ? 1 : -1)); + const sortedMembers = newMembers.map((m: Member) => m.account); + + await time.increase(3600 * 24 * 30); // next block in 30 days + const updateTx = await guildRegistry.updateSecondsActive(0); + await updateTx.wait(); + + // first member in sortedList becomes inactive + const batch2Tx = await guildRegistry.batchUpdateMembersActivity(sortedMembers.slice(0, 1), [0]); + await batch2Tx.wait(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + await expect(guildRegistry.updateAll(0, members, splitDistributorFee)).to.be.revertedWithCustomError( + l1CalculatorLibrary, + "SplitDistribution__MemberListSizeMismatch", + ); + await expect(guildRegistry.updateAll(0, members.slice(1), splitDistributorFee)).to.be.revertedWithCustomError( + l1CalculatorLibrary, + "SplitDistribution__AccountsOutOfOrder", + ); + + sortedMembers.pop(); // remove the last member in sortedList + // try to execute a update all with first member in sortedList as inactive + await expect(guildRegistry.updateAll(0, sortedMembers, splitDistributorFee)) + .to.be.revertedWithCustomError(l1CalculatorLibrary, "MemberRegistry__NotRegistered") + .withArgs(sortedMembers[0]); + + const activeMembers = sortedMembers.slice(1); // remove inactive member from sortedList + const unregisteredMemberAddr = ethers.utils.getAddress(`0x${"f".repeat(40)}`); // replace last member in sortedList + await expect(guildRegistry.updateAll(0, [...activeMembers, unregisteredMemberAddr], splitDistributorFee)) + .to.be.revertedWithCustomError(l1CalculatorLibrary, "MemberRegistry__NotRegistered") + .withArgs(unregisteredMemberAddr); + }); + + it("Should be able to update all (member's activity + Splits)", async () => { + const batchSize = 10; + const newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + const batch1Date = Number(startDates[0]); + const batch1Tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await batch1Tx.wait(); + + await time.increase(3600 * 24 * 30); // next block in 30 days + + members.sort((a: string, b: string) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1)); + const splitDistributorFee = splitConfig.distributorFee; + + const tx = await guildRegistry.updateAll(0, members, splitDistributorFee); + await tx.wait(); + + const lastBlockTimestamp = await time.latest(); + + // MUST get calculations after the updateAll call so it uses the latest activeSeconds + const { _receivers, _percentAllocations } = await guildRegistry.calculate(members); + const splitHash = hashSplit(_receivers, _percentAllocations, splitDistributorFee); + + for (let i = 0; i < batchSize; i++) { + await expect(tx) + .to.emit(guildRegistry, "UpdateMemberSeconds") + .withArgs( + newMembers[i].account, + Math.floor(((lastBlockTimestamp - batch1Date) * Number(newMembers[i].activityMultiplier)) / 100), + ); + } + const totalMembers = await guildRegistry.totalMembers(); + await expect(tx).to.emit(guildRegistry, "RegistryActivityUpdate").withArgs(lastBlockTimestamp, totalMembers); + + await expect(tx).to.emit(l1SplitMain, "UpdateSplit").withArgs(l1SplitAddress); + await expect(tx) + .to.emit(guildRegistry, "SplitsDistributionUpdated") + .withArgs(l1SplitAddress, splitHash, splitDistributorFee); + }); + }); + + // ########################################################################################################## + // ################################# ##################################################### + // ################################# GuildRegistry ##################################################### + // ################################# Getters ##################################################### + // ########################################################################################################## + // ########################################################################################################## + // ########################################################################################################## + + describe("GuildRegistry getters", function () { + const batchSize: number = 10; + let newMembers: Array; + + beforeEach(async function () { + newMembers = await generateMemberBatch(batchSize); + const members = newMembers.map((m: Member) => m.account); + const activityMultipliers = newMembers.map((m: Member) => m.activityMultiplier); + const startDates = newMembers.map((m: Member) => m.startDate); + const tx = await guildRegistry.batchNewMembers(members, activityMultipliers, startDates); + await tx.wait(); + }); + + it("Should be able to get the current number of registered members", async () => { + const totalMembers = await guildRegistry.totalMembers(); + expect(totalMembers).to.equal(newMembers.length); + }); + + it("Should be able to get the current number of active members", async () => { + // TODO: check members updates being removed from the registry + expect(await guildRegistry.totalActiveMembers()).to.equal(newMembers.length); + + const updateTx = await guildRegistry.updateSecondsActive(0); + await updateTx.wait(); + + const tx = await guildRegistry.batchUpdateMembersActivity([members[0]], [0]); + await tx.wait(); + + expect(await guildRegistry.totalActiveMembers()).to.equal(newMembers.length - 1); + }); + + it("Should throw an error when trying to fetch an unregistered user", async () => { + await expect(guildRegistry.getMember(users.owner.address)).to.be.revertedWithCustomError( + guildRegistry, + "MemberRegistry__NotRegistered", + ); + await expect(guildRegistry.getMembersProperties([users.owner.address])).to.be.revertedWithCustomError( + guildRegistry, + "MemberRegistry__NotRegistered", + ); + }); + + it("Should be able to fetch a registered member", async () => { + const member = await guildRegistry.getMember(newMembers[0].account); + expect(member.account).to.equal(newMembers[0].account); + expect(member.activityMultiplier).to.equal(newMembers[0].activityMultiplier); + expect(member.startDate).to.equal(newMembers[0].startDate); + expect(member.secondsActive).to.equal(0); + + const memberProperties = await guildRegistry.getMembersProperties([newMembers[0].account]); + expect(memberProperties[0][0]).to.equal(newMembers[0].activityMultiplier); + expect(memberProperties[1][0]).to.equal(newMembers[0].startDate); + expect(memberProperties[2][0]).to.equal(0); + }); + + it("Should be able to fetch all registered members", async () => { + const members = await guildRegistry.getMembers(); + for (let i = 0; i < newMembers.length; i++) { + expect(members[i].account).to.equal(newMembers[i].account); + expect(members[i].activityMultiplier).to.equal(newMembers[i].activityMultiplier); + expect(members[i].startDate).to.equal(newMembers[i].startDate); + expect(members[i].secondsActive).to.equal(0); + } + }); + + it("Should not be able to fetch members paginated if index is out of bounds", async () => { + await expect(guildRegistry.getMembersPaginated(100, 10000)).to.be.revertedWithCustomError( + guildRegistry, + "MemberRegistry__IndexOutOfBounds", + ); + await expect(guildRegistry.getMembersPaginated(0, 100)).to.be.revertedWithCustomError( + guildRegistry, + "MemberRegistry__IndexOutOfBounds", + ); + }); + + it("Should be able to fetch members paginated", async () => { + const toIndex = 5; + const members = await guildRegistry.getMembersPaginated(0, toIndex); + expect(members.length).to.equal(toIndex + 1); + }); + + it("Should be able to calculate members total contributions", async () => { + // update registry activity + const syncUpdateTx = await guildRegistry.updateSecondsActive(0); + await syncUpdateTx.wait(); + + const totalContribBefore = await guildRegistry.calculateTotalContributions(); + + // get member contribution before getting inactive + const member = newMembers[newMembers.length - 1].account; + const memberContrib = await guildRegistry.calculateContributionOf(member); + + // member gets inactive + const syncTx = await guildRegistry.batchUpdateMembersActivity([member], [0]); + await syncTx.wait(); + + const totalContribAfter = await guildRegistry.calculateTotalContributions(); + expect(totalContribBefore).to.eql(totalContribAfter.add(memberContrib)); + }); + }); + + // ########################################################################################################## + // ################################# ##################################################### + // ################################# GuildRegistry ##################################################### + // ################################# UUPS Proxy ##################################################### + // ########################################################################################################## + // ########################################################################################################## + // ########################################################################################################## + + describe("GuildRegistry UUPS Upgradeability", function () { + let newRegistryImplementation: GuildRegistryV2Mock; + + beforeEach(async () => { + const { deployer } = await getNamedAccounts(); + const signer = await ethers.getSigner(deployer); + const implDeployed = await deployments.deploy("GuildRegistryV2Mock", { + contract: "GuildRegistryV2Mock", + from: deployer, + args: [], + libraries: { + PGContribCalculator: l1CalculatorLibrary.address, + }, + log: true, + }); + newRegistryImplementation = await ethers.getContractAt("GuildRegistryV2Mock", implDeployed.address, signer); + }); + + it("Should not be able to upgrade the implementation of a registry if not owner", async () => { + const [, , , , outsider] = await getUnnamedAccounts(); + const signer = await ethers.getSigner(outsider); + const l1NetRegistry = guildRegistry.connect(signer); + await expect(l1NetRegistry.upgradeToAndCall(ethers.constants.AddressZero, "0x")).to.be.revertedWithCustomError( + guildRegistry, + "Registry__UnauthorizedToUpgrade", + ); + }); + + it("Should not be able to upgrade the implementation of a registry if not UUPS compliant", async () => { + await expect( + guildRegistry.upgradeToAndCall( + l1CalculatorLibrary.address, // wrong contract implementation + "0x", + ), + ).to.be.revertedWithCustomError(guildRegistry, "ERC1967InvalidImplementation"); + }); + + it("Should be able to upgrade the registry implementation if owner", async () => { + await expect(guildRegistry.upgradeToAndCall(newRegistryImplementation.address, "0x")) + .to.emit(guildRegistry, "Upgraded") + .withArgs(newRegistryImplementation.address); + + const initializationParams = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "address"], + [l1SplitMain.address, l1SplitAddress, users.owner.address], + ); + + const calldata = newRegistryImplementation.interface.encodeFunctionData("initialize", [initializationParams]); + + const tx = await guildRegistry.upgradeToAndCall(newRegistryImplementation.address, calldata); + await tx.wait(); + + await expect(tx).to.emit(guildRegistry, "Upgraded").withArgs(newRegistryImplementation.address); + await expect(tx).to.emit(guildRegistry, "Initialized").withArgs(2); + }); + }); +}); diff --git a/test/types.ts b/test/types.ts index 27b6796..8a572d4 100644 --- a/test/types.ts +++ b/test/types.ts @@ -25,15 +25,10 @@ export type NetworkRegistryArgs = { owner: string; }; -export type NetworkRegistryShamanArgs = { - connext: string; - updaterDomainId: number; - updaterAddress: string; +export type GuildRegistryArgs = { splitMain: string; split: string; - baal: string; - sharesToMint: BigNumberish; - burnShares: boolean; + owner: string; }; export type Member = { diff --git a/test/utils/networkRegistry.ts b/test/utils/networkRegistry.ts index d68a58d..ac6d4ec 100644 --- a/test/utils/networkRegistry.ts +++ b/test/utils/networkRegistry.ts @@ -2,7 +2,7 @@ import { time } from "@nomicfoundation/hardhat-network-helpers"; import { deployments, ethers, getNamedAccounts, getUnnamedAccounts } from "hardhat"; // import { NetworkRegistrySummoner } from "../../types"; -import { Member, NetworkRegistryArgs } from "../types"; +import { GuildRegistryArgs, Member, NetworkRegistryArgs } from "../types"; // export const summonRegistry = async ( // summoner: NetworkRegistrySummoner, @@ -68,6 +68,21 @@ export const summonNetworkRegistryProxy = async ( return await summonRegistryProxy(calculatorLibraryAddress, initializationParams, registryName, "NetworkRegistry"); }; + +export const summonGuildRegistryProxy = async ( + calculatorLibraryAddress: string, + registryArgs: GuildRegistryArgs, + registryName: string = "GuildRegistry", +) => { + const { splitMain, split, owner } = registryArgs; + const initializationParams = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "address"], + [splitMain, split, owner], + ); + + return await summonRegistryProxy(calculatorLibraryAddress, initializationParams, registryName, "GuildRegistry"); +}; + export const generateMemberBatch = async (totalMembers: number): Promise> => { const accounts = await getUnnamedAccounts(); const members = accounts.slice(0, totalMembers);