diff --git a/packages/contracts/addresses-staging.json b/packages/contracts/addresses-staging.json index 11a912d43..5bf3369ef 100644 --- a/packages/contracts/addresses-staging.json +++ b/packages/contracts/addresses-staging.json @@ -203,6 +203,25 @@ "runtimeCodeHash": "0xd853da2001c213dd00d674114c254f18a7b5a36aabc5452fb492b9b4faf2faa9", "txHash": "0xf868871475dc711c5c03818b16a922f740bc2fc314d24586df2255161a907422" } + }, + "SubgraphAvailabilityManager": { + "address": "0xF00AA63f1a8eE67BAD135759eC7a6542b1d56a8f", + "constructorArgs": [ + "0x72ee30d43Fb5A90B3FE983156C5d2fBE6F6d07B3", + "0x00b9d319E3D09E83c62f453B44354049Dd93a345", + "3", + "300", + [ + "0xb4d396a40BB976118Bb2c9DB9C5539e65c7Fe64A", + "0x633259dB4A33DB664d4C3Fa57BEc45E5030131E2", + "0x9FC436A69E290227b99A9CE0468a1b9D73068E80", + "0xC9b90ab0B9cA63fDBfB46f66BE813ea94561614d", + "0xf3B8F917EcFA569089CF6A0988F9a9F5155cEF83" + ] + ], + "creationCodeHash": "0xc34842fa42ee2f21e3a435b52fa69a122a5de795448faa890fbc63d4838f45da", + "runtimeCodeHash": "0xe6c0ccd0b0514dd55e58806b1c84f09805dd6202985722579c11d7e252df6b8d", + "txHash": "0x5c1dd91c76e547fe31a5df822353b50e93eb795b915aac0ea8f8364229e9c29c" } }, "11155111": { diff --git a/packages/contracts/addresses.json b/packages/contracts/addresses.json index 076ee8cb0..f9c7a1f71 100644 --- a/packages/contracts/addresses.json +++ b/packages/contracts/addresses.json @@ -790,6 +790,25 @@ "creationCodeHash": "0xbfc20ab9b880712ab90b5dec9d2a14c724b0bf7c20f02ede8ea76610bd41b6ef", "runtimeCodeHash": "0xd7fdd744c7a88993435a2978876b1e4341c5e0fb4d611011bb56e8738ab2485d", "txHash": "0xcc449d1ca1007fba76b25e987ea0d39164acf4027c10c40bd669ede1c65dc569" + }, + "SubgraphAvailabilityManager": { + "address": "0x1cB555359319A94280aCf85372Ac2323AaE2f5fd", + "constructorArgs": [ + "0x8C6de8F8D562f3382417340A6994601eE08D3809", + "0x971B9d3d0Ae3ECa029CAB5eA1fB0F72c85e6a525", + "3", + "300", + [ + "0xdcAA0a094F2Eb7cF7f73248EE64217D59B7B938d", + "0xBD9dc46cb1dd9F31dCbF0617c2Dd5f77A21dB8e8", + "0x16eAd4088d4308a7A4E0F7a1455ed56CDf1AC8AA", + "0x61923453906Eadc15fc1F610B8D1b67bc27658c2", + "0x10eb33C5E2fb6c7a37B110Cc4930d03A9e4C4D09" + ] + ], + "creationCodeHash": "0xc34842fa42ee2f21e3a435b52fa69a122a5de795448faa890fbc63d4838f45da", + "runtimeCodeHash": "0x52fcfd39c6ab3cf5ed4a736bc38eb1153d73c8cf1ca9e23370badc7843467ab4", + "txHash": "0x2eb44036d157e39c56377403029aebde4961028b404fc8c3f4cadc0f299d06dd" } }, "421613": { @@ -1221,6 +1240,25 @@ "creationCodeHash": "0x20cd202f7991716a84c097da5fbd365fd27f7f35f241f82c529ad7aba18b814b", "runtimeCodeHash": "0x5f396ffd54b6cd6b3faded0f366c5d7e148cc54743926061be2dfd12a75391de", "txHash": "0x2cefbc169b8ae51c263d0298956d86a397b05f11f076b71c918551f63fe33784" + }, + "SubgraphAvailabilityManager": { + "address": "0x71D9aE967d1f31fbbD1817150902de78f8f2f73E", + "constructorArgs": [ + "0x72ee30d43Fb5A90B3FE983156C5d2fBE6F6d07B3", + "0x1F49caE7669086c8ba53CC35d1E9f80176d67E79", + "3", + "300", + [ + "0x5e4e823Ed094c035133eEC5Ec0d08ae1Af04e9Fa", + "0x5aeE4c46cF9260E85E630ca7d9D757D5D5DbaFD6", + "0x7369Cf2a917296c36f506144f3dE552403d1e1f1", + "0x1e1f84c07e0471fc979f6f08228b0bd34cda9e54", + "0x711aEA1f358DFAf74D34B4B525F9190e631F406C" + ] + ], + "creationCodeHash": "0xc34842fa42ee2f21e3a435b52fa69a122a5de795448faa890fbc63d4838f45da", + "runtimeCodeHash": "0x3907d0fe5a1fa28fee51100e57a3b193dfcee6720163067011e787262b1749bb", + "txHash": "0xb00751b4dc1c0603fe1b8b9ae9de8840ad1c29b42489c5bb267d80b10ae44ef0" } }, "11155111": { diff --git a/packages/contracts/config/graph.arbitrum-hardhat.yml b/packages/contracts/config/graph.arbitrum-hardhat.yml index 80f76b46a..ec4a161b1 100644 --- a/packages/contracts/config/graph.arbitrum-hardhat.yml +++ b/packages/contracts/config/graph.arbitrum-hardhat.yml @@ -2,7 +2,12 @@ general: arbitrator: &arbitrator "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0" # Arbitration Council governor: &governor "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b" # Graph Council authority: &authority "0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d" # Authority that signs payment vouchers - availabilityOracle: &availabilityOracle "0xd03ea8624C8C5987235048901fB614fDcA89b117" # Subgraph Availability Oracle + availabilityOracles: &availabilityOracles # Subgraph Availability Oracles + - "0xd03ea8624C8C5987235048901fB614fDcA89b117" + - "0xd03ea8624C8C5987235048901fB614fDcA89b117" + - "0xd03ea8624C8C5987235048901fB614fDcA89b117" + - "0xd03ea8624C8C5987235048901fB614fDcA89b117" + - "0xd03ea8624C8C5987235048901fB614fDcA89b117" pauseGuardian: &pauseGuardian "0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC" # Protocol pause guardian allocationExchangeOwner: &allocationExchangeOwner "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9" # Allocation Exchange owner @@ -131,7 +136,7 @@ contracts: - fn: "setIssuancePerBlock" issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" - subgraphAvailabilityOracle: *availabilityOracle + subgraphAvailabilityOracle: "${{SubgraphAvailabilityManager.address}}" - fn: "syncAllContracts" AllocationExchange: init: @@ -149,3 +154,10 @@ contracts: - fn: "syncAllContracts" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian + SubgraphAvailabilityManager: + init: + governor: *governor + rewardsManager: "${{RewardsManager.address}}" + executionThreshold: 5 + voteTimeLimit: 300 + oracles: *availabilityOracles diff --git a/packages/contracts/config/graph.arbitrum-localhost.yml b/packages/contracts/config/graph.arbitrum-localhost.yml index d1364b581..62598a07c 100644 --- a/packages/contracts/config/graph.arbitrum-localhost.yml +++ b/packages/contracts/config/graph.arbitrum-localhost.yml @@ -2,7 +2,12 @@ general: arbitrator: &arbitrator "0x4237154FE0510FdE3575656B60c68a01B9dCDdF8" # Arbitration Council governor: &governor "0x1257227a2ECA34834940110f7B5e341A5143A2c4" # Graph Council authority: &authority "0x12B8D08b116E1E3cc29eE9Cf42bB0AA8129C3215" # Authority that signs payment vouchers - availabilityOracle: &availabilityOracle "0x7694a48065f063a767a962610C6717c59F36b445" # Subgraph Availability Oracle + availabilityOracles: &availabilityOracles # Subgraph Availability Oracles + - "0x7694a48065f063a767a962610C6717c59F36b445" + - "0x7694a48065f063a767a962610C6717c59F36b445" + - "0x7694a48065f063a767a962610C6717c59F36b445" + - "0x7694a48065f063a767a962610C6717c59F36b445" + - "0x7694a48065f063a767a962610C6717c59F36b445" pauseGuardian: &pauseGuardian "0x601060e0DC5349AA55EC73df5A58cB0FC1cD2e3C" # Protocol pause guardian allocationExchangeOwner: &allocationExchangeOwner "0xbD38F7b67a591A5cc7D642e1026E5095B819d952" # Allocation Exchange owner @@ -132,7 +137,7 @@ contracts: - fn: "setIssuancePerBlock" issuancePerBlock: "6036500000000000000" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" - subgraphAvailabilityOracle: *availabilityOracle + subgraphAvailabilityOracle: "${{SubgraphAvailabilityManager.address}}" - fn: "syncAllContracts" AllocationExchange: init: @@ -150,3 +155,10 @@ contracts: - fn: "syncAllContracts" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian + SubgraphAvailabilityManager: + init: + governor: *governor + rewardsManager: "${{RewardsManager.address}}" + executionThreshold: 5 + voteTimeLimit: 300 + oracles: *availabilityOracles diff --git a/packages/contracts/config/graph.arbitrum-sepolia.yml b/packages/contracts/config/graph.arbitrum-sepolia.yml index 930feae82..c5fe97010 100644 --- a/packages/contracts/config/graph.arbitrum-sepolia.yml +++ b/packages/contracts/config/graph.arbitrum-sepolia.yml @@ -2,7 +2,12 @@ general: arbitrator: &arbitrator "0x1726A5d52e279d02ff4732eCeB2D67BFE5Add328" # EOA (TODO: update to a multisig) governor: &governor "0x72ee30d43Fb5A90B3FE983156C5d2fBE6F6d07B3" # EOA (TODO: update to a multisig) authority: &authority "0x49D4CFC037430cA9355B422bAeA7E9391e1d3215" # Authority that signs payment vouchers - availabilityOracle: &availabilityOracle "0x5e4e823Ed094c035133eEC5Ec0d08ae1Af04e9Fa" # Subgraph Availability Oracle + availabilityOracles: &availabilityOracles # Array of Subgraph Availability Oracles + - "0x5e4e823Ed094c035133eEC5Ec0d08ae1Af04e9Fa" + - "0x5aeE4c46cF9260E85E630ca7d9D757D5D5DbaFD6" + - "0x7369Cf2a917296c36f506144f3dE552403d1e1f1" + - "0x1e1f84c07e0471fc979f6f08228b0bd34cda9e54" + - "0x711aEA1f358DFAf74D34B4B525F9190e631F406C" pauseGuardian: &pauseGuardian "0xa0444508232dA3FA6C2f96a5f105f3f0cc0d20D7" # Protocol pause guardian allocationExchangeOwner: &allocationExchangeOwner "0x72ee30d43Fb5A90B3FE983156C5d2fBE6F6d07B3" # Allocation Exchange owner @@ -131,7 +136,7 @@ contracts: - fn: "setIssuancePerBlock" issuancePerBlock: "6036500000000000000" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" - subgraphAvailabilityOracle: *availabilityOracle + subgraphAvailabilityOracle: "${{SubgraphAvailabilityManager.address}}" - fn: "syncAllContracts" AllocationExchange: init: @@ -149,3 +154,10 @@ contracts: - fn: "syncAllContracts" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian + SubgraphAvailabilityManager: + init: + governor: *governor + rewardsManager: "${{RewardsManager.address}}" + executionThreshold: 5 + voteTimeLimit: 300 + oracles: *availabilityOracles diff --git a/packages/contracts/contracts/rewards/IRewardsManager.sol b/packages/contracts/contracts/rewards/IRewardsManager.sol index 4c062e774..511f1adf9 100644 --- a/packages/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/contracts/contracts/rewards/IRewardsManager.sol @@ -25,8 +25,6 @@ interface IRewardsManager { function setDenied(bytes32 _subgraphDeploymentID, bool _deny) external; - function setDeniedMany(bytes32[] calldata _subgraphDeploymentID, bool[] calldata _deny) external; - function isDenied(bytes32 _subgraphDeploymentID) external view returns (bool); // -- Getters -- diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 03cadacb0..f19aad1a6 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -127,22 +127,6 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa _setDenied(_subgraphDeploymentID, _deny); } - /** - * @dev Denies to claim rewards for multiple subgraph. - * NOTE: Can only be called by the subgraph availability oracle - * @param _subgraphDeploymentID Array of subgraph deployment ID - * @param _deny Array of denied status for claiming rewards for each subgraph - */ - function setDeniedMany( - bytes32[] calldata _subgraphDeploymentID, - bool[] calldata _deny - ) external override onlySubgraphAvailabilityOracle { - require(_subgraphDeploymentID.length == _deny.length, "!length"); - for (uint256 i = 0; i < _subgraphDeploymentID.length; i++) { - _setDenied(_subgraphDeploymentID[i], _deny[i]); - } - } - /** * @dev Internal: Denies to claim rewards for a subgraph. * @param _subgraphDeploymentID Subgraph deployment ID diff --git a/packages/contracts/contracts/rewards/SubgraphAvailabilityManager.sol b/packages/contracts/contracts/rewards/SubgraphAvailabilityManager.sol new file mode 100644 index 000000000..a8e3d0e30 --- /dev/null +++ b/packages/contracts/contracts/rewards/SubgraphAvailabilityManager.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import { Governed } from "../governance/Governed.sol"; +import { IRewardsManager } from "../rewards/IRewardsManager.sol"; + +/** + * @title Subgraph Availability Manager + * @dev Manages the availability of subgraphs by allowing oracles to vote on whether + * a subgraph should be denied rewards or not. When enough oracles have voted to deny or + * allow rewards for a subgraph, it calls the RewardsManager Contract to set the correct + * state. The oracles and the execution threshold are set at deployment time. + * Only governance can update the oracles and voteTimeLimit. + * Governor can transfer ownership to a new governor. + */ +contract SubgraphAvailabilityManager is Governed { + // -- Immutable -- + + /// @notice Number of oracles + uint256 public constant NUM_ORACLES = 5; + + /// @notice Number of votes required to execute a deny or allow call to the RewardsManager + uint256 public immutable executionThreshold; + + /// @dev Address of the RewardsManager contract + IRewardsManager private immutable rewardsManager; + + // -- State -- + + /// @dev Nonce for generating votes on subgraph deployment IDs. + /// Increased whenever oracles or voteTimeLimit change, to invalidate old votes. + uint256 public currentNonce; + + /// @notice Time limit for a vote to be valid + uint256 public voteTimeLimit; + + /// @notice Array of oracle addresses + address[NUM_ORACLES] public oracles; + + /// @notice Mapping of current nonce to subgraph deployment ID to an array of timestamps of last deny vote + /// currentNonce => subgraphDeploymentId => timestamp[oracleIndex] + mapping(uint256 => mapping(bytes32 => uint256[NUM_ORACLES])) public lastDenyVote; + + /// @notice Mapping of current nonce to subgraph deployment ID to an array of timestamp of last allow vote + /// currentNonce => subgraphDeploymentId => timestamp[oracleIndex] + mapping(uint256 => mapping(bytes32 => uint256[NUM_ORACLES])) public lastAllowVote; + + // -- Events -- + + /** + * @dev Emitted when an oracle is set + * @param index Index of the oracle + * @param oracle Address of the oracle + */ + event OracleSet(uint256 indexed index, address indexed oracle); + + /** + * @dev Emitted when the vote time limit is set + * @param voteTimeLimit Vote time limit in seconds + */ + event VoteTimeLimitSet(uint256 voteTimeLimit); + + /** + * @dev Emitted when an oracle votes to deny or allow a subgraph + * @param subgraphDeploymentID Subgraph deployment ID + * @param deny True to deny, false to allow + * @param oracleIndex Index of the oracle voting + * @param timestamp Timestamp of the vote + */ + event OracleVote(bytes32 indexed subgraphDeploymentID, bool deny, uint256 indexed oracleIndex, uint256 timestamp); + + // -- Modifiers -- + + modifier onlyOracle(uint256 _oracleIndex) { + require(_oracleIndex < NUM_ORACLES, "SAM: index out of bounds"); + require(msg.sender == oracles[_oracleIndex], "SAM: caller must be oracle"); + _; + } + + // -- Constructor -- + + /** + * @dev Contract constructor + * @param _governor Account that can set or remove oracles and set the vote time limit + * @param _rewardsManager Address of the RewardsManager contract + * @param _executionThreshold Number of votes required to execute a deny or allow call to the RewardsManager + * @param _voteTimeLimit Vote time limit in seconds + * @param _oracles Array of oracle addresses, must be NUM_ORACLES in length. + */ + constructor( + address _governor, + address _rewardsManager, + uint256 _executionThreshold, + uint256 _voteTimeLimit, + address[NUM_ORACLES] memory _oracles + ) { + require(_governor != address(0), "SAM: governor must be set"); + require(_rewardsManager != address(0), "SAM: rewardsManager must be set"); + require(_executionThreshold >= (NUM_ORACLES / 2) + 1, "SAM: executionThreshold too low"); + require(_executionThreshold <= NUM_ORACLES, "SAM: executionThreshold too high"); + + // Oracles should not be address zero + for (uint256 i; i < _oracles.length; i++) { + address oracle = _oracles[i]; + require(oracle != address(0), "SAM: oracle cannot be address zero"); + oracles[i] = oracle; + emit OracleSet(i, oracle); + } + + Governed._initialize(_governor); + rewardsManager = IRewardsManager(_rewardsManager); + + executionThreshold = _executionThreshold; + voteTimeLimit = _voteTimeLimit; + } + + // -- Functions -- + + /** + * @dev Set the vote time limit. Refreshes all existing votes by incrementing the current nonce. + * @param _voteTimeLimit Vote time limit in seconds + */ + function setVoteTimeLimit(uint256 _voteTimeLimit) external onlyGovernor { + voteTimeLimit = _voteTimeLimit; + currentNonce++; + emit VoteTimeLimitSet(_voteTimeLimit); + } + + /** + * @dev Set oracle address with index. Refreshes all existing votes by incrementing the current nonce. + * @param _index Index of the oracle + * @param _oracle Address of the oracle + */ + function setOracle(uint256 _index, address _oracle) external onlyGovernor { + require(_index < NUM_ORACLES, "SAM: index out of bounds"); + require(_oracle != address(0), "SAM: oracle cannot be address zero"); + + oracles[_index] = _oracle; + // Increment the current nonce to refresh all existing votes for subgraph deployment IDs + currentNonce++; + + emit OracleSet(_index, _oracle); + } + + /** + * @dev Vote deny or allow for a subgraph. + * NOTE: Can only be called by an oracle. + * @param _subgraphDeploymentID Subgraph deployment ID + * @param _deny True to deny, false to allow + * @param _oracleIndex Index of the oracle voting + */ + function vote(bytes32 _subgraphDeploymentID, bool _deny, uint256 _oracleIndex) external onlyOracle(_oracleIndex) { + _vote(_subgraphDeploymentID, _deny, _oracleIndex); + } + + /** + * @dev Vote deny or allow for many subgraphs. + * NOTE: Can only be called by an oracle. + * @param _subgraphDeploymentID Array of subgraph deployment IDs + * @param _deny Array of booleans, true to deny, false to allow + * @param _oracleIndex Index of the oracle voting + */ + function voteMany( + bytes32[] calldata _subgraphDeploymentID, + bool[] calldata _deny, + uint256 _oracleIndex + ) external onlyOracle(_oracleIndex) { + require(_subgraphDeploymentID.length == _deny.length, "!length"); + for (uint256 i; i < _subgraphDeploymentID.length; i++) { + _vote(_subgraphDeploymentID[i], _deny[i], _oracleIndex); + } + } + + /** + * @dev Vote deny or allow for a subgraph. + * When oracles cast their votes we store the timestamp of the vote. + * Check if the execution threshold has been reached for a subgraph. + * If execution threshold is reached we call the RewardsManager to set the correct state. + * @param _subgraphDeploymentID Subgraph deployment ID + * @param _deny True to deny, false to allow + * @param _oracleIndex Index of the oracle voting + */ + function _vote(bytes32 _subgraphDeploymentID, bool _deny, uint256 _oracleIndex) private { + uint256 timestamp = block.timestamp; + + if (_deny) { + lastDenyVote[currentNonce][_subgraphDeploymentID][_oracleIndex] = timestamp; + // clear opposite vote for a subgraph deployment if it exists + lastAllowVote[currentNonce][_subgraphDeploymentID][_oracleIndex] = 0; + } else { + lastAllowVote[currentNonce][_subgraphDeploymentID][_oracleIndex] = timestamp; + // clear opposite vote for a subgraph deployment if it exists + lastDenyVote[currentNonce][_subgraphDeploymentID][_oracleIndex] = 0; + } + + emit OracleVote(_subgraphDeploymentID, _deny, _oracleIndex, timestamp); + + // check if execution threshold is reached, if it is call the RewardsManager + if (checkVotes(_subgraphDeploymentID, _deny)) { + rewardsManager.setDenied(_subgraphDeploymentID, _deny); + } + } + + /** + * @dev Check if the execution threshold has been reached for a subgraph. + * For a vote to be valid it needs to be within the vote time limit. + * @param _subgraphDeploymentID Subgraph deployment ID + * @param _deny True to deny, false to allow + * @return True if execution threshold is reached + */ + function checkVotes(bytes32 _subgraphDeploymentID, bool _deny) public view returns (bool) { + uint256 votes; + + // timeframe for a vote to be valid + uint256 voteTimeValidity = block.timestamp - voteTimeLimit; + + // corresponding votes based on _deny for a subgraph deployment + uint256[NUM_ORACLES] storage lastVoteForSubgraph = _deny + ? lastDenyVote[currentNonce][_subgraphDeploymentID] + : lastAllowVote[currentNonce][_subgraphDeploymentID]; + + for (uint256 i; i < NUM_ORACLES; i++) { + // check if vote is within the vote time limit + if (lastVoteForSubgraph[i] > voteTimeValidity) { + votes++; + } + + // check if execution threshold is reached + if (votes == executionThreshold) { + return true; + } + } + + return false; + } +} diff --git a/packages/contracts/test/e2e/deployment/config/l1/rewardsManager.test.ts b/packages/contracts/test/e2e/deployment/config/l1/rewardsManager.test.ts index 0f9807d92..b7781fd96 100644 --- a/packages/contracts/test/e2e/deployment/config/l1/rewardsManager.test.ts +++ b/packages/contracts/test/e2e/deployment/config/l1/rewardsManager.test.ts @@ -1,17 +1,26 @@ import { expect } from 'chai' import hre from 'hardhat' import { isGraphL2ChainId } from '@graphprotocol/sdk' +import { NamedAccounts } from '@graphprotocol/sdk/gre' describe('[L1] RewardsManager configuration', () => { const graph = hre.graph() const { RewardsManager } = graph.contracts - before(function () { + let namedAccounts: NamedAccounts + + before(async function () { if (isGraphL2ChainId(graph.chainId)) this.skip() + namedAccounts = await graph.getNamedAccounts() }) it('issuancePerBlock should match "issuancePerBlock" in the config file', async function () { const value = await RewardsManager.issuancePerBlock() expect(value).eq('114693500000000000000') // hardcoded as it's set with a function call rather than init parameter }) + + it('should allow subgraph availability oracle to deny rewards', async function () { + const availabilityOracle = await RewardsManager.subgraphAvailabilityOracle() + expect(availabilityOracle).eq(namedAccounts.availabilityOracle.address) + }) }) diff --git a/packages/contracts/test/e2e/deployment/config/l2/rewardsManager.test.ts b/packages/contracts/test/e2e/deployment/config/l2/rewardsManager.test.ts index 9759ba78d..c324dc6bf 100644 --- a/packages/contracts/test/e2e/deployment/config/l2/rewardsManager.test.ts +++ b/packages/contracts/test/e2e/deployment/config/l2/rewardsManager.test.ts @@ -4,7 +4,7 @@ import hre from 'hardhat' describe('[L2] RewardsManager configuration', () => { const graph = hre.graph() - const { RewardsManager } = graph.contracts + const { RewardsManager, SubgraphAvailabilityManager } = graph.contracts before(function () { if (isGraphL1ChainId(graph.chainId)) this.skip() @@ -14,4 +14,9 @@ describe('[L2] RewardsManager configuration', () => { const value = await RewardsManager.issuancePerBlock() expect(value).eq('6036500000000000000') // hardcoded as it's set with a function call rather than init parameter }) + + it('should allow subgraph availability manager to deny rewards', async function () { + const availabilityOracle = await RewardsManager.subgraphAvailabilityOracle() + expect(availabilityOracle).eq(SubgraphAvailabilityManager.address) + }) }) diff --git a/packages/contracts/test/e2e/deployment/config/rewardsManager.test.ts b/packages/contracts/test/e2e/deployment/config/rewardsManager.test.ts index c0005f81b..e120392b6 100644 --- a/packages/contracts/test/e2e/deployment/config/rewardsManager.test.ts +++ b/packages/contracts/test/e2e/deployment/config/rewardsManager.test.ts @@ -1,26 +1,13 @@ import { expect } from 'chai' import hre from 'hardhat' -import { NamedAccounts } from '@graphprotocol/sdk/gre' describe('RewardsManager configuration', () => { const { - getNamedAccounts, contracts: { RewardsManager, Controller }, } = hre.graph() - let namedAccounts: NamedAccounts - - before(async () => { - namedAccounts = await getNamedAccounts() - }) - it('should be controlled by Controller', async function () { const controller = await RewardsManager.controller() expect(controller).eq(Controller.address) }) - - it('should allow subgraph availability oracle to deny rewards', async function () { - const availabilityOracle = await RewardsManager.subgraphAvailabilityOracle() - expect(availabilityOracle).eq(namedAccounts.availabilityOracle.address) - }) }) diff --git a/packages/contracts/test/unit/rewards/rewards.test.ts b/packages/contracts/test/unit/rewards/rewards.test.ts index 1070b9ff3..089ab3801 100644 --- a/packages/contracts/test/unit/rewards/rewards.test.ts +++ b/packages/contracts/test/unit/rewards/rewards.test.ts @@ -213,21 +213,6 @@ describe('Rewards', () => { .withArgs(subgraphDeploymentID1, blockNum + 1) expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(true) }) - - it('reject deny subgraph w/ many if not the oracle', async function () { - const deniedSubgraphs = [subgraphDeploymentID1, subgraphDeploymentID2] - const tx = rewardsManager.connect(oracle).setDeniedMany(deniedSubgraphs, [true, true]) - await expect(tx).revertedWith('Caller must be the subgraph availability oracle') - }) - - it('should deny subgraph w/ many', async function () { - await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(oracle.address) - - const deniedSubgraphs = [subgraphDeploymentID1, subgraphDeploymentID2] - await rewardsManager.connect(oracle).setDeniedMany(deniedSubgraphs, [true, true]) - expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(true) - expect(await rewardsManager.isDenied(subgraphDeploymentID2)).eq(true) - }) }) }) diff --git a/packages/contracts/test/unit/rewards/subgraphAvailability.test.ts b/packages/contracts/test/unit/rewards/subgraphAvailability.test.ts new file mode 100644 index 000000000..93352a5b1 --- /dev/null +++ b/packages/contracts/test/unit/rewards/subgraphAvailability.test.ts @@ -0,0 +1,460 @@ +import hre from 'hardhat' +import { expect } from 'chai' +import { constants } from 'ethers' + +import { ethers } from 'hardhat' + +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +import { SubgraphAvailabilityManager } from '../../../build/types/SubgraphAvailabilityManager' +import { IRewardsManager } from '../../../build/types/IRewardsManager' + +import { NetworkFixture } from '../lib/fixtures' + +import { + deploy, + DeployType, + GraphNetworkContracts, + randomAddress, + randomHexBytes, +} from '@graphprotocol/sdk' + +const { AddressZero } = constants + +describe('SubgraphAvailabilityManager', () => { + const graph = hre.graph() + let me: SignerWithAddress + let governor: SignerWithAddress + + let oracles: string[] + let oracleOne: SignerWithAddress + let oracleTwo: SignerWithAddress + let oracleThree: SignerWithAddress + let oracleFour: SignerWithAddress + let oracleFive: SignerWithAddress + + let newOracle: SignerWithAddress + + let fixture: NetworkFixture + + const executionThreshold = '3' + const voteTimeLimit = '5' // 5 seconds + + let contracts: GraphNetworkContracts + let rewardsManager: IRewardsManager + let subgraphAvailabilityManager: SubgraphAvailabilityManager + + const subgraphDeploymentID1 = randomHexBytes() + const subgraphDeploymentID2 = randomHexBytes() + const subgraphDeploymentID3 = randomHexBytes() + + before(async () => { + [me, oracleOne, oracleTwo, oracleThree, oracleFour, oracleFive, newOracle] + = await graph.getTestAccounts() + ;({ governor } = await graph.getNamedAccounts()) + + oracles = [ + oracleOne.address, + oracleTwo.address, + oracleThree.address, + oracleFour.address, + oracleFive.address, + ] + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + rewardsManager = contracts.RewardsManager as IRewardsManager + const deployResult = await deploy(DeployType.Deploy, governor, { + name: 'SubgraphAvailabilityManager', + args: [governor.address, rewardsManager.address, executionThreshold, voteTimeLimit, oracles], + }) + subgraphAvailabilityManager = deployResult.contract as SubgraphAvailabilityManager + + await rewardsManager + .connect(governor) + .setSubgraphAvailabilityOracle(subgraphAvailabilityManager.address) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('deployment', () => { + it('should deploy', function () { + expect(subgraphAvailabilityManager.address).to.be.properAddress + }) + + it('should revert if oracles array is less than 5', async () => { + await expect( + deploy(DeployType.Deploy, governor, { + name: 'SubgraphAvailabilityManager', + args: [ + governor.address, + rewardsManager.address, + executionThreshold, + voteTimeLimit, + [oracleOne.address, oracleTwo.address, oracleThree.address, oracleFour.address], + ], + }), + ).to.be.reverted + }) + + it('should revert if an oracle is address zero', async () => { + await expect( + deploy(DeployType.Deploy, governor, { + name: 'SubgraphAvailabilityManager', + args: [ + governor.address, + rewardsManager.address, + executionThreshold, + voteTimeLimit, + [ + AddressZero, + oracleTwo.address, + oracleThree.address, + oracleFour.address, + oracleFive.address, + ], + ], + }), + ).to.be.revertedWith('SAM: oracle cannot be address zero') + }) + + it('should revert if governor is address zero', async () => { + await expect( + deploy(DeployType.Deploy, governor, { + name: 'SubgraphAvailabilityManager', + args: [AddressZero, rewardsManager.address, executionThreshold, voteTimeLimit, oracles], + }), + ).to.be.revertedWith('SAM: governor must be set') + }) + + it('should revert if rewardsManager is address zero', async () => { + await expect( + deploy(DeployType.Deploy, governor, { + name: 'SubgraphAvailabilityManager', + args: [governor.address, AddressZero, executionThreshold, voteTimeLimit, oracles], + }), + ).to.be.revertedWith('SAM: rewardsManager must be set') + }) + + it('should revert if executionThreshold is too low', async () => { + await expect( + deploy(DeployType.Deploy, governor, { + name: 'SubgraphAvailabilityManager', + args: [governor.address, rewardsManager.address, '2', voteTimeLimit, oracles], + }), + ).to.be.revertedWith('SAM: executionThreshold too low') + }) + + it('should revert if executionThreshold is too high', async () => { + await expect( + deploy(DeployType.Deploy, governor, { + name: 'SubgraphAvailabilityManager', + args: [governor.address, rewardsManager.address, '6', voteTimeLimit, oracles], + }), + ).to.be.revertedWith('SAM: executionThreshold too high') + }) + }) + + describe('initializer', () => { + it('should init governor', async () => { + expect(await subgraphAvailabilityManager.governor()).to.be.equal(governor.address) + }) + + it('should init executionThreshold', async () => { + expect(await subgraphAvailabilityManager.executionThreshold()).to.be.equal(executionThreshold) + }) + + it('should init voteTimeLimit', async () => { + expect(await subgraphAvailabilityManager.voteTimeLimit()).to.be.equal(voteTimeLimit) + }) + + it('should init oracles', async () => { + for (let i = 0; i < oracles.length; i++) { + expect(await subgraphAvailabilityManager.oracles(i)).to.be.equal(oracles[i]) + } + }) + }) + + describe('set vote limit', function () { + it('sets voteTimeLimit successfully', async () => { + const newVoteTimeLimit = 10 + await expect(subgraphAvailabilityManager.connect(governor).setVoteTimeLimit(newVoteTimeLimit)) + .emit(subgraphAvailabilityManager, 'VoteTimeLimitSet') + .withArgs(newVoteTimeLimit) + expect(await subgraphAvailabilityManager.voteTimeLimit()).to.be.equal(newVoteTimeLimit) + }) + + it('should fail if not called by governor', async () => { + const newVoteTimeLimit = 10 + await expect( + subgraphAvailabilityManager.connect(me).setVoteTimeLimit(newVoteTimeLimit), + ).to.be.revertedWith('Only Governor can call') + }) + }) + + describe('set oracles', () => { + it('sets an oracle successfully', async () => { + const oracle = randomAddress() + await expect(subgraphAvailabilityManager.connect(governor).setOracle(0, oracle)) + .emit(subgraphAvailabilityManager, 'OracleSet') + .withArgs(0, oracle) + expect(await subgraphAvailabilityManager.oracles(0)).to.be.equal(oracle) + }) + + it('should fail if not called by governor', async () => { + const oracle = randomAddress() + await expect(subgraphAvailabilityManager.connect(me).setOracle(0, oracle)).to.be.revertedWith( + 'Only Governor can call', + ) + }) + + it('should fail if setting oracle to address zero', async () => { + await expect( + subgraphAvailabilityManager.connect(governor).setOracle(0, AddressZero), + ).to.revertedWith('SAM: oracle cannot be address zero') + }) + + it('should fail if index is out of bounds', async () => { + const oracle = randomAddress() + await expect( + subgraphAvailabilityManager.connect(governor).setOracle(5, oracle), + ).to.be.revertedWith('SAM: index out of bounds') + }) + }) + + describe('voting', function () { + it('votes denied successfully', async () => { + const denied = true + const tx = await subgraphAvailabilityManager + .connect(oracleOne) + .vote(subgraphDeploymentID1, denied, 0) + const timestamp = (await ethers.provider.getBlock('latest')).timestamp + await expect(tx) + .to.emit(subgraphAvailabilityManager, 'OracleVote') + .withArgs(subgraphDeploymentID1, denied, 0, timestamp) + }) + + it('should fail if not called by oracle', async () => { + const denied = true + await expect( + subgraphAvailabilityManager.connect(me).vote(subgraphDeploymentID1, denied, 0), + ).to.be.revertedWith('SAM: caller must be oracle') + }) + + it('should fail if index is out of bounds', async () => { + const denied = true + await expect( + subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 5), + ).to.be.revertedWith('SAM: index out of bounds') + }) + + it('should fail if oracle used an incorrect index', async () => { + const denied = true + await expect( + subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 1), + ).to.be.revertedWith('SAM: caller must be oracle') + }) + + it('should still be allowed if only one oracle has voted', async () => { + const denied = true + const tx = await subgraphAvailabilityManager + .connect(oracleOne) + .vote(subgraphDeploymentID1, denied, 0) + await expect(tx).to.emit(subgraphAvailabilityManager, 'OracleVote') + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.false + }) + + it('should be denied or allowed if majority of oracles have voted', async () => { + // 3/5 oracles vote denied = true + let denied = true + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + await subgraphAvailabilityManager.connect(oracleTwo).vote(subgraphDeploymentID1, denied, 1) + const tx = await subgraphAvailabilityManager + .connect(oracleThree) + .vote(subgraphDeploymentID1, denied, 2) + await expect(tx) + .to.emit(rewardsManager, 'RewardsDenylistUpdated') + .withArgs(subgraphDeploymentID1, tx.blockNumber) + + // check events order + const receipt = await tx.wait() + expect(receipt.events[0].event).to.be.equal('OracleVote') + const rewardsManangerEvent = rewardsManager.interface.parseLog(receipt.logs[1]).name + expect(rewardsManangerEvent).to.be.equal('RewardsDenylistUpdated') + + // check that subgraph is denied + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.true + + // 3/5 oracles vote denied = false + denied = false + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + await subgraphAvailabilityManager.connect(oracleTwo).vote(subgraphDeploymentID1, denied, 1) + await subgraphAvailabilityManager.connect(oracleThree).vote(subgraphDeploymentID1, denied, 2) + + // check that subgraph is not denied + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.false + }) + + it('should not be denied if the same oracle votes three times', async () => { + const denied = true + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.false + }) + + it('should not be denied if voteTimeLimit has passed and not enough oracles have voted', async () => { + // 2/3 oracles vote denied = true + const denied = true + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + await subgraphAvailabilityManager.connect(oracleTwo).vote(subgraphDeploymentID1, denied, 1) + + // increase time by 6 seconds + await ethers.provider.send('evm_increaseTime', [6]) + // last oracle votes denied = true + const tx = await subgraphAvailabilityManager + .connect(oracleThree) + .vote(subgraphDeploymentID1, denied, 2) + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenylistUpdated') + + // subgraph state didn't change because enough time has passed so that + // previous votes are no longer valid + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.false + }) + + it('clears opposite vote when voting', async () => { + const denied = true + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + await subgraphAvailabilityManager.connect(oracleTwo).vote(subgraphDeploymentID1, denied, 1) + await subgraphAvailabilityManager.connect(oracleThree).vote(subgraphDeploymentID1, denied, 2) + + // 3/5 oracles vote denied = true so subgraph is denied + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.true + + // oracleOne changes its vote to denied = false + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, false, 0) + + // last deny vote should be 0 for oracleOne + expect( + await subgraphAvailabilityManager.lastDenyVote(0, subgraphDeploymentID1, 0), + ).to.be.equal(0) + + // executionThreshold isn't met now since oracleOne changed its vote + expect(await subgraphAvailabilityManager.checkVotes(subgraphDeploymentID1, denied)).to.be + .false + + // subgraph is still denied in rewards manager because only one oracle changed its vote + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.true + }) + }) + + describe('vote many', function () { + it('votes many successfully', async () => { + const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2, subgraphDeploymentID3] + const denied = [true, false, true] + const tx = await subgraphAvailabilityManager.connect(oracleOne).voteMany(subgraphs, denied, 0) + const timestamp = (await ethers.provider.getBlock('latest')).timestamp + await expect(tx) + .to.emit(subgraphAvailabilityManager, 'OracleVote') + .withArgs(subgraphDeploymentID1, true, 0, timestamp) + await expect(tx) + .to.emit(subgraphAvailabilityManager, 'OracleVote') + .withArgs(subgraphDeploymentID2, false, 0, timestamp) + await expect(tx) + .to.emit(subgraphAvailabilityManager, 'OracleVote') + .withArgs(subgraphDeploymentID3, true, 0, timestamp) + }) + + it('should change subgraph state if majority of oracles have voted', async () => { + const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2, subgraphDeploymentID3] + const denied = [true, false, true] + // 3/5 oracles vote denied = true + await subgraphAvailabilityManager.connect(oracleOne).voteMany(subgraphs, denied, 0) + await subgraphAvailabilityManager.connect(oracleTwo).voteMany(subgraphs, denied, 1) + + const tx = await subgraphAvailabilityManager + .connect(oracleThree) + .voteMany(subgraphs, denied, 2) + + await expect(tx) + .to.emit(rewardsManager, 'RewardsDenylistUpdated') + .withArgs(subgraphDeploymentID1, tx.blockNumber) + await expect(tx) + .to.emit(rewardsManager, 'RewardsDenylistUpdated') + .withArgs(subgraphDeploymentID2, 0) + await expect(tx) + .to.emit(rewardsManager, 'RewardsDenylistUpdated') + .withArgs(subgraphDeploymentID3, tx.blockNumber) + + // check that subgraphs are denied + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.true + expect(await rewardsManager.isDenied(subgraphDeploymentID2)).to.be.false + expect(await rewardsManager.isDenied(subgraphDeploymentID3)).to.be.true + }) + + it('should fail if not called by oracle', async () => { + const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2, subgraphDeploymentID3] + const denied = [true, false, true] + await expect( + subgraphAvailabilityManager.connect(me).voteMany(subgraphs, denied, 0), + ).to.be.revertedWith('SAM: caller must be oracle') + }) + + it('should fail if index is out of bounds', async () => { + const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2, subgraphDeploymentID3] + const denied = [true, false, true] + await expect( + subgraphAvailabilityManager.connect(oracleOne).voteMany(subgraphs, denied, 5), + ).to.be.revertedWith('SAM: index out of bounds') + }) + + it('should fail if oracle used an incorrect index', async () => { + const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2, subgraphDeploymentID3] + const denied = [true, false, true] + await expect( + subgraphAvailabilityManager.connect(oracleOne).voteMany(subgraphs, denied, 1), + ).to.be.revertedWith('SAM: caller must be oracle') + }) + }) + + describe('refreshing votes', () => { + it('should refresh votes if an oracle is replaced', async () => { + const denied = true + // 2/3 oracles vote denied = true + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + await subgraphAvailabilityManager.connect(oracleTwo).vote(subgraphDeploymentID1, denied, 1) + + // replace oracleOne with a new oracle + await subgraphAvailabilityManager.connect(governor).setOracle(2, newOracle.address) + + // new oracle votes denied = true + await subgraphAvailabilityManager.connect(newOracle).vote(subgraphDeploymentID1, denied, 2) + + // subgraph shouldn't be denied because setting a new oracle should refresh the votes + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.false + }) + + it('should refresh votes if voteTimeLimit changes', async () => { + const denied = true + // 2/3 oracles vote denied = true + await subgraphAvailabilityManager.connect(oracleOne).vote(subgraphDeploymentID1, denied, 0) + await subgraphAvailabilityManager.connect(oracleTwo).vote(subgraphDeploymentID1, denied, 1) + + // change voteTimeLimit to 10 seconds + await subgraphAvailabilityManager.connect(governor).setVoteTimeLimit(10) + + // last oracle votes denied = true + await subgraphAvailabilityManager.connect(oracleThree).vote(subgraphDeploymentID1, denied, 2) + + // subgraph shouldn't be denied because voteTimeLimit should refresh the votes + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).to.be.false + }) + }) +}) diff --git a/packages/sdk/src/deployments/lib/types/contract.ts b/packages/sdk/src/deployments/lib/types/contract.ts index 754ef4b1b..700689445 100644 --- a/packages/sdk/src/deployments/lib/types/contract.ts +++ b/packages/sdk/src/deployments/lib/types/contract.ts @@ -2,4 +2,4 @@ import type { BigNumber, Contract } from 'ethers' export type ContractList = Partial> -export type ContractParam = string | BigNumber | number +export type ContractParam = string | BigNumber | number | Array diff --git a/packages/sdk/src/deployments/network/deployment/contracts/list.ts b/packages/sdk/src/deployments/network/deployment/contracts/list.ts index eeb2a4078..1b0dc1730 100644 --- a/packages/sdk/src/deployments/network/deployment/contracts/list.ts +++ b/packages/sdk/src/deployments/network/deployment/contracts/list.ts @@ -34,6 +34,7 @@ export const GraphNetworkL2ContractNameList = [ 'L2GNS', 'L2Staking', 'L2GraphTokenGateway', + 'SubgraphAvailabilityManager', ] as const export const GraphNetworkContractNameList = [ diff --git a/packages/sdk/src/deployments/network/deployment/contracts/load.ts b/packages/sdk/src/deployments/network/deployment/contracts/load.ts index 74ab9e1a8..d453ae5d9 100644 --- a/packages/sdk/src/deployments/network/deployment/contracts/load.ts +++ b/packages/sdk/src/deployments/network/deployment/contracts/load.ts @@ -41,6 +41,7 @@ import type { L2GNS, L2Curation, StakingExtension, + SubgraphAvailabilityManager, } from '@graphprotocol/contracts' import { ContractList } from '../../../lib/types/contract' import { loadArtifact } from '../../../lib/deploy/artifacts' @@ -63,6 +64,7 @@ export interface GraphNetworkContracts extends ContractList { const accounts = await accountsPromise - const address = getItemValue(readConfig(graphConfigPath, true), `general/${name}`) - accounts[name] = await SignerWithAddress.create(provider.getSigner(address)) + let address + try { + address = getItemValue(readConfig(graphConfigPath, true), `general/${name}`) + } catch (e) { + // Skip if not found + } + if (address) { + accounts[name] = await SignerWithAddress.create(provider.getSigner(address)) + } return accounts }, Promise.resolve({} as NamedAccounts), @@ -46,10 +53,13 @@ export async function getTestAccounts( ): Promise { // Get list of privileged accounts we don't want as test accounts const namedAccounts = await getNamedAccounts(provider, graphConfigPath) - const blacklist = namedAccountList.map((a) => { - const account = namedAccounts[a] - return account.address - }) + const blacklist = namedAccountList.reduce((accounts: string[], name) => { + const account = namedAccounts[name] + if (account) { + accounts.push(account.address) + } + return accounts + }, []) blacklist.push((await getDeployer(provider)).address) // Get signers and filter out blacklisted accounts