diff --git a/.changeset/smooth-balloons-stare.md b/.changeset/smooth-balloons-stare.md new file mode 100644 index 000000000..66a578e07 --- /dev/null +++ b/.changeset/smooth-balloons-stare.md @@ -0,0 +1,5 @@ +--- +"@graphprotocol/contracts": patch +--- + +make sdk and console table printer a dev dep diff --git a/.github/workflows/e2e-contracts.yml b/.github/workflows/e2e-contracts.yml deleted file mode 100644 index ce5540add..000000000 --- a/.github/workflows/e2e-contracts.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: E2E - packages/contracts - -env: - CI: true - -on: - push: - branches: "*" - paths: - - packages/contracts/contracts/** - - packages/contracts/config/** - - packages/contracts/test/e2e/** - - packages/contracts/tasks/** - - packages/contracts/scripts/** - - packages/contracts/hardhat.config.ts - pull_request: - branches: "*" - paths: - - packages/contracts/contracts/** - - packages/contracts/config/** - - packages/contracts/test/e2e/** - - packages/contracts/tasks/** - - packages/contracts/scripts/** - - packages/contracts/hardhat.config.ts - workflow_dispatch: - -jobs: - test-e2e: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Set up environment - uses: ./.github/actions/setup - - name: Build - run: | - pushd packages/contracts - yarn build || yarn build - - name: Run e2e tests - run: | - git clone https://github.com/OffchainLabs/nitro-testnode/ - pushd nitro-testnode - git checkout c47cb8c643bc8e63ff096f7f88f9152064d1532a - git submodule update --init --recursive - sed -i'' -e 's/^\(.*dev.period.*\)/# \1/' docker-compose.yaml - ./test-node.bash --init --batchposters 0 --redundantsequencers 0 --detach - popd - pushd packages/contracts - L1_NETWORK=localnitrol1 L2_NETWORK=localnitrol2 yarn test:e2e diff --git a/README.md b/README.md index eb1b61d06..85de38b1e 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,8 @@ Build - - CI - - - E2E + + CI-Contracts

@@ -37,13 +34,13 @@ This repository is a Yarn workspaces monorepo containing the following packages: | Package | Latest version | Description | | --- | --- | --- | | [contracts](./packages/contracts) | [![npm version](https://badge.fury.io/js/@graphprotocol%2Fcontracts.svg)](https://badge.fury.io/js/@graphprotocol%2Fcontracts) | Contracts enabling the open and permissionless decentralized network known as The Graph protocol. | -| [eslint-graph-config](./packages/eslint-graph-config) | [![npm version]()]() | Shared linting and formatting rules for TypeScript projects. | -| [horizon](./packages/horizon) | [![npm version]()]() | Contracts for Graph Horizon, the next iteration of The Graph protocol. | +| [eslint-graph-config](./packages/eslint-graph-config) | - | Shared linting and formatting rules for TypeScript projects. | +| [horizon](./packages/horizon) | - | Contracts for Graph Horizon, the next iteration of The Graph protocol. | | [sdk](./packages/sdk) | [![npm version](https://badge.fury.io/js/@graphprotocol%2Fsdk.svg)](https://badge.fury.io/js/@graphprotocol%2Fsdk) | TypeScript based SDK to interact with the protocol contracts | -| [solhint-graph-config](./packages/solhint-graph-config) | [![npm version]()]() | Shared linting and formatting rules for Solidity projects. | -| [solhint-plugin-graph](./packages/solhint-plugin-graph) | [![npm version]()]() | Plugin for Solhint with specific Graph linting rules. | -| [subgraph-service](./packages/subgraph-service) | [![npm version]()]() | Contracts for the Subgraph data service in Graph Horizon. | -| [token-distribution](./packages/token-distribution) | - | Contracts managing token locks for network participants | +| [solhint-graph-config](./packages/solhint-graph-config) | - | Shared linting and formatting rules for Solidity projects. | +| [solhint-plugin-graph](./packages/solhint-plugin-graph) | - | Plugin for Solhint with specific Graph linting rules. | +| [subgraph-service](./packages/subgraph-service) | - | Contracts for the Subgraph data service in Graph Horizon. | +| [token-distribution](./packages/token-distribution) | [![npm version](https://badge.fury.io/js/@graphprotocol%2Ftoken-distribution.svg)](https://badge.fury.io/js/@graphprotocol%2Ftoken-distribution) | Contracts managing token locks for network participants | ## Development diff --git a/packages/contracts/CHANGELOG.md b/packages/contracts/CHANGELOG.md index cdbb2c892..388f30b1f 100644 --- a/packages/contracts/CHANGELOG.md +++ b/packages/contracts/CHANGELOG.md @@ -1,10 +1,10 @@ # @graphprotocol/contracts -## 7.0.0 +## 6.3.0 -### Major Changes +### Minor Changes -- 9686ce1: Add Horizon staking interface +- Remove restriction that prevented closing allocations older than 1 epoch. ## 6.2.1 diff --git a/packages/contracts/addresses-staging.json b/packages/contracts/addresses-staging.json index 8f67d8e2b..5bf3369ef 100644 --- a/packages/contracts/addresses-staging.json +++ b/packages/contracts/addresses-staging.json @@ -134,12 +134,12 @@ "txHash": "0x5ae51383f5ad1e434a78c2a6d5393d497be315c40287d827c63c4ca48cc66aca", "proxy": true, "implementation": { - "address": "0xC4Cfde877Bc77fD93378D33Ec26330127Ed93bbE", - "creationCodeHash": "0x9319aaf6b70b423fa04ad747f004961123e7833b269f69ecff902db13b010fa2", - "runtimeCodeHash": "0x9a3f54b5f61709d40a71892f241f61fc72cf95346e5d928f1d699ea57065efc4", - "txHash": "0x1e53eadecdb33a8e90e5c9bc3d02d438dd21217c5017b596b3e51f64be4501e2", + "address": "0x0a6b1efb7b2e9A27Cf7C5ED1bdF1Bd03bF8F5600", + "creationCodeHash": "0x60bc1fda6913ef750a1d931e87068d0f6cd62b8b42a4fff13b304b9c0c278557", + "runtimeCodeHash": "0xd8bbd97b94f8ac45a479c55c0416e7b0ebef6a99da8ef7580bebaaad1d8d74d9", + "txHash": "0x5d9276e47f3d1fa1fe5c6d826534722aa73d47f5a114e844de73a3cf0cb22d6b", "libraries": { - "LibExponential": "0x6f6Db03f7F3bBE43669F81FD954Bd083FEdBb4a5" + "LibExponential": "0xb7fB963257d4f31F88021d2D7Ee53688e38A0938" } } }, @@ -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 ae66613a2..f9c7a1f71 100644 --- a/packages/contracts/addresses.json +++ b/packages/contracts/addresses.json @@ -705,12 +705,12 @@ "txHash": "0xa33c0d58ddaed7e3f7381a33e3d5f63e39219863019f00d54ce2fd2446076ac7", "proxy": true, "implementation": { - "address": "0x4a886d3E44C7731Dcf888Da704CA5C51ed63DfC8", - "creationCodeHash": "0xa9796308a637b0bfe091f32c1019e4db8efe4bab80788c052fa334e6810c3a98", - "runtimeCodeHash": "0x19d3491cab54b2aae04d05525a532200e946ce9b55573b58f2e3e1606b4514be", - "txHash": "0xc3235306a51c20b28a8b05da69859e03a081a84c9914156c79da07dcc45b3b4e", + "address": "0x0Dae36adCbE384a31309269448E09465B2288429", + "creationCodeHash": "0x7bd2944ba001c51a42d88f89b10eb894e11be62ee96e76331309ff6a8e9b20ff", + "runtimeCodeHash": "0x0808fafef220cfddb64a51f680a18e455d5b7c6b9c0c7aad8f8067f4775dd204", + "txHash": "0x6bdee0f03f74df2b7e6141075b06a88b201cbc78a131aa0fe6bb1ef1da1e2dc1", "libraries": { - "LibExponential": "0x208f638d8804e4ccc874ec39e240feea3dc289ee" + "LibExponential": "0x6F436161bBa439FB7D0A6192D22dd8fcE4C26Fb5" } } }, @@ -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": { @@ -1146,12 +1165,12 @@ "txHash": "0x326cf1f2849da4bb4d7e39f2783779e3c99fa48e4ee8ef004cfdd50c62e775df", "proxy": true, "implementation": { - "address": "0xD07dFD514dc1b57020e6C1F49e05c48d0658C99f", - "creationCodeHash": "0x6a763345e5f166ea4e73ce9a116a49c9fc0833d9ea235a86fa5a997e91cf09e5", - "runtimeCodeHash": "0xb4c31859ac132241f04c802d4add70a94c7f2c6eb9dfd4bf224048d249dbc7bc", - "txHash": "0x68b34eda64287b84582c8f005c4e96162252d36c9c5c9b84332336a7c2e3d6d3", + "address": "0x64Ed77b164d3B22339DA4DB6d56a1C1d8A051c0A", + "creationCodeHash": "0x56a3dd587f3f4ae38dd782e9c35125fa7015c708394950de90bc190204502438", + "runtimeCodeHash": "0x9040ccf84a89ba2e7a32eb297f0da148827e7e272b20d9e57b1acf749baa35da", + "txHash": "0xbfec14a2dd1a571612076b71b4f2e78ea18f95d7d80e14cd1456ecaa7466db54", "libraries": { - "LibExponential": "0xd844116f6d79a280b117Bb6d9EBf4121D4e8B44b" + "LibExponential": "0x413d16eF53d3dd8b7e769570115ee5419CF77C98" } } }, @@ -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/audits/OpenZeppelin/2023-08-dispute-manager-status.pdf b/packages/contracts/audits/OpenZeppelin/2023-08-dispute-manager-status.pdf new file mode 100644 index 000000000..e5030fcdf Binary files /dev/null and b/packages/contracts/audits/OpenZeppelin/2023-08-dispute-manager-status.pdf differ diff --git a/packages/contracts/audits/OpenZeppelin/2023-11-permissionless-payers.pdf b/packages/contracts/audits/OpenZeppelin/2023-11-permissionless-payers.pdf new file mode 100644 index 000000000..3a37b9158 Binary files /dev/null and b/packages/contracts/audits/OpenZeppelin/2023-11-permissionless-payers.pdf differ diff --git a/packages/contracts/audits/OpenZeppelin/2023-11-remove-delegation-parameters-cooldown.pdf b/packages/contracts/audits/OpenZeppelin/2023-11-remove-delegation-parameters-cooldown.pdf new file mode 100644 index 000000000..d99daa31f Binary files /dev/null and b/packages/contracts/audits/OpenZeppelin/2023-11-remove-delegation-parameters-cooldown.pdf differ diff --git a/packages/contracts/audits/OpenZeppelin/2024-02-graph-availability-manager-minimum-allocation-removal.pdf b/packages/contracts/audits/OpenZeppelin/2024-02-graph-availability-manager-minimum-allocation-removal.pdf new file mode 100644 index 000000000..d41a64e75 Binary files /dev/null and b/packages/contracts/audits/OpenZeppelin/2024-02-graph-availability-manager-minimum-allocation-removal.pdf differ 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 3b6bf3ff6..a1f9a3ba6 100644 --- a/packages/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/contracts/contracts/rewards/IRewardsManager.sol @@ -27,8 +27,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 a9b3b61e2..a02e4518b 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -139,22 +139,6 @@ contract RewardsManager is RewardsManagerV5Storage, 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/contracts/staking/Staking.sol b/packages/contracts/contracts/staking/Staking.sol index 6aef50efc..eaafdee0c 100644 --- a/packages/contracts/contracts/staking/Staking.sol +++ b/packages/contracts/contracts/staking/Staking.sol @@ -796,18 +796,18 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M // Get allocation Allocation memory alloc = __allocations[_allocationID]; - // Validate that an allocation cannot be closed before one epoch alloc.closedAtEpoch = epochManager().currentEpoch(); + + // Allocation duration in epochs uint256 epochs = MathUtils.diffOrZero(alloc.closedAtEpoch, alloc.createdAtEpoch); - require(epochs > 0, " 0) { // Distribute rewards if proof of indexing was presented by the indexer or operator - if (isIndexer && _poi != 0) { + // and the allocation is at least one epoch old (most indexed chains require the EBO + // posting epoch block numbers to produce a valid POI which happens once per epoch) + if (isIndexerOrOperator && _poi != 0 && epochs > 0) { _distributeRewards(_allocationID, alloc.indexer); } else { _updateRewards(alloc.subgraphDeploymentID); @@ -842,7 +844,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M _allocationID, msg.sender, _poi, - !isIndexer + !isIndexerOrOperator ); } diff --git a/packages/contracts/package.json b/packages/contracts/package.json index f4ecce85a..cde47f6eb 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -13,15 +13,12 @@ "README.md", "addresses.json" ], - "dependencies": { - "@graphprotocol/sdk": "workspace:^0.5.0", - "console-table-printer": "^2.11.1" - }, "devDependencies": { "@arbitrum/sdk": "~3.1.13", "@defi-wonderland/smock": "~2.3.0", "@ethersproject/experimental": "^5.6.0", "@graphprotocol/common-ts": "^1.8.3", + "@graphprotocol/sdk": "workspace:^0.5.0", "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-etherscan": "^3.1.7", "@nomiclabs/hardhat-waffle": "2.0.3", @@ -49,6 +46,7 @@ "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "cli-table": "^0.3.6", + "console-table-printer": "^2.11.1", "dotenv": "^9.0.0", "eslint": "^8.57.0", "eslint-graph-config": "workspace:^0.0.1", diff --git a/packages/contracts/scripts/ops/testDisputeConflict/acceptDispute.ts b/packages/contracts/scripts/ops/testDisputeConflict/acceptDispute.ts new file mode 100644 index 000000000..013daec30 --- /dev/null +++ b/packages/contracts/scripts/ops/testDisputeConflict/acceptDispute.ts @@ -0,0 +1,20 @@ +import { Wallet } from 'ethers' +import hre from 'hardhat' + +async function main() { + const graph = hre.graph() + const arbitratorPrivateKey = process.env.ARBITRATOR_PRIVATE_KEY + const arbitrator = new Wallet(arbitratorPrivateKey, graph.provider) + console.log('Arbitrator:', arbitrator.address) + + const disputeId = '0x35e6e68aa71ee59cb710d8005563d63d644f11f2eee879eca9bc22f523c9fade' + console.log('Dispute ID:', disputeId) + + // Accept dispute + await graph.contracts.DisputeManager.connect(arbitrator).acceptDispute(disputeId) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/packages/contracts/scripts/ops/testDisputeConflict/createDispute.ts b/packages/contracts/scripts/ops/testDisputeConflict/createDispute.ts new file mode 100644 index 000000000..26df99a4a --- /dev/null +++ b/packages/contracts/scripts/ops/testDisputeConflict/createDispute.ts @@ -0,0 +1,51 @@ +import { + GraphChainId, + buildAttestation, + encodeAttestation, + randomHexBytes, +} from '@graphprotocol/sdk' +import hre from 'hardhat' + +async function main() { + const graph = hre.graph() + const deployer = await graph.getDeployer() + const [indexer] = await graph.getTestAccounts() + const indexerChannelPrivKey = '0x82226c70efbe0d9525a5f9dc85c29b11fed1f46798a416b7626e21fdd6518d08' + + console.log('Deployer:', deployer.address) + console.log('Indexer:', indexer.address) + + const receipt = { + requestCID: '0x8bec406793c8e1c5d4bd4e059833e95b7a9aeed6a118cbe335a79735836f9ff7', + responseCID: '0xbdfc41643b5ff8d55f6cdb50f05575e1fdf177fa54d98cae1b9c76d8b360ff57', + subgraphDeploymentID: '0xa3bfbfc6f53fd8a61b78e0b9a90c7fbe9ff290cba87b045bc476137fb2963cf9', + } + const receipt2 = { ...receipt, responseCID: randomHexBytes() } + + const attestation1 = await buildAttestation( + receipt, + indexerChannelPrivKey, + graph.contracts.DisputeManager.address, + graph.chainId as GraphChainId, + ) + const attestation2 = await buildAttestation( + receipt2, + indexerChannelPrivKey, + graph.contracts.DisputeManager.address, + graph.chainId as GraphChainId, + ) + + console.log('Attestation 1:', attestation1) + console.log('Attestation 2:', attestation2) + + // Create dispute + await graph.contracts.DisputeManager.connect(deployer).createQueryDisputeConflict( + encodeAttestation(attestation1), + encodeAttestation(attestation2), + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/packages/contracts/scripts/ops/testDisputeConflict/setupIndexer.ts b/packages/contracts/scripts/ops/testDisputeConflict/setupIndexer.ts new file mode 100644 index 000000000..9d2cd0ede --- /dev/null +++ b/packages/contracts/scripts/ops/testDisputeConflict/setupIndexer.ts @@ -0,0 +1,40 @@ +import { allocateFrom, deriveChannelKey, randomHexBytes, stake, toGRT } from '@graphprotocol/sdk' +import hre, { ethers } from 'hardhat' + +async function main() { + const graph = hre.graph() + const deployer = await graph.getDeployer() + const [indexer] = await graph.getTestAccounts() + + console.log('Deployer:', deployer.address) + console.log('Indexer:', indexer.address) + + const receipt = { + requestCID: '0x8bec406793c8e1c5d4bd4e059833e95b7a9aeed6a118cbe335a79735836f9ff7', + responseCID: '0xbdfc41643b5ff8d55f6cdb50f05575e1fdf177fa54d98cae1b9c76d8b360ff57', + subgraphDeploymentID: '0xa3bfbfc6f53fd8a61b78e0b9a90c7fbe9ff290cba87b045bc476137fb2963cf9', + } + + console.log('Receipt requestCID:', receipt.requestCID) + console.log('Receipt response CID:', receipt.responseCID) + console.log('Receipt subgraphDeploymentID:', receipt.subgraphDeploymentID) + + const indexerChannelKey = deriveChannelKey() + console.log('Indexer channel key:', indexerChannelKey.address) + console.log('Indexer channel key privKey:', indexerChannelKey.privKey) + + // Set up indexer + await deployer.sendTransaction({ value: toGRT('0.05'), to: indexer.address }) + await graph.contracts.GraphToken.connect(deployer).transfer(indexer.address, toGRT('100000')) + await stake(graph.contracts, indexer, { amount: toGRT('100000') }) + await allocateFrom(graph.contracts, indexer, { + channelKey: indexerChannelKey, + amount: toGRT('100000'), + subgraphDeploymentID: receipt.subgraphDeploymentID, + }) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/packages/contracts/tasks/contract/deploy.ts b/packages/contracts/tasks/contract/deploy.ts index 3571444d0..33de96863 100644 --- a/packages/contracts/tasks/contract/deploy.ts +++ b/packages/contracts/tasks/contract/deploy.ts @@ -1,4 +1,4 @@ -import { confirm, deploy, DeployType, GraphNetworkAddressBook } from '@graphprotocol/sdk' +import { confirm, deploy, DeployType } from '@graphprotocol/sdk' import { greTask } from '@graphprotocol/sdk/gre' greTask('contract:deploy', 'Deploy a contract') @@ -37,7 +37,7 @@ greTask('contract:deploy', 'Deploy a contract') name: taskArgs.contract, args: taskArgs.init?.split(',') || [], }, - new GraphNetworkAddressBook(taskArgs.addressBook, graph.chainId), + graph.addressBook, ) console.log(`Contract deployed at ${deployment.contract.address}`) }) 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 c9e0f6a28..7e977a9c0 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/contracts/test/unit/staking/allocation.test.ts b/packages/contracts/test/unit/staking/allocation.test.ts index 92afedca4..bd930eaba 100644 --- a/packages/contracts/test/unit/staking/allocation.test.ts +++ b/packages/contracts/test/unit/staking/allocation.test.ts @@ -19,6 +19,7 @@ import { toGRT, } from '@graphprotocol/sdk' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { IRewardsManager } from '../../../build/types' const { AddressZero } = constants @@ -93,6 +94,7 @@ describe('Staking:Allocation', () => { let epochManager: EpochManager let grt: GraphToken let staking: IStaking + let rewardsManager: IRewardsManager let libExponential: LibExponential // Test values @@ -375,6 +377,7 @@ describe('Staking:Allocation', () => { epochManager = contracts.EpochManager grt = contracts.GraphToken as GraphToken staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager as IRewardsManager const stakingName = isGraphL1ChainId(graph.chainId) ? 'L1Staking' : 'L2Staking' const entry = graph.addressBook.getEntry(stakingName) @@ -875,9 +878,24 @@ describe('Staking:Allocation', () => { await expect(tx).revertedWith('!active') }) - it('reject close before at least one epoch has passed', async function () { + it('allow close before one epoch has passed', async function () { + const currentEpoch = await epochManager.currentEpoch() + const beforeAlloc = await staking.getAllocation(allocationID) + const tx = staking.connect(indexer).closeAllocation(allocationID, poi) - await expect(tx).revertedWith(' { it('should close an allocation (by public) only if allocation is non-zero', async function () { // Reject to close if public address and under max allocation epochs const tx1 = staking.connect(me).closeAllocation(allocationID, poi) - await expect(tx1).revertedWith(' deployer: ${await contract.signer.getAddress()}`) - console.log(`> contract: ${contract.address}`) - console.log(`> tx: ${tx.hash} nonce:${tx.nonce} limit: ${tx.gasLimit.toString()} gas: ${tx.gasPrice.toNumber() / 1e9} (gwei)`) - - // The contract is NOT deployed yet; we must wait until it is mined - await contract.deployed() - console.log(`Done!`) - - // Update addresses.json - const chainId = (network.config.chainId).toString() - if (!addresses[chainId]) { - addresses[chainId] = {} - } - addresses[chainId]['EventfulDataEdge'] = contract.address - return fs.writeFile('addresses.json', JSON.stringify(addresses, null, 2)) -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) diff --git a/packages/data-edge/scripts/deploy.ts b/packages/data-edge/scripts/deploy.ts deleted file mode 100644 index 7a55b659c..000000000 --- a/packages/data-edge/scripts/deploy.ts +++ /dev/null @@ -1,39 +0,0 @@ -import '@nomiclabs/hardhat-ethers' -import { ethers, network } from 'hardhat' - -import addresses from '../addresses.json' - -import { promises as fs } from 'fs' - -async function main() { - const factory = await ethers.getContractFactory('DataEdge') - - // If we had constructor arguments, they would be passed into deploy() - console.log(`Deploying contract...`) - const contract = await factory.deploy() - const tx = contract.deployTransaction - - // The address the Contract WILL have once mined - console.log(`> deployer: ${await contract.signer.getAddress()}`) - console.log(`> contract: ${contract.address}`) - console.log(`> tx: ${tx.hash} nonce:${tx.nonce} limit: ${tx.gasLimit.toString()} gas: ${tx.gasPrice.toNumber() / 1e9} (gwei)`) - - // The contract is NOT deployed yet; we must wait until it is mined - await contract.deployed() - console.log(`Done!`) - - // Update addresses.json - const chainId = (network.config.chainId).toString() - if (!addresses[chainId]) { - addresses[chainId] = {} - } - addresses[chainId]['DataEdge'] = contract.address - return fs.writeFile('addresses.json', JSON.stringify(addresses, null, 2)) -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) diff --git a/packages/data-edge/tasks/deploy.ts b/packages/data-edge/tasks/deploy.ts new file mode 100644 index 000000000..028d6fa66 --- /dev/null +++ b/packages/data-edge/tasks/deploy.ts @@ -0,0 +1,53 @@ +import '@nomiclabs/hardhat-ethers' +import { task } from 'hardhat/config' + +import addresses from '../addresses.json' + +import { promises as fs } from 'fs' + +enum Contract { + DataEdge, + EventfulDataEdge +} + +enum DeployName { + EBODataEdge = 'EBO', + SAODataEdge = 'SAO', +} + +task('data-edge:deploy', 'Deploy a DataEdge contract') + .addParam('contract', 'Chose DataEdge or EventfulDataEdge') + .addParam('deployName', 'Chose EBO or SAO') + .setAction(async (taskArgs, hre) => { + if (!Object.values(Contract).includes(taskArgs.contract)) { + throw new Error(`Contract ${taskArgs.contract} not supported`) + } + + if (!Object.values(DeployName).includes(taskArgs.deployName)) { + throw new Error(`Deploy name ${taskArgs.deployName} not supported`) + } + + const factory = await hre.ethers.getContractFactory(taskArgs.contract) + + console.log(`Deploying contract...`) + const contract = await factory.deploy() + const tx = contract.deployTransaction + + // The address the Contract WILL have once mined + console.log(`> deployer: ${await contract.signer.getAddress()}`) + console.log(`> contract: ${contract.address}`) + console.log(`> tx: ${tx.hash} nonce:${tx.nonce} limit: ${tx.gasLimit.toString()} gas: ${tx.gasPrice.toNumber() / 1e9} (gwei)`) + + // The contract is NOT deployed yet; we must wait until it is mined + await contract.deployed() + console.log(`Done!`) + + // Update addresses.json + const chainId = (hre.network.config.chainId).toString() + if (!addresses[chainId]) { + addresses[chainId] = {} + } + let deployName = `${taskArgs.deployName}${taskArgs.contract}` + addresses[chainId][deployName] = contract.address + return fs.writeFile('addresses.json', JSON.stringify(addresses, null, 2)) + }) diff --git a/packages/sdk/src/deployments/index.ts b/packages/sdk/src/deployments/index.ts index 23b4b0e16..43bca79cf 100644 --- a/packages/sdk/src/deployments/index.ts +++ b/packages/sdk/src/deployments/index.ts @@ -33,6 +33,7 @@ export { getDefaults, } from './network/deployment/config' +export * from './network/actions/disputes' export * from './network/actions/gns' export * from './network/actions/staking' export * from './network/actions/graph-token' diff --git a/packages/sdk/src/deployments/lib/contracts/log.ts b/packages/sdk/src/deployments/lib/contracts/log.ts index ebc19119b..b087b9b4b 100644 --- a/packages/sdk/src/deployments/lib/contracts/log.ts +++ b/packages/sdk/src/deployments/lib/contracts/log.ts @@ -39,6 +39,7 @@ export function logContractDeployReceipt( runtimeCodeHash: string, ) { const msg: string[] = [] + msg.push(` = Contract deployed at: ${receipt.contractAddress}`) msg.push(` = CreationCodeHash: ${creationCodeHash}`) msg.push(` = RuntimeCodeHash: ${runtimeCodeHash}`) logToConsoleAndFile(msg) 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/actions/disputes.ts b/packages/sdk/src/deployments/network/actions/disputes.ts new file mode 100644 index 000000000..785bfb626 --- /dev/null +++ b/packages/sdk/src/deployments/network/actions/disputes.ts @@ -0,0 +1,20 @@ +import { + createAttestation, + encodeAttestation as encodeAttestationLib, + Attestation, + Receipt, +} from '@graphprotocol/common-ts' +import { GraphChainId } from '../../../chain' + +export async function buildAttestation( + receipt: Receipt, + signer: string, + disputeManagerAddress: string, + chainId: GraphChainId, +) { + return await createAttestation(signer, chainId, disputeManagerAddress, receipt, '0') +} + +export function encodeAttestation(attestation: Attestation): string { + return encodeAttestationLib(attestation) +} diff --git a/packages/sdk/src/deployments/network/actions/staking.ts b/packages/sdk/src/deployments/network/actions/staking.ts index 3ad513cf8..1d67e4408 100644 --- a/packages/sdk/src/deployments/network/actions/staking.ts +++ b/packages/sdk/src/deployments/network/actions/staking.ts @@ -6,6 +6,7 @@ import { randomHexBytes } from '../../../utils/bytes' import type { GraphNetworkAction } from './types' import type { GraphNetworkContracts } from '../deployment/contracts/load' +import { ChannelKey } from '../../../utils' export const stake: GraphNetworkAction<{ amount: BigNumber }> = async ( contracts: GraphNetworkContracts, @@ -28,23 +29,18 @@ export const stake: GraphNetworkAction<{ amount: BigNumber }> = async ( } export const allocateFrom: GraphNetworkAction<{ - allocationSigner: SignerWithAddress + channelKey: ChannelKey subgraphDeploymentID: string amount: BigNumber }> = async ( contracts: GraphNetworkContracts, indexer: SignerWithAddress, - args: { allocationSigner: SignerWithAddress; subgraphDeploymentID: string; amount: BigNumber }, + args: { channelKey: ChannelKey; subgraphDeploymentID: string; amount: BigNumber }, ): Promise => { - const { allocationSigner, subgraphDeploymentID, amount } = args + const { channelKey, subgraphDeploymentID, amount } = args - const allocationId = allocationSigner.address - const messageHash = ethers.utils.solidityKeccak256( - ['address', 'address'], - [indexer.address, allocationId], - ) - const messageHashBytes = ethers.utils.arrayify(messageHash) - const proof = await allocationSigner.signMessage(messageHashBytes) + const allocationId = channelKey.address + const proof = await channelKey.generateProof(indexer.address) const metadata = ethers.constants.HashZero console.log(`\nAllocating ${amount} tokens on ${allocationId}...`) 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 diff --git a/packages/token-distribution/README.md b/packages/token-distribution/README.md index e4017294f..d52cb4ad4 100644 --- a/packages/token-distribution/README.md +++ b/packages/token-distribution/README.md @@ -9,7 +9,7 @@ An important premise is that participants with locked tokens can perform a numbe The contract lock manages a number of tokens deposited into the contract to ensure that they can only be released under certain time conditions. -This contract implements a release scheduled based on periods where tokens are released in steps after each period ends. It can be configured with one period in which case it works like a plain TimeLock. +This contract implements a release schedule based on periods where tokens are released in steps after each period ends. It can be configured with one period in which case it works like a plain TimeLock. It also supports revocation by the contract owner to be used for vesting schedules. The contract supports receiving extra funds over the managed tokens that can be withdrawn by the beneficiary at any time. @@ -54,7 +54,7 @@ The following functions signatures will be authorized for use: Contract that works as a factory of **GraphTokenLockWallet** contracts. It manages the function calls authorized to be called on any GraphTokenWallet and also holds addresses of our protocol contracts configured as targets. -The Manager supports creating TokenLock contracts based on a mastercopy bytecode using a Minimal Proxy to save gas. It also do so with CREATE2 to have reproducible addresses, this way any future to be deployed contract address can be passed to beneficiaries before actual deployment. +The Manager supports creating TokenLock contracts based on a mastercopy bytecode using a Minimal Proxy to save gas. It also does so with CREATE2 to have reproducible addresses, this way any future to be deployed contract address can be passed to beneficiaries before actual deployment. For convenience, the Manager will also fund the created contract with the amount of each contract's managed tokens. diff --git a/yarn.lock b/yarn.lock index 2aad2064b..10dda3146 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3377,7 +3377,7 @@ __metadata: "@nomiclabs/hardhat-waffle": "npm:^2.0.1" "@openzeppelin/contracts": "npm:^4.5.0" "@openzeppelin/hardhat-upgrades": "npm:^1.8.2" - "@tenderly/hardhat-tenderly": "npm:^1.0.12" + "@tenderly/hardhat-tenderly": "npm:^1.0.13" "@typechain/ethers-v5": "npm:^9.0.0" "@typechain/hardhat": "npm:^4.0.0" "@types/mocha": "npm:^9.0.0" @@ -3389,7 +3389,7 @@ __metadata: ethereum-waffle: "npm:^3.0.2" ethers: "npm:^5.1.3" ethlint: "npm:^1.2.5" - hardhat: "npm:^2.6.1" + hardhat: "npm:~2.14.0" hardhat-abi-exporter: "npm:^2.2.0" hardhat-contract-sizer: "npm:^2.0.3" hardhat-gas-reporter: "npm:^1.0.4" @@ -6990,7 +6990,7 @@ __metadata: languageName: node linkType: hard -"@tenderly/hardhat-tenderly@npm:^1.0.12": +"@tenderly/hardhat-tenderly@npm:^1.0.13": version: 1.8.0 resolution: "@tenderly/hardhat-tenderly@npm:1.8.0" dependencies: