diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 780ab1aa9..310c412bd 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -4,6 +4,8 @@ runs: using: composite steps: + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 - name: Enable corepack for modern yarn shell: bash run: corepack enable diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f35ed6d37..2a7a8b2ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up environment uses: ./.github/actions/setup - name: Build diff --git a/.github/workflows/ci-contracts.yml b/.github/workflows/ci-contracts.yml index 421a64b24..d60ab0d9b 100644 --- a/.github/workflows/ci-contracts.yml +++ b/.github/workflows/ci-contracts.yml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up environment uses: ./.github/actions/setup - name: Build diff --git a/.github/workflows/ci-horizon.yml b/.github/workflows/ci-horizon.yml new file mode 100644 index 000000000..056e692bb --- /dev/null +++ b/.github/workflows/ci-horizon.yml @@ -0,0 +1,50 @@ +name: CI - packages/horizon + +env: + CI: true + +on: + push: + branches: "*" + paths: + - packages/horizon/** + pull_request: + branches: "*" + paths: + - packages/horizon/** + workflow_dispatch: + +jobs: + test-ci: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Set up environment + uses: ./.github/actions/setup + - name: Build contracts + run: | + pushd packages/contracts + yarn build + popd + - name: Build horizon + run: | + pushd packages/horizon + yarn build + popd + - name: Build subgraph service + run: | + pushd packages/subgraph-service + yarn build + popd + - name: Build hardhat-graph-protocol + run: | + pushd packages/hardhat-graph-protocol + yarn build + popd + - name: Run tests + run: | + pushd packages/horizon + yarn test \ No newline at end of file diff --git a/.github/workflows/ci-subgraph-service.yml b/.github/workflows/ci-subgraph-service.yml new file mode 100644 index 000000000..0f9e6cff7 --- /dev/null +++ b/.github/workflows/ci-subgraph-service.yml @@ -0,0 +1,50 @@ +name: CI - packages/subgraph-service + +env: + CI: true + +on: + push: + branches: "*" + paths: + - packages/subgraph-service/** + pull_request: + branches: "*" + paths: + - packages/subgraph-service/** + workflow_dispatch: + +jobs: + test-ci: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Set up environment + uses: ./.github/actions/setup + - name: Build contracts + run: | + pushd packages/contracts + yarn build + popd + - name: Build horizon + run: | + pushd packages/horizon + yarn build + popd + - name: Build subgraph service + run: | + pushd packages/subgraph-service + yarn build + popd + - name: Build hardhat-graph-protocol + run: | + pushd packages/hardhat-graph-protocol + yarn build + popd + - name: Run tests + run: | + pushd packages/subgraph-service + yarn test \ No newline at end of file diff --git a/.github/workflows/ci-token-dist.yml b/.github/workflows/ci-token-dist.yml index 864654227..b991c1608 100644 --- a/.github/workflows/ci-token-dist.yml +++ b/.github/workflows/ci-token-dist.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up environment uses: ./.github/actions/setup - name: Build @@ -28,4 +30,6 @@ jobs: pushd packages/token-distribution yarn build - name: Run tests - run: yarn test \ No newline at end of file + run: | + pushd packages/token-distribution + yarn test \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dc01211d7..f942b6579 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up environment uses: ./.github/actions/setup - name: Publish 🚀 diff --git a/.github/workflows/verifydeployed.yml b/.github/workflows/verifydeployed.yml index 1f5d848d8..db5b90133 100644 --- a/.github/workflows/verifydeployed.yml +++ b/.github/workflows/verifydeployed.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + submodules: recursive - name: Set up environment uses: ./.github/actions/setup diff --git a/.gitignore b/.gitignore index ecd5f0d2c..de3ae9b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ cached/ # Build artifacts dist/ build/ +typechain/ +typechain-types/ deployments/hardhat/ # Ignore solc bin output @@ -32,8 +34,10 @@ bin/ .vscode # Coverage and other reports +coverage/ reports/ coverage.json +lcov.info # Local test files addresses-local.json @@ -45,8 +49,20 @@ addresses-fork.json # Keys .keystore +# Forge artifacts +cache_forge + # Graph client .graphclient tx-builder-*.json -!tx-builder-template.json \ No newline at end of file +!tx-builder-template.json + +# Hardhat Ignition +**/chain-31337/ +**/chain-1377/ +**/horizon-localhost/ +**/horizon-hardhat/ +**/subgraph-service-localhost/ +**/subgraph-service-hardhat/ +!**/ignition/**/artifacts/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..8cd5a0672 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "packages/subgraph-service/lib/forge-std"] + path = packages/subgraph-service/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "packages/horizon/lib/forge-std"] + path = packages/horizon/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "packages/horizon/lib/openzeppelin-foundry-upgrades"] + path = packages/horizon/lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "packages/subgraph-service/lib/openzeppelin-foundry-upgrades"] + path = packages/subgraph-service/lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades diff --git a/.husky/pre-commit b/.husky/pre-commit index 487427eda..aa9adeab4 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -6,7 +6,17 @@ pushd packages/contracts npx --no-install lint-staged popd +# subgraph service +pushd packages/subgraph-service +npx --no-install lint-staged +popd + # data-edge pushd packages/data-edge npx --no-install lint-staged -popd \ No newline at end of file +popd + +# graph horizon +pushd packages/horizon +npx --no-install lint-staged +popd diff --git a/README.md b/README.md index dc813abb8..85de38b1e 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,12 @@ This repository is a Yarn workspaces monorepo containing the following packages: | --- | --- | --- | | [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) | - | Shared linting and formatting rules for TypeScript projects. | -| [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 | +| [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) | - | 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/package.json b/package.json index 4f1c06aa7..d1e0f60d8 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,19 @@ "packages/contracts", "packages/data-edge", "packages/eslint-graph-config", + "packages/hardhat-graph-protocol", + "packages/horizon", "packages/sdk", "packages/solhint-graph-config", + "packages/solhint-plugin-graph", + "packages/subgraph-service", "packages/token-distribution" ], "scripts": { "postinstall": "husky install", "clean": "yarn workspaces foreach --all --parallel --verbose run clean", "clean:all": "yarn clean && rm -rf node_modules packages/*/node_modules", - "build": "yarn workspaces foreach --all --verbose run build", + "build": "chmod +x ./scripts/build && ./scripts/build", "lint": "yarn workspaces foreach --all --parallel --verbose run lint", "test": "yarn workspaces foreach --all --parallel --verbose --interlaced run test" }, diff --git a/packages/contracts/.solhint.json b/packages/contracts/.solhint.json deleted file mode 100644 index e52cf9346..000000000 --- a/packages/contracts/.solhint.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "solhint:recommended", - "plugins": ["prettier"], - "rules": { - "prettier/prettier": "error", - "func-visibility": ["warn", { "ignoreConstructors": true }], - "compiler-version": ["off"], - "constructor-syntax": "warn", - "quotes": ["error", "double"], - "reason-string": ["off"], - "not-rely-on-time": "off", - "no-empty-blocks": "off" - } -} diff --git a/packages/contracts/.solhintignore b/packages/contracts/.solhintignore deleted file mode 100644 index b36dffeb7..000000000 --- a/packages/contracts/.solhintignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules - -./contracts/bancor -./contracts/discovery/erc1056 -./contracts/rewards/RewardsManager.sol -./contracts/staking/libs/LibFixedMath.sol -./contracts/tests/ens -./contracts/tests/testnet/GSRManager.sol diff --git a/packages/contracts/contracts/arbitrum/Arbitrum.md b/packages/contracts/contracts/arbitrum/Arbitrum.md new file mode 100644 index 000000000..abc87553e --- /dev/null +++ b/packages/contracts/contracts/arbitrum/Arbitrum.md @@ -0,0 +1,5 @@ +# Arbitrum contracts + +These contracts have been copied from the [Arbitrum repo](https://github.com/OffchainLabs/arbitrum). + +They are also available as part of the npm packages [arb-bridge-eth](https://www.npmjs.com/package/arb-bridge-eth) and [arb-bridge-peripherals](https://www.npmjs.com/package/arb-bridge-peripherals). The reason for copying them rather than installing those packages is the contracts only support Solidity `^0.6.11`, so we had to change the version to `^0.7.6` for it to be compatible with our other contracts. diff --git a/packages/contracts/contracts/arbitrum/ITokenGateway.sol b/packages/contracts/contracts/arbitrum/ITokenGateway.sol index 977fe07f2..3b12e578e 100644 --- a/packages/contracts/contracts/arbitrum/ITokenGateway.sol +++ b/packages/contracts/contracts/arbitrum/ITokenGateway.sol @@ -23,7 +23,7 @@ * */ -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; interface ITokenGateway { /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated @@ -47,20 +47,20 @@ interface ITokenGateway { // ); function outboundTransfer( - address _token, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data + address token, + address to, + uint256 amunt, + uint256 maxas, + uint256 gasPiceBid, + bytes calldata data ) external payable returns (bytes memory); function finalizeInboundTransfer( - address _token, - address _from, - address _to, - uint256 _amount, - bytes calldata _data + address token, + address from, + address to, + uint256 amount, + bytes calldata data ) external payable; /** diff --git a/packages/contracts/contracts/curation/Curation.sol b/packages/contracts/contracts/curation/Curation.sol index bd3032046..e289d048c 100644 --- a/packages/contracts/contracts/curation/Curation.sol +++ b/packages/contracts/contracts/curation/Curation.sol @@ -257,7 +257,7 @@ contract Curation is CurationV2Storage, GraphUpgradeable { /** * @notice Get the amount of token reserves in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation poool + * @param _subgraphDeploymentID Subgraph deployment curation pool * @return Amount of token reserves in the curation pool */ function getCurationPoolTokens(bytes32 _subgraphDeploymentID) external view override returns (uint256) { @@ -286,7 +286,7 @@ contract Curation is CurationV2Storage, GraphUpgradeable { /** * @notice Get the amount of signal in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation poool + * @param _subgraphDeploymentID Subgraph deployment curation pool * @return Amount of signal minted for the subgraph deployment */ function getCurationPoolSignal(bytes32 _subgraphDeploymentID) public view override returns (uint256) { diff --git a/packages/contracts/contracts/curation/CurationStorage.sol b/packages/contracts/contracts/curation/CurationStorage.sol index bcbf0df6b..12f5b255b 100644 --- a/packages/contracts/contracts/curation/CurationStorage.sol +++ b/packages/contracts/contracts/curation/CurationStorage.sol @@ -68,3 +68,16 @@ abstract contract CurationV1Storage is Managed, ICuration { abstract contract CurationV2Storage is CurationV1Storage, Initializable { // Nothing here, just adding Initializable } + +/** + * @title Curation Storage version 3 + * @dev This contract holds the third version of the storage variables + * for the Curation and L2Curation contracts. + * It adds a new variable subgraphService to the storage. + * When adding new variables, create a new version that inherits this and update + * the contracts to use the new version instead. + */ +abstract contract CurationV3Storage is CurationV2Storage { + // Address of the subgraph service + address public subgraphService; +} diff --git a/packages/contracts/contracts/curation/ICuration.sol b/packages/contracts/contracts/curation/ICuration.sol index d92aa9c69..4f2c2bac5 100644 --- a/packages/contracts/contracts/curation/ICuration.sol +++ b/packages/contracts/contracts/curation/ICuration.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; /** * @title Curation Interface @@ -84,14 +84,14 @@ interface ICuration { /** * @notice Get the amount of signal in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation poool + * @param _subgraphDeploymentID Subgraph deployment curation pool * @return Amount of signal minted for the subgraph deployment */ function getCurationPoolSignal(bytes32 _subgraphDeploymentID) external view returns (uint256); /** * @notice Get the amount of token reserves in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation poool + * @param _subgraphDeploymentID Subgraph deployment curation pool * @return Amount of token reserves in the curation pool */ function getCurationPoolTokens(bytes32 _subgraphDeploymentID) external view returns (uint256); diff --git a/packages/contracts/contracts/disputes/DisputeManager.sol b/packages/contracts/contracts/disputes/DisputeManager.sol index 6700ec341..013a21b03 100644 --- a/packages/contracts/contracts/disputes/DisputeManager.sol +++ b/packages/contracts/contracts/disputes/DisputeManager.sol @@ -579,7 +579,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa * @notice Ignore a dispute with ID `_disputeID` * @param _disputeID ID of the dispute to be disregarded */ - function drawDispute(bytes32 _disputeID) public override onlyArbitrator onlyPendingDispute(_disputeID) { + function drawDispute(bytes32 _disputeID) external override onlyArbitrator onlyPendingDispute(_disputeID) { Dispute storage dispute = disputes[_disputeID]; // Return deposit to the fisherman diff --git a/packages/contracts/contracts/epochs/IEpochManager.sol b/packages/contracts/contracts/epochs/IEpochManager.sol index 36b1f47a3..c65280d59 100644 --- a/packages/contracts/contracts/epochs/IEpochManager.sol +++ b/packages/contracts/contracts/epochs/IEpochManager.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; interface IEpochManager { // -- Configuration -- diff --git a/packages/contracts/contracts/gateway/ICallhookReceiver.sol b/packages/contracts/contracts/gateway/ICallhookReceiver.sol index 885b0cdb2..8d003cb76 100644 --- a/packages/contracts/contracts/gateway/ICallhookReceiver.sol +++ b/packages/contracts/contracts/gateway/ICallhookReceiver.sol @@ -6,7 +6,7 @@ * be allowlisted by the governor, but also implement this interface that contains * the function that will actually be called by the L2GraphTokenGateway. */ -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; interface ICallhookReceiver { /** diff --git a/packages/contracts/contracts/governance/Controller.sol b/packages/contracts/contracts/governance/Controller.sol index affb29a05..707a27fff 100644 --- a/packages/contracts/contracts/governance/Controller.sol +++ b/packages/contracts/contracts/governance/Controller.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; import { IController } from "./IController.sol"; import { IManaged } from "./IManaged.sol"; @@ -89,10 +89,10 @@ contract Controller is Governed, Pausable, IController { /** * @notice Change the partial paused state of the contract * Partial pause is intended as a partial pause of the protocol - * @param _toPause True if the contracts should be (partially) paused, false otherwise + * @param _toPartialPause True if the contracts should be (partially) paused, false otherwise */ - function setPartialPaused(bool _toPause) external override onlyGovernorOrGuardian { - _setPartialPaused(_toPause); + function setPartialPaused(bool _toPartialPause) external override onlyGovernorOrGuardian { + _setPartialPaused(_toPartialPause); } /** diff --git a/packages/contracts/contracts/governance/Governed.sol b/packages/contracts/contracts/governance/Governed.sol index f692b2d19..76a3247dd 100644 --- a/packages/contracts/contracts/governance/Governed.sol +++ b/packages/contracts/contracts/governance/Governed.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; /** * @title Graph Governance contract diff --git a/packages/contracts/contracts/governance/IController.sol b/packages/contracts/contracts/governance/IController.sol index 7df3d94ee..6ab72010e 100644 --- a/packages/contracts/contracts/governance/IController.sol +++ b/packages/contracts/contracts/governance/IController.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.6.12 <0.8.0; +pragma solidity ^0.7.6 || 0.8.27; interface IController { function getGovernor() external view returns (address); diff --git a/packages/contracts/contracts/governance/IManaged.sol b/packages/contracts/contracts/governance/IManaged.sol index 76f05e0fb..ff6625d81 100644 --- a/packages/contracts/contracts/governance/IManaged.sol +++ b/packages/contracts/contracts/governance/IManaged.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; import { IController } from "./IController.sol"; diff --git a/packages/contracts/contracts/governance/Pausable.sol b/packages/contracts/contracts/governance/Pausable.sol index 552b0aa15..2bc1795cd 100644 --- a/packages/contracts/contracts/governance/Pausable.sol +++ b/packages/contracts/contracts/governance/Pausable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; abstract contract Pausable { /** @@ -14,7 +14,7 @@ abstract contract Pausable { bool internal _paused; /// Timestamp for the last time the partial pause was set - uint256 public lastPausePartialTime; + uint256 public lastPartialPauseTime; /// Timestamp for the last time the full pause was set uint256 public lastPauseTime; @@ -31,15 +31,15 @@ abstract contract Pausable { /** * @dev Change the partial paused state of the contract - * @param _toPause New value for the partial pause state (true means the contracts will be partially paused) + * @param _toPartialPause New value for the partial pause state (true means the contracts will be partially paused) */ - function _setPartialPaused(bool _toPause) internal { - if (_toPause == _partialPaused) { + function _setPartialPaused(bool _toPartialPause) internal { + if (_toPartialPause == _partialPaused) { return; } - _partialPaused = _toPause; + _partialPaused = _toPartialPause; if (_partialPaused) { - lastPausePartialTime = block.timestamp; + lastPartialPauseTime = block.timestamp; } emit PartialPauseChanged(_partialPaused); } @@ -66,6 +66,6 @@ abstract contract Pausable { function _setPauseGuardian(address newPauseGuardian) internal { address oldPauseGuardian = pauseGuardian; pauseGuardian = newPauseGuardian; - emit NewPauseGuardian(oldPauseGuardian, pauseGuardian); + emit NewPauseGuardian(oldPauseGuardian, newPauseGuardian); } } diff --git a/packages/contracts/contracts/l2/curation/IL2Curation.sol b/packages/contracts/contracts/l2/curation/IL2Curation.sol index bbbfd82ff..b59960792 100644 --- a/packages/contracts/contracts/l2/curation/IL2Curation.sol +++ b/packages/contracts/contracts/l2/curation/IL2Curation.sol @@ -6,6 +6,12 @@ pragma solidity ^0.7.6; * @title Interface of the L2 Curation contract. */ interface IL2Curation { + /** + * @notice Set the subgraph service address. + * @param _subgraphService Address of the SubgraphService contract + */ + function setSubgraphService(address _subgraphService) external; + /** * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. * @dev This function charges no tax and can only be called by GNS in specific scenarios (for now diff --git a/packages/contracts/contracts/l2/curation/L2Curation.sol b/packages/contracts/contracts/l2/curation/L2Curation.sol index 4d51bf3f1..f6d64209b 100644 --- a/packages/contracts/contracts/l2/curation/L2Curation.sol +++ b/packages/contracts/contracts/l2/curation/L2Curation.sol @@ -12,7 +12,7 @@ import { TokenUtils } from "../../utils/TokenUtils.sol"; import { IRewardsManager } from "../../rewards/IRewardsManager.sol"; import { Managed } from "../../governance/Managed.sol"; import { IGraphToken } from "../../token/IGraphToken.sol"; -import { CurationV2Storage } from "../../curation/CurationStorage.sol"; +import { CurationV3Storage } from "../../curation/CurationStorage.sol"; import { IGraphCurationToken } from "../../curation/IGraphCurationToken.sol"; import { IL2Curation } from "./IL2Curation.sol"; @@ -28,7 +28,7 @@ import { IL2Curation } from "./IL2Curation.sol"; * Holders can burn GCS using this contract to get GRT tokens back according to the * bonding curve. */ -contract L2Curation is CurationV2Storage, GraphUpgradeable, IL2Curation { +contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { using SafeMathUpgradeable for uint256; /// @dev 100% in parts per million @@ -67,6 +67,11 @@ contract L2Curation is CurationV2Storage, GraphUpgradeable, IL2Curation { */ event Collected(bytes32 indexed subgraphDeploymentID, uint256 tokens); + /** + * @dev Emitted when the subgraph service is set. + */ + event SubgraphServiceSet(address indexed newSubgraphService); + /** * @dev Modifier for functions that can only be called by the GNS contract */ @@ -131,16 +136,25 @@ contract L2Curation is CurationV2Storage, GraphUpgradeable, IL2Curation { _setCurationTokenMaster(_curationTokenMaster); } + /** + * @notice Set the subgraph service address + * @param _subgraphService Address of the subgraph service contract + */ + function setSubgraphService(address _subgraphService) external override onlyGovernor { + subgraphService = _subgraphService; + emit SubgraphServiceSet(_subgraphService); + } + /** * @notice Assign Graph Tokens collected as curation fees to the curation pool reserve. - * @dev This function can only be called by the Staking contract and will do the bookeeping of + * @dev This function can only be called by the Staking contract and will do the Bookkeeping of * transferred tokens into this contract. * @param _subgraphDeploymentID SubgraphDeployment where funds should be allocated as reserves * @param _tokens Amount of Graph Tokens to add to reserves */ function collect(bytes32 _subgraphDeploymentID, uint256 _tokens) external override { - // Only Staking contract is authorized as caller - require(msg.sender == address(staking()), "Caller must be the staking contract"); + // Only SubgraphService or Staking contract are authorized as caller + require(msg.sender == subgraphService || msg.sender == address(staking()), "Caller must be the subgraph service or staking contract"); // Must be curated to accept tokens require(isCurated(_subgraphDeploymentID), "Subgraph deployment must be curated to collect fees"); @@ -312,7 +326,7 @@ contract L2Curation is CurationV2Storage, GraphUpgradeable, IL2Curation { /** * @notice Get the amount of token reserves in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation poool + * @param _subgraphDeploymentID Subgraph deployment curation pool * @return Amount of token reserves in the curation pool */ function getCurationPoolTokens(bytes32 _subgraphDeploymentID) external view override returns (uint256) { @@ -341,7 +355,7 @@ contract L2Curation is CurationV2Storage, GraphUpgradeable, IL2Curation { /** * @notice Get the amount of signal in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation poool + * @param _subgraphDeploymentID Subgraph deployment curation pool * @return Amount of signal minted for the subgraph deployment */ function getCurationPoolSignal(bytes32 _subgraphDeploymentID) public view override returns (uint256) { diff --git a/packages/contracts/contracts/l2/staking/IL2Staking.sol b/packages/contracts/contracts/l2/staking/IL2Staking.sol index 73ff936ce..4b7748e31 100644 --- a/packages/contracts/contracts/l2/staking/IL2Staking.sol +++ b/packages/contracts/contracts/l2/staking/IL2Staking.sol @@ -5,6 +5,7 @@ pragma abicoder v2; import { IStaking } from "../../staking/IStaking.sol"; import { IL2StakingBase } from "./IL2StakingBase.sol"; +import { IL2StakingTypes } from "./IL2StakingTypes.sol"; /** * @title Interface for the L2 Staking contract @@ -15,21 +16,4 @@ import { IL2StakingBase } from "./IL2StakingBase.sol"; * the custom setup of the Staking contract where part of the functionality is implemented * in a separate contract (StakingExtension) to which calls are delegated through the fallback function. */ -interface IL2Staking is IStaking, IL2StakingBase { - /// @dev Message codes for the L1 -> L2 bridge callhook - enum L1MessageCodes { - RECEIVE_INDEXER_STAKE_CODE, - RECEIVE_DELEGATION_CODE - } - - /// @dev Encoded message struct when receiving indexer stake through the bridge - struct ReceiveIndexerStakeData { - address indexer; - } - - /// @dev Encoded message struct when receiving delegation through the bridge - struct ReceiveDelegationData { - address indexer; - address delegator; - } -} +interface IL2Staking is IStaking, IL2StakingBase, IL2StakingTypes {} diff --git a/packages/contracts/contracts/l2/staking/IL2StakingBase.sol b/packages/contracts/contracts/l2/staking/IL2StakingBase.sol index 6f701ec89..f5c33c2d0 100644 --- a/packages/contracts/contracts/l2/staking/IL2StakingBase.sol +++ b/packages/contracts/contracts/l2/staking/IL2StakingBase.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; diff --git a/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol b/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol new file mode 100644 index 000000000..500694e89 --- /dev/null +++ b/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6 || 0.8.27; + +interface IL2StakingTypes { + /// @dev Message codes for the L1 -> L2 bridge callhook + enum L1MessageCodes { + RECEIVE_INDEXER_STAKE_CODE, + RECEIVE_DELEGATION_CODE + } + + /// @dev Encoded message struct when receiving indexer stake through the bridge + struct ReceiveIndexerStakeData { + address indexer; + } + + /// @dev Encoded message struct when receiving delegation through the bridge + struct ReceiveDelegationData { + address indexer; + address delegator; + } +} diff --git a/packages/contracts/contracts/l2/staking/L2Staking.sol b/packages/contracts/contracts/l2/staking/L2Staking.sol index 3f9d28e5a..4bde512ec 100644 --- a/packages/contracts/contracts/l2/staking/L2Staking.sol +++ b/packages/contracts/contracts/l2/staking/L2Staking.sol @@ -8,6 +8,7 @@ import { Staking } from "../../staking/Staking.sol"; import { IL2StakingBase } from "./IL2StakingBase.sol"; import { IL2Staking } from "./IL2Staking.sol"; import { Stakes } from "../../staking/libs/Stakes.sol"; +import { IL2StakingTypes } from "./IL2StakingTypes.sol"; /** * @title L2Staking contract @@ -63,16 +64,16 @@ contract L2Staking is Staking, IL2StakingBase { require(_from == counterpartStakingAddress, "ONLY_L1_STAKING_THROUGH_BRIDGE"); (uint8 code, bytes memory functionData) = abi.decode(_data, (uint8, bytes)); - if (code == uint8(IL2Staking.L1MessageCodes.RECEIVE_INDEXER_STAKE_CODE)) { - IL2Staking.ReceiveIndexerStakeData memory indexerData = abi.decode( + if (code == uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_INDEXER_STAKE_CODE)) { + IL2StakingTypes.ReceiveIndexerStakeData memory indexerData = abi.decode( functionData, - (IL2Staking.ReceiveIndexerStakeData) + (IL2StakingTypes.ReceiveIndexerStakeData) ); _receiveIndexerStake(_amount, indexerData); - } else if (code == uint8(IL2Staking.L1MessageCodes.RECEIVE_DELEGATION_CODE)) { - IL2Staking.ReceiveDelegationData memory delegationData = abi.decode( + } else if (code == uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_DELEGATION_CODE)) { + IL2StakingTypes.ReceiveDelegationData memory delegationData = abi.decode( functionData, - (IL2Staking.ReceiveDelegationData) + (IL2StakingTypes.ReceiveDelegationData) ); _receiveDelegation(_amount, delegationData); } else { @@ -87,7 +88,10 @@ contract L2Staking is Staking, IL2StakingBase { * @param _amount Amount of tokens that were transferred * @param _indexerData struct containing the indexer's address */ - function _receiveIndexerStake(uint256 _amount, IL2Staking.ReceiveIndexerStakeData memory _indexerData) internal { + function _receiveIndexerStake( + uint256 _amount, + IL2StakingTypes.ReceiveIndexerStakeData memory _indexerData + ) internal { address _indexer = _indexerData.indexer; // Deposit tokens into the indexer stake __stakes[_indexer].deposit(_amount); @@ -108,7 +112,10 @@ contract L2Staking is Staking, IL2StakingBase { * @param _amount Amount of tokens that were transferred * @param _delegationData struct containing the delegator's address and the indexer's address */ - function _receiveDelegation(uint256 _amount, IL2Staking.ReceiveDelegationData memory _delegationData) internal { + function _receiveDelegation( + uint256 _amount, + IL2StakingTypes.ReceiveDelegationData memory _delegationData + ) internal { // Get the delegation pool of the indexer DelegationPool storage pool = __delegationPools[_delegationData.indexer]; Delegation storage delegation = pool.delegators[_delegationData.delegator]; diff --git a/packages/contracts/contracts/rewards/IRewardsIssuer.sol b/packages/contracts/contracts/rewards/IRewardsIssuer.sol new file mode 100644 index 000000000..9da18b92b --- /dev/null +++ b/packages/contracts/contracts/rewards/IRewardsIssuer.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6 || 0.8.27; + +interface IRewardsIssuer { + /** + * @dev Get allocation data to calculate rewards issuance + * @param allocationId The allocation Id + * @return indexer The indexer address + * @return subgraphDeploymentId Subgraph deployment id for the allocation + * @return tokens Amount of allocated tokens + * @return accRewardsPerAllocatedToken Rewards snapshot + * @return accRewardsPending Snapshot of accumulated rewards from previous allocation resizing, pending to be claimed + */ + function getAllocationData( + address allocationId + ) + external + view + returns ( + address indexer, + bytes32 subgraphDeploymentId, + uint256 tokens, + uint256 accRewardsPerAllocatedToken, + uint256 accRewardsPending + ); + + /** + * @notice Return the total amount of tokens allocated to subgraph. + * @param _subgraphDeploymentId Deployment Id for the subgraph + * @return Total tokens allocated to subgraph + */ + function getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentId) external view returns (uint256); +} diff --git a/packages/contracts/contracts/rewards/IRewardsManager.sol b/packages/contracts/contracts/rewards/IRewardsManager.sol index 511f1adf9..b31064d1b 100644 --- a/packages/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/contracts/contracts/rewards/IRewardsManager.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; interface IRewardsManager { /** @@ -19,6 +19,8 @@ interface IRewardsManager { function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; + function setSubgraphService(address _subgraphService) external; + // -- Denylist -- function setSubgraphAvailabilityOracle(address _subgraphAvailabilityOracle) external; @@ -37,7 +39,9 @@ interface IRewardsManager { function getAccRewardsPerAllocatedToken(bytes32 _subgraphDeploymentID) external view returns (uint256, uint256); - function getRewards(address _allocationID) external view returns (uint256); + function getRewards(address _rewardsIssuer, address _allocationID) external view returns (uint256); + + function calcRewards(uint256 _tokens, uint256 _accRewardsPerAllocatedToken) external pure returns (uint256); // -- Updates -- diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index f19aad1a6..1517775f9 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -3,13 +3,10 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/math/SafeMath.sol"; - import "../upgrades/GraphUpgradeable.sol"; import "../staking/libs/MathUtils.sol"; import "./RewardsManagerStorage.sol"; -import "./IRewardsManager.sol"; /** * @title Rewards Manager Contract @@ -28,7 +25,7 @@ import "./IRewardsManager.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; @@ -37,19 +34,25 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa /** * @dev Emitted when rewards are assigned to an indexer. + * @dev We use the Horizon prefix to change the event signature which makes network subgraph development much easier */ - event RewardsAssigned(address indexed indexer, address indexed allocationID, uint256 epoch, uint256 amount); + event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount); /** * @dev Emitted when rewards are denied to an indexer. */ - event RewardsDenied(address indexed indexer, address indexed allocationID, uint256 epoch); + event RewardsDenied(address indexed indexer, address indexed allocationID); /** * @dev Emitted when a subgraph is denied for claiming rewards. */ event RewardsDenylistUpdated(bytes32 indexed subgraphDeploymentID, uint256 sinceBlock); + /** + * @dev Emitted when the subgraph service is set + */ + event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService); + // -- Modifiers -- modifier onlySubgraphAvailabilityOracle() { @@ -115,6 +118,12 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa emit ParameterUpdated("minimumSubgraphSignal"); } + function setSubgraphService(address _subgraphService) external override onlyGovernor { + address oldSubgraphService = address(subgraphService); + subgraphService = IRewardsIssuer(_subgraphService); + emit SubgraphServiceSet(oldSubgraphService, _subgraphService); + } + // -- Denylist -- /** @@ -231,7 +240,19 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa subgraph.accRewardsForSubgraphSnapshot ); - uint256 subgraphAllocatedTokens = staking().getSubgraphAllocatedTokens(_subgraphDeploymentID); + // There are two contributors to subgraph allocated tokens: + // - the legacy allocations on the legacy staking contract + // - the new allocations on the subgraph service + uint256 subgraphAllocatedTokens = 0; + address[2] memory rewardsIssuers = [address(staking()), address(subgraphService)]; + for (uint256 i = 0; i < rewardsIssuers.length; i++) { + if (rewardsIssuers[i] != address(0)) { + subgraphAllocatedTokens += IRewardsIssuer(rewardsIssuers[i]).getSubgraphAllocatedTokens( + _subgraphDeploymentID + ); + } + } + if (subgraphAllocatedTokens == 0) { return (0, accRewardsForSubgraph); } @@ -294,19 +315,40 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa /** * @dev Calculate current rewards for a given allocation on demand. + * The allocation could be a legacy allocation or a new subgraph service allocation. * @param _allocationID Allocation * @return Rewards amount for an allocation */ - function getRewards(address _allocationID) external view override returns (uint256) { - IStaking.AllocationState allocState = staking().getAllocationState(_allocationID); - if (allocState != IStakingBase.AllocationState.Active) { - return 0; - } + function getRewards(address _rewardsIssuer, address _allocationID) external view override returns (uint256) { + require( + _rewardsIssuer == address(staking()) || _rewardsIssuer == address(subgraphService), + "Not a rewards issuer" + ); - IStaking.Allocation memory alloc = staking().getAllocation(_allocationID); + ( + , + bytes32 subgraphDeploymentId, + uint256 tokens, + uint256 alloAccRewardsPerAllocatedToken, + uint256 accRewardsPending + ) = IRewardsIssuer(_rewardsIssuer).getAllocationData(_allocationID); + + (uint256 accRewardsPerAllocatedToken, ) = getAccRewardsPerAllocatedToken(subgraphDeploymentId); + return + accRewardsPending.add(_calcRewards(tokens, alloAccRewardsPerAllocatedToken, accRewardsPerAllocatedToken)); + } - (uint256 accRewardsPerAllocatedToken, ) = getAccRewardsPerAllocatedToken(alloc.subgraphDeploymentID); - return _calcRewards(alloc.tokens, alloc.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken); + /** + * @dev Calculate rewards for a given accumulated rewards per allocated token. + * @param _tokens Tokens allocated + * @param _accRewardsPerAllocatedToken Allocation accumulated rewards per token + * @return Rewards amount + */ + function calcRewards( + uint256 _tokens, + uint256 _accRewardsPerAllocatedToken + ) external pure override returns (uint256) { + return _accRewardsPerAllocatedToken.mul(_tokens).div(FIXED_POINT_SCALING_FACTOR); } /** @@ -327,35 +369,47 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa /** * @dev Pull rewards from the contract for a particular allocation. - * This function can only be called by the Staking contract. + * This function can only be called by an authorized rewards issuer which are + * the staking contract (for legacy allocations), and the subgraph service (for new allocations). * This function will mint the necessary tokens to reward based on the inflation calculation. * @param _allocationID Allocation * @return Assigned rewards amount */ function takeRewards(address _allocationID) external override returns (uint256) { - // Only Staking contract is authorized as caller - IStaking staking = staking(); - require(msg.sender == address(staking), "Caller must be the staking contract"); + address rewardsIssuer = msg.sender; + require( + rewardsIssuer == address(staking()) || rewardsIssuer == address(subgraphService), + "Caller must be a rewards issuer" + ); - IStaking.Allocation memory alloc = staking.getAllocation(_allocationID); - uint256 accRewardsPerAllocatedToken = onSubgraphAllocationUpdate(alloc.subgraphDeploymentID); + ( + address indexer, + bytes32 subgraphDeploymentID, + uint256 tokens, + uint256 accRewardsPerAllocatedToken, + uint256 accRewardsPending + ) = IRewardsIssuer(rewardsIssuer).getAllocationData(_allocationID); + + uint256 updatedAccRewardsPerAllocatedToken = onSubgraphAllocationUpdate(subgraphDeploymentID); // Do not do rewards on denied subgraph deployments ID - if (isDenied(alloc.subgraphDeploymentID)) { - emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch); + if (isDenied(subgraphDeploymentID)) { + emit RewardsDenied(indexer, _allocationID); return 0; } // Calculate rewards accrued by this allocation - uint256 rewards = _calcRewards(alloc.tokens, alloc.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken); + uint256 rewards = accRewardsPending.add( + _calcRewards(tokens, accRewardsPerAllocatedToken, updatedAccRewardsPerAllocatedToken) + ); if (rewards > 0) { - // Mint directly to staking contract for the reward amount - // The staking contract will do bookkeeping of the reward and + // Mint directly to rewards issuer for the reward amount + // The rewards issuer contract will do bookkeeping of the reward and // assign in proportion to each stakeholder incentive - graphToken().mint(address(staking), rewards); + graphToken().mint(rewardsIssuer, rewards); } - emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + emit HorizonRewardsAssigned(indexer, _allocationID, rewards); return rewards; } diff --git a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol index 72dbda373..ded325593 100644 --- a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol +++ b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; import "./IRewardsManager.sol"; import "../governance/Managed.sol"; +import { IRewardsIssuer } from "./IRewardsIssuer.sol"; contract RewardsManagerV1Storage is Managed { // -- State -- @@ -36,3 +37,8 @@ contract RewardsManagerV4Storage is RewardsManagerV3Storage { // GRT issued for indexer rewards per block uint256 public issuancePerBlock; } + +contract RewardsManagerV5Storage is RewardsManagerV4Storage { + // Address of the subgraph service + IRewardsIssuer public subgraphService; +} diff --git a/packages/contracts/contracts/staking/IStakingBase.sol b/packages/contracts/contracts/staking/IStakingBase.sol index b30d00499..f01ca4326 100644 --- a/packages/contracts/contracts/staking/IStakingBase.sol +++ b/packages/contracts/contracts/staking/IStakingBase.sol @@ -361,6 +361,22 @@ interface IStakingBase is IStakingData { */ function getAllocation(address _allocationID) external view returns (Allocation memory); + /** + * @dev New function to get the allocation data for the rewards manager + * @dev Note that this is only to make tests pass, as the staking contract with + * this changes will never get deployed. HorizonStaking is taking it's place. + */ + function getAllocationData( + address _allocationID + ) external view returns (address, bytes32, uint256, uint256, uint256); + + /** + * @dev New function to get the allocation active status for the rewards manager + * @dev Note that this is only to make tests pass, as the staking contract with + * this changes will never get deployed. HorizonStaking is taking it's place. + */ + function isActiveAllocation(address _allocationID) external view returns (bool); + /** * @notice Return the current state of an allocation * @param _allocationID Allocation identifier diff --git a/packages/contracts/contracts/staking/L1Staking.sol b/packages/contracts/contracts/staking/L1Staking.sol index df4e145bd..5f25cb229 100644 --- a/packages/contracts/contracts/staking/L1Staking.sol +++ b/packages/contracts/contracts/staking/L1Staking.sol @@ -9,12 +9,12 @@ import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; import { Staking } from "./Staking.sol"; import { Stakes } from "./libs/Stakes.sol"; import { IStakingData } from "./IStakingData.sol"; -import { IL2Staking } from "../l2/staking/IL2Staking.sol"; import { L1StakingV1Storage } from "./L1StakingStorage.sol"; import { IGraphToken } from "../token/IGraphToken.sol"; import { IL1StakingBase } from "./IL1StakingBase.sol"; import { MathUtils } from "./libs/MathUtils.sol"; import { IL1GraphTokenLockTransferTool } from "./IL1GraphTokenLockTransferTool.sol"; +import { IL2StakingTypes } from "../l2/staking/IL2StakingTypes.sol"; /** * @title L1Staking contract @@ -267,11 +267,11 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { ); } - IL2Staking.ReceiveIndexerStakeData memory functionData; + IL2StakingTypes.ReceiveIndexerStakeData memory functionData; functionData.indexer = _l2Beneficiary; bytes memory extraData = abi.encode( - uint8(IL2Staking.L1MessageCodes.RECEIVE_INDEXER_STAKE_CODE), + uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_INDEXER_STAKE_CODE), abi.encode(functionData) ); @@ -324,10 +324,13 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { delegation.shares = 0; bytes memory extraData; { - IL2Staking.ReceiveDelegationData memory functionData; + IL2StakingTypes.ReceiveDelegationData memory functionData; functionData.indexer = indexerTransferredToL2[_indexer]; functionData.delegator = _l2Beneficiary; - extraData = abi.encode(uint8(IL2Staking.L1MessageCodes.RECEIVE_DELEGATION_CODE), abi.encode(functionData)); + extraData = abi.encode( + uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_DELEGATION_CODE), + abi.encode(functionData) + ); } _sendTokensAndMessageToL2Staking( diff --git a/packages/contracts/contracts/staking/Staking.sol b/packages/contracts/contracts/staking/Staking.sol index f00097de2..eaafdee0c 100644 --- a/packages/contracts/contracts/staking/Staking.sol +++ b/packages/contracts/contracts/staking/Staking.sol @@ -472,6 +472,27 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M return __allocations[_allocationID]; } + /** + * @dev New function to get the allocation data for the rewards manager + * @dev Note that this is only to make tests pass, as the staking contract with + * this changes will never get deployed. HorizonStaking is taking it's place. + */ + function getAllocationData( + address _allocationID + ) external view override returns (address, bytes32, uint256, uint256, uint256) { + Allocation memory alloc = __allocations[_allocationID]; + return (alloc.indexer, alloc.subgraphDeploymentID, alloc.tokens, alloc.accRewardsPerAllocatedToken, 0); + } + + /** + * @dev New function to get the allocation active status for the rewards manager + * @dev Note that this is only to make tests pass, as the staking contract with + * this changes will never get deployed. HorizonStaking is taking it's place. + */ + function isActiveAllocation(address _allocationID) external view override returns (bool) { + return _getAllocationState(_allocationID) == AllocationState.Active; + } + /** * @notice Return the current state of an allocation * @param _allocationID Allocation identifier @@ -893,7 +914,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M if (curationFees > 0) { // Transfer and call collect() // This function transfer tokens to a trusted protocol contracts - // Then we call collect() to do the transfer bookeeping + // Then we call collect() to do the transfer Bookkeeping rewardsManager().onSubgraphSignalUpdate(_subgraphDeploymentID); TokenUtils.pushTokens(_graphToken, address(curation), curationFees); curation.collect(_subgraphDeploymentID, curationFees); diff --git a/packages/contracts/contracts/staking/libs/Stakes.sol b/packages/contracts/contracts/staking/libs/Stakes.sol index b0524b14c..e856cdec1 100644 --- a/packages/contracts/contracts/staking/libs/Stakes.sol +++ b/packages/contracts/contracts/staking/libs/Stakes.sol @@ -85,7 +85,7 @@ library Stakes { /** * @dev Unlock tokens. * @param stake Stake data - * @param _tokens Amount of tokens to unkock + * @param _tokens Amount of tokens to unlock */ function unlockTokens(Stakes.Indexer storage stake, uint256 _tokens) internal { stake.tokensLocked = stake.tokensLocked.sub(_tokens); diff --git a/packages/contracts/contracts/token/IGraphToken.sol b/packages/contracts/contracts/token/IGraphToken.sol index 8255e18d5..df3b7643f 100644 --- a/packages/contracts/contracts/token/IGraphToken.sol +++ b/packages/contracts/contracts/token/IGraphToken.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/packages/contracts/contracts/upgrades/GraphProxy.sol b/packages/contracts/contracts/upgrades/GraphProxy.sol index 7d227b065..d6fbfac7f 100644 --- a/packages/contracts/contracts/upgrades/GraphProxy.sol +++ b/packages/contracts/contracts/upgrades/GraphProxy.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; - -import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +pragma solidity ^0.7.6 || 0.8.27; import { GraphProxyStorage } from "./GraphProxyStorage.sol"; @@ -160,7 +158,6 @@ contract GraphProxy is GraphProxyStorage, IGraphProxy { */ function _acceptUpgrade() internal { address _pendingImplementation = _getPendingImplementation(); - require(Address.isContract(_pendingImplementation), "Impl must be a contract"); require(_pendingImplementation != address(0), "Impl cannot be zero address"); require(msg.sender == _pendingImplementation, "Only pending implementation"); diff --git a/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol b/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol index 7d809d5ec..db8e9dcb3 100644 --- a/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol +++ b/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; import { Governed } from "../governance/Governed.sol"; diff --git a/packages/contracts/contracts/upgrades/GraphProxyStorage.sol b/packages/contracts/contracts/upgrades/GraphProxyStorage.sol index 05b922647..7871e4996 100644 --- a/packages/contracts/contracts/upgrades/GraphProxyStorage.sol +++ b/packages/contracts/contracts/upgrades/GraphProxyStorage.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; /** * @title Graph Proxy Storage diff --git a/packages/contracts/contracts/upgrades/GraphUpgradeable.sol b/packages/contracts/contracts/upgrades/GraphUpgradeable.sol index 862f7e7d5..60dfbe888 100644 --- a/packages/contracts/contracts/upgrades/GraphUpgradeable.sol +++ b/packages/contracts/contracts/upgrades/GraphUpgradeable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; import { IGraphProxy } from "./IGraphProxy.sol"; diff --git a/packages/contracts/contracts/upgrades/IGraphProxy.sol b/packages/contracts/contracts/upgrades/IGraphProxy.sol index 61946e948..4f501ed7c 100644 --- a/packages/contracts/contracts/upgrades/IGraphProxy.sol +++ b/packages/contracts/contracts/upgrades/IGraphProxy.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; interface IGraphProxy { function admin() external returns (address); diff --git a/packages/contracts/contracts/utils/TokenUtils.sol b/packages/contracts/contracts/utils/TokenUtils.sol index 265f918a5..fb125613a 100644 --- a/packages/contracts/contracts/utils/TokenUtils.sol +++ b/packages/contracts/contracts/utils/TokenUtils.sol @@ -1,9 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6; +pragma solidity ^0.7.6 || 0.8.27; import "../token/IGraphToken.sol"; +/** + * @title TokenUtils library + * @notice This library contains utility functions for handling tokens (transfers and burns). + * It is specifically adapted for the GraphToken, so does not need to handle edge cases + * for other tokens. + */ library TokenUtils { /** * @dev Pull tokens from an address to this contract. diff --git a/packages/contracts/eslint.config.js b/packages/contracts/eslint.config.js index c7c0ba1b2..566196117 100644 --- a/packages/contracts/eslint.config.js +++ b/packages/contracts/eslint.config.js @@ -9,6 +9,8 @@ module.exports = [ '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-redundant-type-constituents': 'off', }, }, { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index af7b27781..cde47f6eb 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,10 +1,11 @@ { "name": "@graphprotocol/contracts", - "version": "6.3.0", + "version": "7.0.0", "description": "Contracts for the Graph Protocol", "directories": { "test": "test" }, + "main": "dist/types/index.js", "types": "build/types/index.d.ts", "files": [ "dist/**/*", @@ -14,7 +15,7 @@ ], "devDependencies": { "@arbitrum/sdk": "~3.1.13", - "@defi-wonderland/smock": "^2.0.7", + "@defi-wonderland/smock": "~2.3.0", "@ethersproject/experimental": "^5.6.0", "@graphprotocol/common-ts": "^1.8.3", "@graphprotocol/sdk": "workspace:^0.5.0", @@ -70,16 +71,17 @@ "prettier-plugin-solidity": "^1.3.1", "solhint": "^4.1.1", "solhint-graph-config": "workspace:^0.0.1", + "solhint-plugin-graph": "workspace:^0.0.1", "solidity-coverage": "^0.7.16", "ts-node": "^10.9.1", "typechain": "^5.0.0", - "typescript": "^4.7.4", + "typescript": "^5.2.2", "winston": "^3.3.3", "yaml": "^1.10.2", "yargs": "^17.0.0" }, "scripts": { - "prepack": "scripts/prepack", + "prepack": "SKIP_LOAD=true scripts/build", "build": "SKIP_LOAD=true scripts/build", "clean": "rm -rf build/ cache/ dist/", "compile": "hardhat compile", @@ -93,7 +95,7 @@ "test:upgrade": "scripts/upgrade", "lint": "yarn lint:ts && yarn lint:sol", "lint:ts": "eslint '**/*.{js,ts}' --fix", - "lint:sol": "prettier --write 'contracts/**/*.sol' && solhint --fix --noPrompt contracts/**/*.sol --config node_modules/solhint-graph-config/index.js", + "lint:sol": "prettier --write 'contracts/**/*.sol' && solhint --fix --noPrompt contracts/**/*.sol --config solhint.config.js", "analyze": "scripts/analyze", "myth": "scripts/myth", "flatten": "scripts/flatten && scripts/clean", diff --git a/packages/contracts/scripts/build b/packages/contracts/scripts/build index df72b9f62..06b9823c1 100755 --- a/packages/contracts/scripts/build +++ b/packages/contracts/scripts/build @@ -1,6 +1,17 @@ #!/bin/bash +TYPECHAIN_DIR=dist/types + set -eo pipefail -# Build +# Build contracts +yarn clean yarn compile +tsc + +# Copy types and abis to distribution folder +cp -R build/types/* dist/build/types +cp -R build/abis/ dist/abis + +# Move compiled types ts +mv dist/build/types dist/types \ No newline at end of file diff --git a/packages/contracts/scripts/prepack b/packages/contracts/scripts/prepack deleted file mode 100755 index bc0e54391..000000000 --- a/packages/contracts/scripts/prepack +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -TYPECHAIN_DIR=dist/types - -set -eo pipefail -set +o noglob - -# Build contracts -yarn clean -yarn build - -# Refresh distribution folder -rm -rf dist && mkdir -p ${TYPECHAIN_DIR} -cp -R build/abis/ dist/abis -cp -R build/types/ ${TYPECHAIN_DIR} - -# Build and create TS declarations -pushd ${TYPECHAIN_DIR} -ls *.ts **/*.ts | xargs tsc --esModuleInterop -popd diff --git a/packages/contracts/scripts/test b/packages/contracts/scripts/test index 376090d90..4156166ae 100755 --- a/packages/contracts/scripts/test +++ b/packages/contracts/scripts/test @@ -6,7 +6,6 @@ source $(pwd)/scripts/evm ### Setup EVM # Ensure we compiled sources - yarn build ### Cleanup diff --git a/packages/contracts/solhint.config.js b/packages/contracts/solhint.config.js new file mode 100644 index 000000000..b8e918530 --- /dev/null +++ b/packages/contracts/solhint.config.js @@ -0,0 +1,28 @@ +module.exports = { + plugins: ['graph'], + extends: 'solhint:recommended', + rules: { + // best practices + 'no-empty-blocks': 'off', + 'constructor-syntax': 'warn', + + // style rules + 'private-vars-leading-underscore': 'off', // see graph/leading-underscore + 'const-name-snakecase': 'warn', + 'named-parameters-mapping': 'warn', + 'imports-on-top': 'warn', + 'ordering': 'warn', + 'visibility-modifier-order': 'warn', + + // miscellaneous + 'quotes': ['error', 'double'], + + // security + 'compiler-version': ['off'], + 'func-visibility': ['warn', { ignoreConstructors: true }], + 'not-rely-on-time': 'off', + + // graph + // 'graph/leading-underscore': 'warn', // Contracts were originally written with a different style + }, +} diff --git a/packages/contracts/test/e2e/scenarios/close-allocations.test.ts b/packages/contracts/test/e2e/scenarios/close-allocations.test.ts deleted file mode 100644 index a8ace4c7c..000000000 --- a/packages/contracts/test/e2e/scenarios/close-allocations.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { expect } from 'chai' -import hre from 'hardhat' -import { AllocationFixture, getIndexerFixtures, IndexerFixture } from './fixtures/indexers' - -enum AllocationState { - Null, - Active, - Closed, -} - -let indexerFixtures: IndexerFixture[] - -describe('Close allocations', () => { - const { contracts, getTestAccounts } = hre.graph() - const { Staking } = contracts - - before(async () => { - indexerFixtures = getIndexerFixtures(await getTestAccounts()) - }) - - describe('Allocations', () => { - let allocations: AllocationFixture[] = [] - let openAllocations: AllocationFixture[] = [] - let closedAllocations: AllocationFixture[] = [] - - before(() => { - allocations = indexerFixtures.map(i => i.allocations).flat() - openAllocations = allocations.filter(a => !a.close) - closedAllocations = allocations.filter(a => a.close) - }) - - it(`some allocatons should be open`, async function () { - for (const allocation of openAllocations) { - const state = await Staking.getAllocationState(allocation.signer.address) - expect(state).eq(AllocationState.Active) - } - }) - - it(`some allocatons should be closed`, async function () { - for (const allocation of closedAllocations) { - const state = await Staking.getAllocationState(allocation.signer.address) - expect(state).eq(AllocationState.Closed) - } - }) - }) -}) diff --git a/packages/contracts/test/e2e/scenarios/close-allocations.ts b/packages/contracts/test/e2e/scenarios/close-allocations.ts deleted file mode 100644 index 96c59174b..000000000 --- a/packages/contracts/test/e2e/scenarios/close-allocations.ts +++ /dev/null @@ -1,55 +0,0 @@ -// ### Scenario description ### -// Common protocol actions > Close some allocations -// This scenario will close several open allocations. See fixtures for details. -// Need to wait at least 1 epoch after the allocations have been created before running it. -// On localhost, the epoch is automatically advanced to guarantee this. -// Run with: -// npx hardhat e2e:scenario close-allocations --network --graph-config config/graph..yml - -import hre from 'hardhat' -import { getIndexerFixtures } from './fixtures/indexers' - -import { closeAllocation, helpers } from '@graphprotocol/sdk' - -import { getGREOptsFromArgv } from '@graphprotocol/sdk/gre' - -async function main() { - const graphOpts = getGREOptsFromArgv() - const graph = hre.graph(graphOpts) - const indexerFixtures = getIndexerFixtures(await graph.getTestAccounts()) - - const ethBalances = indexerFixtures.map(i => ({ - address: i.signer.address, - balance: i.ethBalance, - })) - - // == Fund participants - console.log('\n== Fund indexers') - await helpers.setBalances(ethBalances) - - // == Time travel on local networks, ensure allocations can be closed - if (['hardhat', 'localhost'].includes(hre.network.name)) { - console.log('\n== Advancing to next epoch') - await helpers.mineEpoch(graph.contracts.EpochManager) - } - - // == Close allocations - console.log('\n== Close allocations') - - for (const indexer of indexerFixtures) { - for (const allocation of indexer.allocations.filter(a => a.close)) { - await closeAllocation(graph.contracts, indexer.signer, { - allocationId: allocation.signer.address, - }) - } - } -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exitCode = 1 - }) diff --git a/packages/contracts/test/e2e/scenarios/create-subgraphs.test.ts b/packages/contracts/test/e2e/scenarios/create-subgraphs.test.ts deleted file mode 100644 index 055093dc6..000000000 --- a/packages/contracts/test/e2e/scenarios/create-subgraphs.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { expect } from 'chai' -import hre from 'hardhat' -import { recreatePreviousSubgraphId } from '@graphprotocol/sdk' -import { BigNumber } from 'ethers' -import { CuratorFixture, getCuratorFixtures } from './fixtures/curators' -import { getSubgraphFixtures, getSubgraphOwner, SubgraphFixture } from './fixtures/subgraphs' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' - -let curatorFixtures: CuratorFixture[] -let subgraphFixtures: SubgraphFixture[] -let subgraphOwnerFixture: SignerWithAddress - -describe('Publish subgraphs', () => { - const { contracts, getTestAccounts, chainId } = hre.graph() - const { GNS, GraphToken, Curation } = contracts - - before(async () => { - const testAccounts = await getTestAccounts() - curatorFixtures = getCuratorFixtures(testAccounts) - subgraphFixtures = getSubgraphFixtures() - subgraphOwnerFixture = getSubgraphOwner(testAccounts).signer - }) - - describe('GRT balances', () => { - it(`curator balances should match airdropped amount minus signalled`, async function () { - for (const curator of curatorFixtures) { - const address = curator.signer.address - const balance = await GraphToken.balanceOf(address) - expect(balance).eq(curator.grtBalance.sub(curator.signalled)) - } - }) - }) - - describe('Subgraphs', () => { - it(`should be published`, async function () { - for (let i = 0; i < subgraphFixtures.length; i++) { - const subgraphId = await recreatePreviousSubgraphId(contracts, undefined, { - owner: subgraphOwnerFixture.address, - previousIndex: subgraphFixtures.length - i, - chainId: chainId, - }) - const isPublished = await GNS.isPublished(subgraphId) - expect(isPublished).eq(true) - } - }) - - it(`should have signal`, async function () { - for (let i = 0; i < subgraphFixtures.length; i++) { - const subgraph = subgraphFixtures[i] - const subgraphId = await recreatePreviousSubgraphId(contracts, undefined, { - owner: subgraphOwnerFixture.address, - previousIndex: subgraphFixtures.length - i, - chainId: chainId, - }) - - let totalSignal: BigNumber = BigNumber.from(0) - for (const curator of curatorFixtures) { - const _subgraph = curator.subgraphs.find(s => s.deploymentId === subgraph.deploymentId) - if (_subgraph) { - totalSignal = totalSignal.add(_subgraph.signal) - } - } - - const tokens = await GNS.subgraphTokens(subgraphId) - const MAX_PPM = 1000000 - const curationTax = await Curation.curationTaxPercentage() - const tax = totalSignal.mul(curationTax).div(MAX_PPM) - expect(tokens).eq(totalSignal.sub(tax)) - } - }) - }) -}) diff --git a/packages/contracts/test/e2e/scenarios/create-subgraphs.ts b/packages/contracts/test/e2e/scenarios/create-subgraphs.ts deleted file mode 100644 index 9f2486ff7..000000000 --- a/packages/contracts/test/e2e/scenarios/create-subgraphs.ts +++ /dev/null @@ -1,73 +0,0 @@ -// ### Scenario description ### -// Common protocol actions > Set up subgraphs: publish and signal -// This scenario will create a set of subgraphs and add signal to them. See fixtures for details. -// Run with: -// npx hardhat e2e:scenario create-subgraphs --network --graph-config config/graph..yml - -import hre from 'hardhat' -import { getSubgraphFixtures, getSubgraphOwner } from './fixtures/subgraphs' -import { getCuratorFixtures } from './fixtures/curators' -import { getGREOptsFromArgv } from '@graphprotocol/sdk/gre' -import { helpers, mintSignal, publishNewSubgraph, setGRTBalances } from '@graphprotocol/sdk' - -async function main() { - const graphOpts = getGREOptsFromArgv() - const graph = hre.graph(graphOpts) - const testAccounts = await graph.getTestAccounts() - - const subgraphFixtures = getSubgraphFixtures() - const subgraphOwnerFixture = getSubgraphOwner(testAccounts) - const curatorFixtures = getCuratorFixtures(testAccounts) - - const deployer = await graph.getDeployer() - const ethBalances = [ - { - address: subgraphOwnerFixture.signer.address, - balance: subgraphOwnerFixture.ethBalance, - }, - ] - curatorFixtures.map(c => ethBalances.push({ address: c.signer.address, balance: c.ethBalance })) - const grtBalances = curatorFixtures.map(c => ({ - address: c.signer.address, - balance: c.grtBalance, - })) - - // == Fund participants - console.log('\n== Fund subgraph owners and curators') - await helpers.setBalances(ethBalances, deployer) - await setGRTBalances(graph.contracts, deployer, grtBalances) - - // == Publish subgraphs - console.log('\n== Publishing subgraphs') - - for (const subgraph of subgraphFixtures) { - const id = await publishNewSubgraph(graph.contracts, subgraphOwnerFixture.signer, { - deploymentId: subgraph.deploymentId, - chainId: graph.chainId, - }) - const subgraphData = subgraphFixtures.find(s => s.deploymentId === subgraph.deploymentId) - if (subgraphData) subgraphData.subgraphId = id - } - - // == Signal subgraphs - console.log('\n== Signaling subgraphs') - for (const curator of curatorFixtures) { - for (const subgraph of curator.subgraphs) { - const subgraphData = subgraphFixtures.find(s => s.deploymentId === subgraph.deploymentId) - if (subgraphData) - await mintSignal(graph.contracts, curator.signer, { - subgraphId: subgraphData.subgraphId, - amount: subgraph.signal, - }) - } - } -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exitCode = 1 - }) diff --git a/packages/contracts/test/e2e/scenarios/fixtures/bridge.ts b/packages/contracts/test/e2e/scenarios/fixtures/bridge.ts deleted file mode 100644 index 3e2a5f106..000000000 --- a/packages/contracts/test/e2e/scenarios/fixtures/bridge.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { toGRT } from '@graphprotocol/sdk' -import { BigNumber } from 'ethers' - -import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' - -export interface BridgeFixture { - deploymentFile: string - funder: SignerWithAddress - accountsToFund: { - signer: SignerWithAddress - amount: BigNumber - }[] -} - -// Signers -// 0: l1Deployer -// 1: l2Deployer - -export const getBridgeFixture = (signers: SignerWithAddress[]): BridgeFixture => { - return { - deploymentFile: 'localNetwork.json', - funder: signers[0], - accountsToFund: [ - { - signer: signers[1], - amount: toGRT(10_000_000), - }, - ], - } -} diff --git a/packages/contracts/test/e2e/scenarios/fixtures/curators.ts b/packages/contracts/test/e2e/scenarios/fixtures/curators.ts deleted file mode 100644 index aa060d190..000000000 --- a/packages/contracts/test/e2e/scenarios/fixtures/curators.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { toGRT } from '@graphprotocol/sdk' -import { BigNumber } from 'ethers' - -import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' - -export interface CuratorFixture { - signer: SignerWithAddress - ethBalance: BigNumber - grtBalance: BigNumber - signalled: BigNumber - subgraphs: SubgraphFixture[] -} - -export interface SubgraphFixture { - deploymentId: string - signal: BigNumber -} - -// Test account indexes -// 3: curator1 -// 4: curator2 -// 5: curator3 - -export const getCuratorFixtures = (signers: SignerWithAddress[]): CuratorFixture[] => { - return [ - // curator1 - { - signer: signers[3], - ethBalance: toGRT(0.1), - grtBalance: toGRT(100_000), - signalled: toGRT(10_400), - subgraphs: [ - { - deploymentId: '0x0653445635cc1d06bd2370d2a9a072406a420d86e7fa13ea5cde100e2108b527', - signal: toGRT(400), - }, - { - deploymentId: '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', - signal: toGRT(4_000), - }, - { - deploymentId: '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', - signal: toGRT(6_000), - }, - ], - }, - // curator2 - { - signer: signers[4], - ethBalance: toGRT(0.1), - grtBalance: toGRT(100_000), - signalled: toGRT(4_500), - subgraphs: [ - { - deploymentId: '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', - signal: toGRT(2_000), - }, - { - deploymentId: '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', - signal: toGRT(2_500), - }, - ], - }, - // curator3 - { - signer: signers[5], - ethBalance: toGRT(0.1), - grtBalance: toGRT(100_000), - signalled: toGRT(8_000), - subgraphs: [ - { - deploymentId: '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', - signal: toGRT(4_000), - }, - { - deploymentId: '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', - signal: toGRT(4_000), - }, - ], - }, - ] -} diff --git a/packages/contracts/test/e2e/scenarios/fixtures/indexers.ts b/packages/contracts/test/e2e/scenarios/fixtures/indexers.ts deleted file mode 100644 index a343c0190..000000000 --- a/packages/contracts/test/e2e/scenarios/fixtures/indexers.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { BigNumber } from 'ethers' -import { toGRT } from '@graphprotocol/sdk' - -import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' - -export interface IndexerFixture { - signer: SignerWithAddress - ethBalance: BigNumber - grtBalance: BigNumber - stake: BigNumber - allocations: AllocationFixture[] -} - -export interface AllocationFixture { - signer: SignerWithAddress - subgraphDeploymentId: string - amount: BigNumber - close: boolean -} - -// Test account indexes -// 0: indexer1 -// 1: indexer2 -// 6: allocation1 -// 7: allocation2 -// 8: allocation3 -// 9: allocation4 -// 10: allocation5 -// 11: allocation6 -// 12: allocation7 - -export const getIndexerFixtures = (signers: SignerWithAddress[]): IndexerFixture[] => { - return [ - // indexer1 - { - signer: signers[0], - ethBalance: toGRT(0.1), - grtBalance: toGRT(100_000), - stake: toGRT(100_000), - allocations: [ - { - signer: signers[6], - subgraphDeploymentId: - '0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a', - amount: toGRT(25_000), - close: false, - }, - { - signer: signers[7], - subgraphDeploymentId: - '0x0653445635cc1d06bd2370d2a9a072406a420d86e7fa13ea5cde100e2108b527', - amount: toGRT(50_000), - close: true, - }, - { - signer: signers[8], - subgraphDeploymentId: - '0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a', - amount: toGRT(10_000), - close: true, - }, - ], - }, - // indexer2 - { - signer: signers[1], - ethBalance: toGRT(0.1), - grtBalance: toGRT(100_000), - stake: toGRT(100_000), - allocations: [ - { - signer: signers[9], - subgraphDeploymentId: - '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', - amount: toGRT(25_000), - close: true, - }, - { - signer: signers[10], - subgraphDeploymentId: - '0x0653445635cc1d06bd2370d2a9a072406a420d86e7fa13ea5cde100e2108b527', - amount: toGRT(10_000), - close: false, - }, - { - signer: signers[11], - subgraphDeploymentId: - '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', - amount: toGRT(10_000), - close: true, - }, - { - signer: signers[12], - subgraphDeploymentId: - '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', - amount: toGRT(45_000), - close: true, - }, - ], - }, - ] -} diff --git a/packages/contracts/test/e2e/scenarios/fixtures/subgraphs.ts b/packages/contracts/test/e2e/scenarios/fixtures/subgraphs.ts deleted file mode 100644 index 65b6552ff..000000000 --- a/packages/contracts/test/e2e/scenarios/fixtures/subgraphs.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { toGRT } from '@graphprotocol/sdk' -import { BigNumber } from 'ethers' - -import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' - -export interface SubgraphOwnerFixture { - signer: SignerWithAddress - ethBalance: BigNumber - grtBalance: BigNumber -} - -export interface SubgraphFixture { - deploymentId: string - subgraphId: string | null -} - -// Test account indexes -// 2: subgraphOwner -export const getSubgraphOwner = (signers: SignerWithAddress[]): SubgraphOwnerFixture => { - return { - signer: signers[2], - ethBalance: toGRT(0.1), - grtBalance: toGRT(100_000), - } -} - -export const getSubgraphFixtures = (): SubgraphFixture[] => [ - { - deploymentId: '0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a', - subgraphId: null, - }, - { - deploymentId: '0x0653445635cc1d06bd2370d2a9a072406a420d86e7fa13ea5cde100e2108b527', - subgraphId: null, - }, - { - deploymentId: '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', - subgraphId: null, - }, - { - deploymentId: '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', - subgraphId: null, - }, -] diff --git a/packages/contracts/test/e2e/scenarios/open-allocations.test.ts b/packages/contracts/test/e2e/scenarios/open-allocations.test.ts deleted file mode 100644 index 159e81505..000000000 --- a/packages/contracts/test/e2e/scenarios/open-allocations.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { expect } from 'chai' -import hre from 'hardhat' -import { AllocationState } from '@graphprotocol/sdk' - -import { getIndexerFixtures, IndexerFixture } from './fixtures/indexers' - -let indexerFixtures: IndexerFixture[] - -describe('Open allocations', () => { - const { contracts, getTestAccounts } = hre.graph() - const { GraphToken, Staking } = contracts - - before(async () => { - indexerFixtures = getIndexerFixtures(await getTestAccounts()) - }) - - describe('GRT balances', () => { - it(`indexer balances should match airdropped amount minus staked`, async function () { - for (const indexer of indexerFixtures) { - const address = indexer.signer.address - const balance = await GraphToken.balanceOf(address) - expect(balance).eq(indexer.grtBalance.sub(indexer.stake)) - } - }) - }) - - describe('Staking', () => { - it(`indexers should have staked tokens`, async function () { - for (const indexer of indexerFixtures) { - const address = indexer.signer.address - const tokensStaked = (await Staking.stakes(address)).tokensStaked - expect(tokensStaked).eq(indexer.stake) - } - }) - }) - - describe('Allocations', () => { - it(`allocations should be open`, async function () { - const allocations = indexerFixtures.map(i => i.allocations).flat() - for (const allocation of allocations) { - const state = await Staking.getAllocationState(allocation.signer.address) - expect(state).eq(AllocationState.Active) - } - }) - }) -}) diff --git a/packages/contracts/test/e2e/scenarios/open-allocations.ts b/packages/contracts/test/e2e/scenarios/open-allocations.ts deleted file mode 100644 index 8beac8d99..000000000 --- a/packages/contracts/test/e2e/scenarios/open-allocations.ts +++ /dev/null @@ -1,60 +0,0 @@ -// ### Scenario description ### -// Common protocol actions > Set up indexers: stake and open allocations -// This scenario will open several allocations. See fixtures for details. -// Run with: -// npx hardhat e2e:scenario open-allocations --network --graph-config config/graph..yml - -import hre from 'hardhat' -import { getIndexerFixtures } from './fixtures/indexers' -import { getGREOptsFromArgv } from '@graphprotocol/sdk/gre' -import { allocateFrom, helpers, setGRTBalances, stake } from '@graphprotocol/sdk' - -async function main() { - const graphOpts = getGREOptsFromArgv() - const graph = hre.graph(graphOpts) - const indexerFixtures = getIndexerFixtures(await graph.getTestAccounts()) - - const deployer = await graph.getDeployer() - const indexerETHBalances = indexerFixtures.map(i => ({ - address: i.signer.address, - balance: i.ethBalance, - })) - const indexerGRTBalances = indexerFixtures.map(i => ({ - address: i.signer.address, - balance: i.grtBalance, - })) - - // == Fund participants - console.log('\n== Fund indexers') - await helpers.setBalances(indexerETHBalances, deployer) - await setGRTBalances(graph.contracts, deployer, indexerGRTBalances) - - // == Stake - console.log('\n== Staking tokens') - - for (const indexer of indexerFixtures) { - await stake(graph.contracts, indexer.signer, { amount: indexer.stake }) - } - - // == Open allocations - console.log('\n== Open allocations') - - for (const indexer of indexerFixtures) { - for (const allocation of indexer.allocations) { - await allocateFrom(graph.contracts, indexer.signer, { - allocationSigner: allocation.signer, - subgraphDeploymentID: allocation.subgraphDeploymentId, - amount: allocation.amount, - }) - } - } -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exitCode = 1 - }) diff --git a/packages/contracts/test/e2e/scenarios/send-grt-to-l2.test.ts b/packages/contracts/test/e2e/scenarios/send-grt-to-l2.test.ts deleted file mode 100644 index 3cb5ba83e..000000000 --- a/packages/contracts/test/e2e/scenarios/send-grt-to-l2.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect } from 'chai' -import hre from 'hardhat' -import { BridgeFixture, getBridgeFixture } from './fixtures/bridge' - -describe('Bridge GRT to L2', () => { - const graph = hre.graph() - let bridgeFixture: BridgeFixture - - before(async () => { - const l1Deployer = await graph.l1.getDeployer() - const l2Deployer = await graph.l2.getDeployer() - bridgeFixture = getBridgeFixture([l1Deployer, l2Deployer]) - }) - - describe('GRT balances', () => { - it(`L2 balances should match bridged amount`, async function () { - for (const account of bridgeFixture.accountsToFund) { - const l2GrtBalance = await graph.l2.contracts.GraphToken.balanceOf(account.signer.address) - expect(l2GrtBalance).eq(account.amount) - } - }) - }) -}) diff --git a/packages/contracts/test/e2e/scenarios/send-grt-to-l2.ts b/packages/contracts/test/e2e/scenarios/send-grt-to-l2.ts deleted file mode 100644 index d654866fd..000000000 --- a/packages/contracts/test/e2e/scenarios/send-grt-to-l2.ts +++ /dev/null @@ -1,40 +0,0 @@ -// ### Scenario description ### -// Bridge action > Bridge GRT tokens from L1 to L2 -// This scenario will bridge GRT tokens from L1 to L2. See fixtures for details. -// Run with: -// npx hardhat e2e:scenario send-grt-to-l2 --network --graph-config config/graph..yml - -import hre from 'hardhat' -import { getBridgeFixture } from './fixtures/bridge' -import { getGREOptsFromArgv } from '@graphprotocol/sdk/gre' -import { ethers } from 'ethers' - -async function main() { - const graphOpts = getGREOptsFromArgv() - const graph = hre.graph(graphOpts) - - const l1Deployer = await graph.l1.getDeployer() - const l2Deployer = await graph.l2.getDeployer() - - const bridgeFixture = getBridgeFixture([l1Deployer, l2Deployer]) - - // == Send GRT to L2 accounts - for (const account of bridgeFixture.accountsToFund) { - await hre.run('bridge:send-to-l2', { - ...graphOpts, - amount: ethers.utils.formatEther(account.amount), - sender: bridgeFixture.funder.address, - recipient: account.signer.address, - deploymentFile: bridgeFixture.deploymentFile, - }) - } -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exitCode = 1 - }) diff --git a/packages/contracts/test/unit/disputes/common.ts b/packages/contracts/test/unit/disputes/common.ts index 37d77fd5a..f72f3c6cf 100644 --- a/packages/contracts/test/unit/disputes/common.ts +++ b/packages/contracts/test/unit/disputes/common.ts @@ -16,7 +16,7 @@ export interface Dispute { export function createQueryDisputeID( attestation: Attestation, indexerAddress: string, - submitterAddress: string, + fishermanAddress: string, ): string { return solidityKeccak256( ['bytes32', 'bytes32', 'bytes32', 'address', 'address'], @@ -25,7 +25,7 @@ export function createQueryDisputeID( attestation.responseCID, attestation.subgraphDeploymentID, indexerAddress, - submitterAddress, + fishermanAddress, ], ) } diff --git a/packages/contracts/test/unit/l2/l2Curation.test.ts b/packages/contracts/test/unit/l2/l2Curation.test.ts index f004318f1..29ac28319 100644 --- a/packages/contracts/test/unit/l2/l2Curation.test.ts +++ b/packages/contracts/test/unit/l2/l2Curation.test.ts @@ -150,6 +150,20 @@ describe('L2Curation:Config', () => { await expect(tx).revertedWith('Only Controller governor') }) }) + + describe('subgraphService', function () { + it('should set `subgraphService`', async function () { + const newSubgraphService = randomAddress() + await curation.connect(governor).setSubgraphService(newSubgraphService) + expect(await curation.subgraphService()).eq(newSubgraphService) + }) + + it('reject set `subgraphService` if not allowed', async function () { + const newSubgraphService = randomAddress() + const tx = curation.connect(me).setSubgraphService(newSubgraphService) + await expect(tx).revertedWith('Only Controller governor') + }) + }) }) describe('L2Curation', () => { @@ -158,6 +172,7 @@ describe('L2Curation', () => { let governor: SignerWithAddress let curator: SignerWithAddress let stakingMock: SignerWithAddress + let subgraphService: SignerWithAddress let gnsImpersonator: Signer let fixture: NetworkFixture @@ -322,14 +337,14 @@ describe('L2Curation', () => { expect(afterTokenTotalSupply).eq(beforeTokenTotalSupply) } - const shouldCollect = async (tokensToCollect: BigNumber) => { + const shouldCollect = async (tokensToCollect: BigNumber, signer: SignerWithAddress = stakingMock) => { // Before state const beforePool = await curation.pools(subgraphDeploymentID) const beforeTotalBalance = await grt.balanceOf(curation.address) // Source of tokens must be the staking for this to work - await grt.connect(stakingMock).transfer(curation.address, tokensToCollect) - const tx = curation.connect(stakingMock).collect(subgraphDeploymentID, tokensToCollect) + await grt.connect(signer).transfer(curation.address, tokensToCollect) + const tx = curation.connect(signer).collect(subgraphDeploymentID, tokensToCollect) await expect(tx).emit(curation, 'Collected').withArgs(subgraphDeploymentID, tokensToCollect) // After state @@ -343,7 +358,7 @@ describe('L2Curation', () => { before(async function () { // Use stakingMock so we can call collect - [me, curator, stakingMock] = await graph.getTestAccounts() + [me, curator, stakingMock, subgraphService] = await graph.getTestAccounts() ;({ governor } = await graph.getNamedAccounts()) fixture = new NetworkFixture(graph.provider) contracts = await fixture.load(governor, true) @@ -576,10 +591,10 @@ describe('L2Curation', () => { it('reject collect tokens distributed from invalid address', async function () { const tx = curation.connect(me).collect(subgraphDeploymentID, tokensToCollect) - await expect(tx).revertedWith('Caller must be the staking contract') + await expect(tx).revertedWith('Caller must be the subgraph service or staking contract') }) - it('should collect tokens distributed to the curation pool', async function () { + it('should collect tokens distributed to the curation pool from staking contract', async function () { await controller .connect(governor) .setContractProxy(utils.id('Staking'), stakingMock.address) @@ -592,6 +607,16 @@ describe('L2Curation', () => { await shouldCollect(toGRT('500.25')) }) + it('should collect tokens distributed to the curation pool from subgraph service address', async function () { + await grt.connect(governor).mint(subgraphService.address, tokensToCollect) + await curation.connect(governor).setSubgraphService(subgraphService.address) + await shouldCollect(toGRT('1'), subgraphService) + await shouldCollect(toGRT('10'), subgraphService) + await shouldCollect(toGRT('100'), subgraphService) + await shouldCollect(toGRT('200'), subgraphService) + await shouldCollect(toGRT('500.25'), subgraphService) + }) + it('should collect tokens and then unsignal all', async function () { await controller .connect(governor) diff --git a/packages/contracts/test/unit/lib/graphTokenTests.ts b/packages/contracts/test/unit/lib/graphTokenTests.ts index 77682ada9..299458080 100644 --- a/packages/contracts/test/unit/lib/graphTokenTests.ts +++ b/packages/contracts/test/unit/lib/graphTokenTests.ts @@ -55,7 +55,7 @@ export function grtTests(isL2: boolean): void { return permit } - async function createPermitTransaction(permit: Permit, signer: string, salt: string) { + function createPermitTransaction(permit: Permit, signer: string, salt: string) { const signature: Signature = signPermit(signer, graph.chainId, grt.address, permit, salt) const wallet = new ethers.Wallet(signer, graph.provider) return grt diff --git a/packages/contracts/test/unit/rewards/rewards.test.ts b/packages/contracts/test/unit/rewards/rewards.test.ts index 089ab3801..fe5ac75fb 100644 --- a/packages/contracts/test/unit/rewards/rewards.test.ts +++ b/packages/contracts/test/unit/rewards/rewards.test.ts @@ -466,7 +466,7 @@ describe('Rewards', () => { await helpers.mine(ISSUANCE_RATE_PERIODS) // Rewards - const contractRewards = await rewardsManager.getRewards(allocationID1) + const contractRewards = await rewardsManager.getRewards(staking.address, allocationID1) // We trust using this function in the test because we tested it // standalone in a previous test @@ -504,12 +504,12 @@ describe('Rewards', () => { await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) // Rewards - const contractRewards = await rewardsManager.getRewards(allocationID1) + const contractRewards = await rewardsManager.getRewards(staking.address, allocationID1) expect(contractRewards).eq(BigNumber.from(0)) }) it('rewards should be zero if the allocation does not exist', async function () { // Rewards - const contractRewards = await rewardsManager.getRewards(allocationIDNull) + const contractRewards = await rewardsManager.getRewards(staking.address, allocationIDNull) expect(contractRewards).eq(BigNumber.from(0)) }) }) @@ -638,7 +638,6 @@ describe('Rewards', () => { const event = rewardsManager.interface.parseLog(receipt.logs[1]).args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) - expect(event.epoch).eq(await epochManager.currentEpoch()) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) // After state @@ -676,8 +675,8 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) await expect(tx) - .emit(rewardsManager, 'RewardsAssigned') - .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch(), toBN(0)) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) }) it('does not revert with an underflow if the minimum signal changes, and signal came after allocation', async function () { @@ -694,8 +693,8 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) await expect(tx) - .emit(rewardsManager, 'RewardsAssigned') - .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch(), toBN(0)) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) }) it('does not revert if signal was already under minimum', async function () { @@ -711,8 +710,8 @@ describe('Rewards', () => { const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) await expect(tx) - .emit(rewardsManager, 'RewardsAssigned') - .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch(), toBN(0)) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) }) it('should distribute rewards on closed allocation and send to destination', async function () { @@ -746,7 +745,6 @@ describe('Rewards', () => { const event = rewardsManager.interface.parseLog(receipt.logs[1]).args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) - expect(event.epoch).eq(await epochManager.currentEpoch()) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) // After state @@ -838,7 +836,7 @@ describe('Rewards', () => { const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) await expect(tx) .emit(rewardsManager, 'RewardsDenied') - .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch()) + .withArgs(indexer1.address, allocationID1) }) }) }) @@ -1028,7 +1026,7 @@ describe('Rewards', () => { await staking.connect(assetHolder).collect(tokensToCollect, allocationID1) // check rewards diff - await rewardsManager.getRewards(allocationID1).then(formatGRT) + await rewardsManager.getRewards(staking.address, allocationID1).then(formatGRT) await helpers.mine() const accrual = await getRewardsAccrual(subgraphs) diff --git a/packages/contracts/test/unit/staking/allocation.test.ts b/packages/contracts/test/unit/staking/allocation.test.ts index bd930eaba..10bdd22be 100644 --- a/packages/contracts/test/unit/staking/allocation.test.ts +++ b/packages/contracts/test/unit/staking/allocation.test.ts @@ -895,7 +895,7 @@ describe('Staking:Allocation', () => { poi, false, ) - await expect(tx).not.to.emit(rewardsManager, 'RewardsAssigned') + await expect(tx).not.to.emit(rewardsManager, 'HorizonRewardsAssigned') }) it('reject close if not the owner of allocation', async function () { diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index a12f7de23..a876614c5 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -13,6 +13,7 @@ "truffle.js", "eslint.config.js", "prettier.config.js", + "solhint.config.js", "hardhat.config.ts", "index.d.ts", "scripts/**/*.ts", diff --git a/packages/eslint-graph-config/README.md b/packages/eslint-graph-config/README.md index 3e0a41cff..794ad677d 100644 --- a/packages/eslint-graph-config/README.md +++ b/packages/eslint-graph-config/README.md @@ -5,13 +5,13 @@ This repository contains shared linting and formatting rules for TypeScript proj ## Installation ```bash -yarn add --dev eslint eslint-graph-config +yarn add --dev eslint@^8.56.0 eslint-graph-config ``` For projects on this monorepo, you can use the following command to install the package: ```bash -yarn add --dev eslint eslint-graph-config@workspace:^x.y.z +yarn add --dev eslint@^8.56.0 eslint-graph-config@workspace:^x.y.z ``` To enable the rules, you need to create an `eslint.config.js` file in the root of your project with the following content: diff --git a/packages/eslint-graph-config/index.ts b/packages/eslint-graph-config/index.ts index b9548595e..f0b045b12 100644 --- a/packages/eslint-graph-config/index.ts +++ b/packages/eslint-graph-config/index.ts @@ -36,7 +36,7 @@ export default [ '@typescript-eslint/no-inferrable-types': 'warn', '@typescript-eslint/no-empty-function': 'warn', 'no-only-tests/no-only-tests': 'error', - 'no-secrets/no-secrets': ['error', { tolerance: 4.1 }], + 'no-secrets/no-secrets': ['error', { tolerance: 5.1 }], 'sort-imports': [ 'warn', { memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], diff --git a/packages/hardhat-graph-protocol/.mocharc.json b/packages/hardhat-graph-protocol/.mocharc.json new file mode 100644 index 000000000..de4e97026 --- /dev/null +++ b/packages/hardhat-graph-protocol/.mocharc.json @@ -0,0 +1,5 @@ +{ + "require": "ts-node/register/files", + "ignore": ["test/fixtures/**/*"], + "timeout": 6000 +} diff --git a/packages/hardhat-graph-protocol/README.md b/packages/hardhat-graph-protocol/README.md new file mode 100644 index 000000000..9f9d6eb71 --- /dev/null +++ b/packages/hardhat-graph-protocol/README.md @@ -0,0 +1,32 @@ +# hardhat-graph-protocol + + +## Usage + +Install with yarn + +```bash +yarn add --dev hardhat-graph-protocol + +# From the monorepo +yarn add --dev hardhat-graph-protocol@workspace:^x.y.z +``` + +And add it to your `hardhat.config.ts`: + +```ts +import "hardhat-graph-protocol"; + +export default { + ... + graph: { + deployments: { + horizon: require.resolve('@graphprotocol/horizon/addresses.json'), + subgraphService: require.resolve('@graphprotocol/subgraph-service/addresses.json'), + } + }, + ... +}; +``` + +_Note_: When using the plugin from within this monorepo TypeScript fails to properly apply the type extension typings. This is a known issue and can be worked around by adding a `types/hardhat-graph-protocol.d.ts` file with the same content as the `type-extensions.ts` file in this repository. \ No newline at end of file diff --git a/packages/hardhat-graph-protocol/eslint.config.js b/packages/hardhat-graph-protocol/eslint.config.js new file mode 100644 index 000000000..ed2d51271 --- /dev/null +++ b/packages/hardhat-graph-protocol/eslint.config.js @@ -0,0 +1,14 @@ +const config = require('eslint-graph-config') + +module.exports = [ + ...config.default, + { + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + }, + }, +] diff --git a/packages/hardhat-graph-protocol/package.json b/packages/hardhat-graph-protocol/package.json new file mode 100644 index 000000000..7140122db --- /dev/null +++ b/packages/hardhat-graph-protocol/package.json @@ -0,0 +1,68 @@ +{ + "name": "hardhat-graph-protocol", + "version": "0.0.1", + "description": "A hardhat plugin that extends the runtime environment to inject additional functionality related to the usage of the Graph Protocol.", + "keywords": [ + "ethereum", + "smart-contracts", + "hardhat", + "hardhat-plugin", + "graph", + "graph-protocol", + "horizon" + ], + "author": "Tomás Migone ", + "license": "MIT", + "main": "./dist/src/index.js", + "exports": { + ".": { + "types": "./src/types.ts", + "default": "./src/index.ts" + }, + "./sdk": { + "types": "./src/sdk/index.ts", + "default": "./src/sdk/index.ts" + } + }, + "types": "./dist/src/index.d.ts", + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "lint": "eslint '**/*.{js,ts}' --fix", + "test": "mocha --exit --recursive 'test/**/*.test.ts'", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist/", + "src/", + "LICENSE", + "README.md" + ], + "dependencies": { + "@graphprotocol/contracts": "workspace:^7.0.0", + "@graphprotocol/horizon": "workspace:^0.0.1", + "@graphprotocol/subgraph-service": "workspace:^0.0.1", + "@nomicfoundation/hardhat-ethers": "^3.0.8", + "debug": "^4.3.7", + "json5": "^2.2.3" + }, + "devDependencies": { + "@nomicfoundation/hardhat-verify": "^2.0.12", + "@types/chai": "^4.0.0", + "@types/debug": "^4.1.12", + "@types/mocha": "^10.0.9", + "chai": "^4.0.0", + "eslint": "^8.56.0", + "eslint-graph-config": "workspace:^0.0.1", + "ethers": "^6.13.4", + "hardhat": "^2.22.16", + "hardhat-secure-accounts": "^1.0.4", + "mocha": "^10.8.2", + "ts-node": "^8.0.0", + "typescript": "^5.6.3" + }, + "peerDependencies": { + "ethers": "^6.13.4", + "hardhat": "^2.22.16" + } +} diff --git a/packages/hardhat-graph-protocol/src/config.ts b/packages/hardhat-graph-protocol/src/config.ts new file mode 100644 index 000000000..9f2e2af82 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/config.ts @@ -0,0 +1,44 @@ +import fs from 'fs' + +import { GraphPluginError } from './sdk/utils/error' +import { logDebug } from './logger' +import { normalizePath } from './sdk/utils/path' + +import type { GraphDeployment, GraphRuntimeEnvironmentOptions } from './types' +import type { HardhatRuntimeEnvironment } from 'hardhat/types' + +export function getAddressBookPath( + deployment: GraphDeployment, + hre: HardhatRuntimeEnvironment, + opts: GraphRuntimeEnvironmentOptions, +): string { + const optsPath = getPath(opts.deployments?.[deployment]) + const networkPath = getPath(hre.network.config.deployments?.[deployment]) + const globalPath = getPath(hre.config.graph?.deployments?.[deployment]) + + logDebug(`Getting address book path...`) + logDebug(`Graph base dir: ${hre.config.paths.graph}`) + logDebug(`1) opts: ${optsPath}`) + logDebug(`2) network: ${networkPath}`) + logDebug(`3) global: ${globalPath}`) + + const addressBookPath = optsPath ?? networkPath ?? globalPath + if (addressBookPath === undefined) { + throw new GraphPluginError('Must set a an addressBook path!') + } + + const normalizedAddressBookPath = normalizePath(addressBookPath, hre.config.paths.graph) + logDebug(`Address book path: ${normalizedAddressBookPath}`) + return normalizedAddressBookPath +} + +function getPath(value: string | { + addressBook: string +} | undefined): string | undefined { + if (typeof value === 'string') { + return value + } else if (value && typeof value == 'object') { + return value.addressBook + } + return +} diff --git a/packages/hardhat-graph-protocol/src/deployment-list.ts b/packages/hardhat-graph-protocol/src/deployment-list.ts new file mode 100644 index 000000000..fb3e46fab --- /dev/null +++ b/packages/hardhat-graph-protocol/src/deployment-list.ts @@ -0,0 +1,19 @@ +import type { GraphHorizonAddressBook, GraphHorizonContracts } from './sdk/deployments/horizon' +import type { SubgraphServiceAddressBook, SubgraphServiceContracts } from './sdk/deployments/subgraph-service' + +// List of supported Graph deployments +export const GraphDeploymentsList = [ + 'horizon', + 'subgraphService', +] as const + +export type GraphDeploymentRuntimeEnvironmentMap = { + horizon: { + contracts: GraphHorizonContracts + addressBook: GraphHorizonAddressBook + } + subgraphService: { + contracts: SubgraphServiceContracts + addressBook: SubgraphServiceAddressBook + } +} diff --git a/packages/hardhat-graph-protocol/src/gre.ts b/packages/hardhat-graph-protocol/src/gre.ts new file mode 100644 index 000000000..7f6e0566a --- /dev/null +++ b/packages/hardhat-graph-protocol/src/gre.ts @@ -0,0 +1,85 @@ +import path from 'path' + +import { getAddressBookPath } from './config' +import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider' +import { lazyFunction } from 'hardhat/plugins' +import { logDebug } from './logger' + +import { GraphHorizonAddressBook } from './sdk/deployments/horizon' +import { SubgraphServiceAddressBook } from './sdk/deployments/subgraph-service' + +import { assertGraphRuntimeEnvironment, type GraphRuntimeEnvironmentOptions, isGraphDeployment } from './types' +import type { HardhatConfig, HardhatRuntimeEnvironment, HardhatUserConfig } from 'hardhat/types' + +export const greExtendConfig = (config: HardhatConfig, userConfig: Readonly) => { + const userPath = userConfig.paths?.graph + + let newPath: string + if (userPath === undefined) { + newPath = config.paths.root + } else { + if (path.isAbsolute(userPath)) { + newPath = userPath + } else { + newPath = path.normalize(path.join(config.paths.root, userPath)) + } + } + + config.paths.graph = newPath +} + +export const greExtendEnvironment = (hre: HardhatRuntimeEnvironment) => { + hre.graph = lazyFunction(() => (opts: GraphRuntimeEnvironmentOptions = { deployments: {} }) => { + logDebug('*** Initializing Graph Runtime Environment (GRE) ***') + logDebug(`Main network: ${hre.network.name}`) + const chainId = hre.network.config.chainId + if (chainId === undefined) { + throw new Error('Please define chainId in your Hardhat network configuration') + } + logDebug(`Chain Id: ${chainId}`) + + const deployments = [ + ...Object.keys(opts.deployments ?? {}), + ...Object.keys(hre.network.config.deployments ?? {}), + ...Object.keys(hre.config.graph?.deployments ?? {}), + ].filter(v => isGraphDeployment(v)) + logDebug(`Detected deployments: ${deployments.join(', ')}`) + + // Build the Graph Runtime Environment (GRE) for each deployment + const provider = new HardhatEthersProvider(hre.network.provider, hre.network.name) + const greDeployments: Record = {} + for (const deployment of deployments) { + logDebug(`== Initializing deployment: ${deployment} ==`) + const addressBookPath = getAddressBookPath(deployment, hre, opts) + let addressBook + + switch (deployment) { + case 'horizon': + addressBook = new GraphHorizonAddressBook(addressBookPath, chainId) + greDeployments.horizon = { + addressBook: addressBook, + contracts: addressBook.loadContracts(provider), + } + break + case 'subgraphService': + addressBook = new SubgraphServiceAddressBook(addressBookPath, chainId) + greDeployments.subgraphService = { + addressBook: addressBook, + contracts: addressBook.loadContracts(provider), + } + break + default: + break + } + } + + const gre = { + ...greDeployments, + provider, + chainId, + } + assertGraphRuntimeEnvironment(gre) + logDebug('GRE initialized successfully!') + return gre + }) +} diff --git a/packages/hardhat-graph-protocol/src/index.ts b/packages/hardhat-graph-protocol/src/index.ts new file mode 100644 index 000000000..f4284811c --- /dev/null +++ b/packages/hardhat-graph-protocol/src/index.ts @@ -0,0 +1,10 @@ +import { extendConfig, extendEnvironment } from 'hardhat/config' +import { greExtendConfig, greExtendEnvironment } from './gre' + +// This import is needed to let the TypeScript compiler know that it should include your type +// extensions in your npm package's types file. +import './type-extensions' + +// ** Graph Runtime Environment (GRE) extensions for the HRE ** +extendConfig(greExtendConfig) +extendEnvironment(greExtendEnvironment) diff --git a/packages/hardhat-graph-protocol/src/logger.ts b/packages/hardhat-graph-protocol/src/logger.ts new file mode 100644 index 000000000..3529373f6 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/logger.ts @@ -0,0 +1,7 @@ +import debug from 'debug' + +const LOG_BASE = 'hardhat:graph' + +export const logDebug = debug(`${LOG_BASE}:debug`) +export const logWarn = debug(`${LOG_BASE}:warn`) +export const logError = debug(`${LOG_BASE}:error`) diff --git a/packages/hardhat-graph-protocol/src/sdk/address-book.ts b/packages/hardhat-graph-protocol/src/sdk/address-book.ts new file mode 100644 index 000000000..3c9634bec --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/address-book.ts @@ -0,0 +1,249 @@ +import fs from 'fs' + +import { assertObject } from './utils/assertion' + +import { ContractList, loadContract } from './lib/contract' +import { logDebug, logError, logWarn } from '../logger' +import { Provider, Signer } from 'ethers' + +export type AddressBookJson< + ChainId extends number = number, + ContractName extends string = string, +> = Record> + +export type AddressBookEntry = { + address: string + proxy?: 'graph' | 'transparent' + proxyAdmin?: string + implementation?: string +} + +/** + * An abstract class to manage an address book + * The address book must be a JSON file with the following structure: + * { + * "": { + * "": { + * "address": "
", + * "proxy": "", // optional + * "proxyAdmin": "
", // optional + * "implementation": "
", // optional + * ... + * } + * } + * Uses generics to allow specifying a ContractName type to indicate which contracts should be loaded from the address book + * Implementation should provide: + * - `isContractName(name: string): name is ContractName`, a type predicate to check if a given string is a ContractName + * - `loadContracts(signerOrProvider?: Signer | Provider): ContractList` to load contracts from the address book + */ +export abstract class AddressBook< + ChainId extends number = number, + ContractName extends string = string, +> { + // The path to the address book file + public file: string + + // The chain id of the network the address book should be loaded for + public chainId: ChainId + + // The raw contents of the address book file + public addressBook: AddressBookJson + + // Contracts in the address book of type ContractName + private validContracts: ContractName[] = [] + + // Contracts in the address book that are not of type ContractName, these are ignored + private invalidContracts: string[] = [] + + // Type predicate to check if a given string is a ContractName + abstract isContractName(name: string): name is ContractName + + // Method to load valid contracts from the address book + abstract loadContracts(signerOrProvider?: Signer | Provider): ContractList + + /** + * Constructor for the `AddressBook` class + * + * @param _file the path to the address book file + * @param _chainId the chain id of the network the address book should be loaded for + * @param _strictAssert + * + * @throws AssertionError if the target file is not a valid address book + * @throws Error if the target file does not exist + */ + constructor(_file: string, _chainId: ChainId, _strictAssert = false) { + this.file = _file + this.chainId = _chainId + + logDebug(`Loading address book from ${this.file}.`) + + // Create empty address book if file doesn't exist + if (!fs.existsSync(this.file)) { + const emptyAddressBook = { [this.chainId]: {} } + fs.writeFileSync(this.file, JSON.stringify(emptyAddressBook, null, 2)) + logDebug(`Created new address book at ${this.file}`) + } + + // Load address book and validate its shape + const fileContents = JSON.parse(fs.readFileSync(this.file, 'utf8')) + if (!fileContents[this.chainId]) { + fileContents[this.chainId] = {} + } + this.assertAddressBookJson(fileContents) + this.addressBook = fileContents + this._parseAddressBook() + } + + /** + * List entry names in the address book + * + * @returns a list with all the names of the entries in the address book + */ + listEntries(): ContractName[] { + return this.validContracts + } + + entryExists(name: string): boolean { + if (!this.isContractName(name)) { + throw new Error(`Contract name ${name} is not a valid contract name`) + } + return this.addressBook[this.chainId][name] !== undefined + } + + /** + * Get an entry from the address book + * + * @param name the name of the contract to get + * @param strict if true it will throw an error if the contract is not found + * @returns the address book entry for the contract + * Returns an empty address book entry if the contract is not found + */ + getEntry(name: string): AddressBookEntry { + if (!this.isContractName(name)) { + throw new Error(`Contract name ${name} is not a valid contract name`) + } + const entry = this.addressBook[this.chainId][name] + this._assertAddressBookEntry(entry) + return entry + } + + /** + * Save an entry to the address book + * Allows partial address book entries to be saved + * @param name the name of the contract to save + * @param entry the address book entry for the contract + */ + setEntry(name: ContractName, entry: Partial): void { + if (entry.address === undefined) { + entry.address = '0x0000000000000000000000000000000000000000' + } + this._assertAddressBookEntry(entry) + this.addressBook[this.chainId][name] = entry + try { + fs.writeFileSync(this.file, JSON.stringify(this.addressBook, null, 2)) + } catch (e: unknown) { + if (e instanceof Error) logError(`Error saving entry: ${e.message}`) + else logError(`Error saving entry`) + } + } + + /** + * Parse address book and separate valid and invalid contracts + */ + _parseAddressBook() { + const contractList = this.addressBook[this.chainId] + + const contractNames = contractList ? Object.keys(contractList) : [] + for (const contract of contractNames) { + if (!this.isContractName(contract)) { + this.invalidContracts.push(contract) + } else { + this.validContracts.push(contract) + } + } + + if (this.invalidContracts.length > 0) { + logWarn(`Detected invalid contracts in address book - these will not be loaded: ${this.invalidContracts.join(', ')}`) + } + } + + /** + * Loads all valid contracts from an address book + * + * @param addressBook Address book to use + * @param signerOrProvider Signer or provider to use + * @returns the loaded contracts + */ + _loadContracts( + artifactsPath: string | string[] | Record, + signerOrProvider?: Signer | Provider, + ): ContractList { + const contracts = {} as ContractList + if (this.listEntries().length == 0) { + logError('No valid contracts found in address book') + return contracts + } + for (const contractName of this.listEntries()) { + const artifactPath = typeof artifactsPath === 'object' && !Array.isArray(artifactsPath) + ? artifactsPath[contractName] + : artifactsPath + + if (Array.isArray(artifactPath) + ? !artifactPath.some(fs.existsSync) + : !fs.existsSync(artifactPath)) { + logWarn(`Could not load contract ${contractName} - artifact not found`) + logWarn(artifactPath) + continue + } + logDebug(`Loading contract ${contractName}`) + + const contract = loadContract( + contractName, + this.getEntry(contractName).address, + artifactPath, + signerOrProvider, + ) + contracts[contractName] = contract + } + + return contracts + } + + // Asserts the provided object has the correct JSON format shape for an address book + // This method can be overridden by subclasses to provide custom validation + assertAddressBookJson( + json: unknown, + ): asserts json is AddressBookJson { + this._assertAddressBookJson(json) + } + + // Asserts the provided object is a valid address book + _assertAddressBookJson(json: unknown): asserts json is AddressBookJson { + assertObject(json, 'Assertion failed: address book is not an object') + + const contractList = json[this.chainId] + assertObject(contractList, 'Assertion failed: chain contract list is not an object') + + const contractNames = Object.keys(contractList) + for (const contractName of contractNames) { + this._assertAddressBookEntry(contractList[contractName]) + } + } + + // Asserts the provided object is a valid address book entry + _assertAddressBookEntry( + entry: unknown, + ): asserts entry is AddressBookEntry { + assertObject(entry) + if (!('address' in entry)) { + throw new Error('Address book entry must have an address field') + } + + const allowedFields = ['address', 'implementation', 'proxyAdmin', 'proxy'] + const entryFields = Object.keys(entry) + const invalidFields = entryFields.filter(field => !allowedFields.includes(field)) + if (invalidFields.length > 0) { + throw new Error(`Address book entry contains invalid fields: ${invalidFields.join(', ')}`) + } + } +} diff --git a/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/address-book.ts b/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/address-book.ts new file mode 100644 index 000000000..e940a6313 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/address-book.ts @@ -0,0 +1,65 @@ +import { GraphHorizonArtifactsMap, GraphHorizonContractNameList } from './contracts' +import { logDebug, logError } from '../../../logger' +import { Provider, Signer } from 'ethers' +import { AddressBook } from '../../address-book' +import { assertObject } from '../../utils/assertion' +import { Contract } from 'ethers' +import { loadArtifact } from '../../lib/artifact' +import { mergeABIs } from '../../utils/abi' + +import type { GraphHorizonContractName, GraphHorizonContracts } from './contracts' + +export class GraphHorizonAddressBook extends AddressBook { + isContractName(name: unknown): name is GraphHorizonContractName { + return ( + typeof name === 'string' + && GraphHorizonContractNameList.includes(name as GraphHorizonContractName) + ) + } + + loadContracts( + signerOrProvider?: Signer | Provider, + ): GraphHorizonContracts { + logDebug('Loading Graph Horizon contracts...') + + const contracts = this._loadContracts( + GraphHorizonArtifactsMap, + signerOrProvider, + ) + + // Handle HorizonStaking specially to include extension functions + if (contracts.HorizonStaking) { + const stakingOverride = new Contract( + this.getEntry('HorizonStaking').address, + mergeABIs( + loadArtifact('HorizonStaking', GraphHorizonArtifactsMap.HorizonStaking).abi, + loadArtifact('HorizonStakingExtension', GraphHorizonArtifactsMap.HorizonStaking).abi, + ), + signerOrProvider, + ) + contracts.HorizonStaking = stakingOverride + } + + this._assertGraphHorizonContracts(contracts) + + // Aliases + contracts.GraphToken = contracts.L2GraphToken + contracts.GraphTokenGateway = contracts.L2GraphTokenGateway + + return contracts + } + + _assertGraphHorizonContracts( + contracts: unknown, + ): asserts contracts is GraphHorizonContracts { + assertObject(contracts) + + // Assert that all GraphHorizonContracts were loaded + for (const contractName of GraphHorizonContractNameList) { + if (!contracts[contractName]) { + const errMessage = `Missing GraphHorizon contract: ${contractName}` + logError(errMessage) + } + } + } +} diff --git a/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/contracts.ts b/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/contracts.ts new file mode 100644 index 000000000..2797f2c83 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/contracts.ts @@ -0,0 +1,77 @@ +import path from 'path' + +import type { + Controller, + EpochManager, + GraphProxyAdmin, + L2GraphToken, + L2GraphTokenGateway, + RewardsManager, +} from '@graphprotocol/contracts' +import type { + GraphPayments, + GraphTallyCollector, + HorizonStaking, + PaymentsEscrow, +} from '@graphprotocol/horizon' +import type { ContractList } from '../../lib/contract' + +export const GraphHorizonContractNameList = [ + // @graphprotocol/contracts + 'GraphProxyAdmin', + 'Controller', + 'EpochManager', + 'RewardsManager', + 'L2GraphToken', + 'L2GraphTokenGateway', + 'L2Curation', + + // @graphprotocol/horizon + 'HorizonStaking', + 'GraphPayments', + 'PaymentsEscrow', + 'GraphTallyCollector', +] as const + +const root = path.resolve(__dirname, '../../../..') // hardhat-graph-protocol root +export const CONTRACTS_ARTIFACTS_PATH = path.resolve(root, 'node_modules', '@graphprotocol/contracts/build/contracts') +export const HORIZON_ARTIFACTS_PATH = path.resolve(root, 'node_modules', '@graphprotocol/horizon/build/contracts') + +export const GraphHorizonArtifactsMap = { + // @graphprotocol/contracts + GraphProxyAdmin: CONTRACTS_ARTIFACTS_PATH, + Controller: CONTRACTS_ARTIFACTS_PATH, + EpochManager: CONTRACTS_ARTIFACTS_PATH, + RewardsManager: CONTRACTS_ARTIFACTS_PATH, + L2GraphToken: CONTRACTS_ARTIFACTS_PATH, + L2GraphTokenGateway: CONTRACTS_ARTIFACTS_PATH, + L2Curation: CONTRACTS_ARTIFACTS_PATH, + + // @graphprotocol/horizon + HorizonStaking: HORIZON_ARTIFACTS_PATH, + GraphPayments: HORIZON_ARTIFACTS_PATH, + PaymentsEscrow: HORIZON_ARTIFACTS_PATH, + GraphTallyCollector: HORIZON_ARTIFACTS_PATH, +} as const + +export interface GraphHorizonContracts extends ContractList { + // @graphprotocol/contracts + EpochManager: EpochManager + RewardsManager: RewardsManager + GraphProxyAdmin: GraphProxyAdmin + Controller: Controller + L2GraphToken: L2GraphToken + L2GraphTokenGateway: L2GraphTokenGateway + + // @graphprotocol/horizon + HorizonStaking: HorizonStaking + GraphPayments: GraphPayments + PaymentsEscrow: PaymentsEscrow + GraphTallyCollector: GraphTallyCollector + + // Aliases + GraphToken: L2GraphToken + GraphTokenGateway: L2GraphTokenGateway +} + +export type GraphHorizonContractName = (typeof GraphHorizonContractNameList)[number] diff --git a/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/index.ts b/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/index.ts new file mode 100644 index 000000000..1a451a161 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/deployments/horizon/index.ts @@ -0,0 +1,6 @@ +import { GraphHorizonAddressBook } from './address-book' + +import type { GraphHorizonContractName, GraphHorizonContracts } from './contracts' + +export { GraphHorizonAddressBook } +export type { GraphHorizonContractName, GraphHorizonContracts } diff --git a/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/address-book.ts b/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/address-book.ts new file mode 100644 index 000000000..3fca297a0 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/address-book.ts @@ -0,0 +1,48 @@ +import { logDebug, logError } from '../../../logger' +import { Provider, Signer } from 'ethers' +import { SubgraphServiceArtifactsMap, SubgraphServiceContractNameList } from './contracts' +import { AddressBook } from '../../address-book' +import { assertObject } from '../../utils/assertion' + +import type { SubgraphServiceContractName, SubgraphServiceContracts } from './contracts' + +export class SubgraphServiceAddressBook extends AddressBook { + isContractName(name: unknown): name is SubgraphServiceContractName { + return ( + typeof name === 'string' + && SubgraphServiceContractNameList.includes(name as SubgraphServiceContractName) + ) + } + + loadContracts( + signerOrProvider?: Signer | Provider, + ): SubgraphServiceContracts { + logDebug('Loading Subgraph Service contracts...') + + const contracts = this._loadContracts( + SubgraphServiceArtifactsMap, + signerOrProvider, + ) + this._assertSubgraphServiceContracts(contracts) + + // Aliases + contracts.Curation = contracts.L2Curation + contracts.GNS = contracts.L2GNS + + return contracts + } + + _assertSubgraphServiceContracts( + contracts: unknown, + ): asserts contracts is SubgraphServiceContracts { + assertObject(contracts) + + // Assert that all SubgraphServiceContracts were loaded + for (const contractName of SubgraphServiceContractNameList) { + if (!contracts[contractName]) { + const errMessage = `Missing SubgraphService contract: ${contractName}` + logError(errMessage) + } + } + } +} diff --git a/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/contracts.ts b/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/contracts.ts new file mode 100644 index 000000000..1e770e1ff --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/contracts.ts @@ -0,0 +1,60 @@ +import path from 'path' + +import type { + L2Curation, + L2GNS, + ServiceRegistry, + SubgraphNFT, +} from '@graphprotocol/contracts' + +import type { + DisputeManager, + SubgraphService, +} from '@graphprotocol/subgraph-service' +import type { ContractList } from '../../lib/contract' + +export const SubgraphServiceContractNameList = [ + // @graphprotocol/contracts + 'L2Curation', + 'L2GNS', + 'SubgraphNFT', + 'ServiceRegistry', + + // @graphprotocol/subgraph-service + 'SubgraphService', + 'DisputeManager', +] as const + +const root = path.resolve(__dirname, '../../../..') // hardhat-graph-protocol root +export const CONTRACTS_ARTIFACTS_PATH = path.resolve(root, 'node_modules', '@graphprotocol/contracts/build/contracts') +export const SUBGRAPH_SERVICE_ARTIFACTS_PATH = path.resolve(root, 'node_modules', '@graphprotocol/subgraph-service/build/contracts') + +export const SubgraphServiceArtifactsMap = { + // @graphprotocol/contracts + L2Curation: CONTRACTS_ARTIFACTS_PATH, + L2GNS: CONTRACTS_ARTIFACTS_PATH, + SubgraphNFT: CONTRACTS_ARTIFACTS_PATH, + ServiceRegistry: CONTRACTS_ARTIFACTS_PATH, + + // @graphprotocol/subgraph-service + SubgraphService: SUBGRAPH_SERVICE_ARTIFACTS_PATH, + DisputeManager: SUBGRAPH_SERVICE_ARTIFACTS_PATH, +} as const + +export interface SubgraphServiceContracts extends ContractList { + // @graphprotocol/contracts + L2Curation: L2Curation + L2GNS: L2GNS + SubgraphNFT: SubgraphNFT + ServiceRegistry: ServiceRegistry + + // @graphprotocol/subgraph-service + SubgraphService: SubgraphService + DisputeManager: DisputeManager + + // Aliases + Curation: L2Curation + GNS: L2GNS +} + +export type SubgraphServiceContractName = (typeof SubgraphServiceContractNameList)[number] diff --git a/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/index.ts b/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/index.ts new file mode 100644 index 000000000..8f9d7bfa6 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/deployments/subgraph-service/index.ts @@ -0,0 +1,6 @@ +import { SubgraphServiceAddressBook } from './address-book' + +import type { SubgraphServiceContractName, SubgraphServiceContracts } from './contracts' + +export { SubgraphServiceAddressBook } +export type { SubgraphServiceContractName, SubgraphServiceContracts } diff --git a/packages/hardhat-graph-protocol/src/sdk/hardhat.base.config.ts b/packages/hardhat-graph-protocol/src/sdk/hardhat.base.config.ts new file mode 100644 index 000000000..26536ea29 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/hardhat.base.config.ts @@ -0,0 +1,114 @@ +import { vars } from 'hardhat/config' + +import type { HardhatUserConfig, NetworksUserConfig, ProjectPathsUserConfig, SolidityUserConfig } from 'hardhat/types' +import type { EtherscanConfig } from '@nomicfoundation/hardhat-verify/types' +import type { GraphRuntimeEnvironmentOptions } from '../types' + +// TODO: this should be imported from hardhat-secure-accounts, but currently it's not exported +interface SecureAccountsOptions { + enabled?: boolean +} + +// Hardhat variables +const ARBITRUM_ONE_RPC = vars.get('ARBITRUM_ONE_RPC', 'https://arb1.arbitrum.io/rpc') +const ARBITRUM_SEPOLIA_RPC = vars.get('ARBITRUM_SEPOLIA_RPC', 'https://sepolia-rollup.arbitrum.io/rpc') +const LOCALHOST_RPC = vars.get('LOCALHOST_RPC', 'http://localhost:8545') +const LOCALHOST_CHAIN_ID = vars.get('LOCALHOST_CHAIN_ID', '31337') + +export const solidityUserConfig: SolidityUserConfig = { + version: '0.8.27', + settings: { + optimizer: { + enabled: true, + runs: 100, + }, + }, +} + +export const projectPathsUserConfig: ProjectPathsUserConfig = { + artifacts: './build/contracts', + sources: './contracts', +} + +export const etherscanUserConfig: Partial = { + apiKey: { + ...(vars.has('ARBISCAN_API_KEY') && { + arbitrumSepolia: vars.get('ARBISCAN_API_KEY'), + }), + }, +} + +// In general: +// - hardhat is used for unit tests +// - localhost is used for local development on a hardhat network or fork +type BaseNetworksUserConfig = NetworksUserConfig & + Record +export const networksUserConfig: BaseNetworksUserConfig = { + hardhat: { + chainId: 31337, + accounts: { + mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect', + }, + deployments: { + horizon: resolveLocalAddressBook('@graphprotocol/horizon/addresses.json'), + subgraphService: resolveLocalAddressBook('@graphprotocol/subgraph-service/addresses.json'), + }, + }, + localhost: { + chainId: parseInt(LOCALHOST_CHAIN_ID), + url: LOCALHOST_RPC, + secureAccounts: { + enabled: true, + }, + ...(vars.has('LOCALHOST_ACCOUNTS_MNEMONIC') && { + accounts: { mnemonic: vars.get('LOCALHOST_ACCOUNTS_MNEMONIC') }, + }), + deployments: { + horizon: resolveLocalAddressBook('@graphprotocol/horizon/addresses.json'), + subgraphService: resolveLocalAddressBook('@graphprotocol/subgraph-service/addresses.json'), + }, + }, + arbitrumOne: { + chainId: 42161, + url: ARBITRUM_ONE_RPC, + secureAccounts: { + enabled: true, + }, + }, + arbitrumSepolia: { + chainId: 421614, + url: ARBITRUM_SEPOLIA_RPC, + secureAccounts: { + enabled: true, + }, + }, +} + +// Local address books are not commited to GitHub so they might not exist +// require.resolve will throw an error if the file does not exist, so we hack it a bit +function resolveLocalAddressBook(path: string) { + const resolvedPath = require.resolve(path) + return resolvedPath.replace('addresses.json', 'addresses-local.json') +} + +type BaseHardhatConfig = HardhatUserConfig & + { etherscan: Partial } & + { graph: GraphRuntimeEnvironmentOptions } & + { secureAccounts: SecureAccountsOptions } +export const hardhatBaseConfig: BaseHardhatConfig = { + solidity: solidityUserConfig, + paths: projectPathsUserConfig, + secureAccounts: { + enabled: false, + }, + networks: networksUserConfig, + graph: { + deployments: { + horizon: require.resolve('@graphprotocol/horizon/addresses.json'), + subgraphService: require.resolve('@graphprotocol/subgraph-service/addresses.json'), + }, + }, + etherscan: etherscanUserConfig, +} + +export default hardhatBaseConfig diff --git a/packages/hardhat-graph-protocol/src/sdk/ignition/ignition.ts b/packages/hardhat-graph-protocol/src/sdk/ignition/ignition.ts new file mode 100644 index 000000000..4b2d7501f --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/ignition/ignition.ts @@ -0,0 +1,116 @@ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +require('json5/lib/register') + +import fs from 'fs' +import path from 'path' + +import type { AddressBook } from '../address-book' + +export function loadConfig(configPath: string, prefix: string, configName: string): any { + const configFileCandidates = [ + path.resolve(process.cwd(), configPath, `${prefix}.${configName}.json5`), + path.resolve(process.cwd(), configPath, `${prefix}.default.json5`), + ] + + const configFile = configFileCandidates.find(file => fs.existsSync(file)) + if (!configFile) { + throw new Error( + `Config file not found. Tried:\n${configFileCandidates.map(f => `- ${f}`).join('\n')}`, + ) + } + + return { config: removeNFromBigInts(require(configFile)), file: configFile } +} + +export function patchConfig(jsonData: any, patches: Record) { + function recursivePatch(obj: any, patchObj: any) { + if (typeof obj === 'object' && obj !== null && typeof patchObj === 'object' && patchObj !== null) { + for (const key in patchObj) { + if (obj.hasOwnProperty(key) && typeof obj[key] === 'object' && typeof patchObj[key] === 'object') { + // Both are objects, recursively merge + recursivePatch(obj[key], patchObj[key]) + } else { + // Either not an object or new key, directly assign + obj[key] = patchObj[key] + } + } + } + return obj + } + + return recursivePatch(jsonData, patches) +} + +export function saveToAddressBook( + contracts: any, + chainId: number | undefined, + addressBook: AddressBook, +): AddressBook { + if (!chainId) { + throw new Error('Chain ID is required') + } + + // Extract contract names and addresses + for (const [ignitionContractName, contract] of Object.entries(contracts)) { + // Proxy contracts + if (ignitionContractName.includes('_Proxy_')) { + const contractName = ignitionContractName.replace(/(Transparent_Proxy_|Graph_Proxy_)/, '') as ContractName + const proxy = ignitionContractName.includes('Transparent_Proxy_') ? 'transparent' : 'graph' + const entry = addressBook.entryExists(contractName) ? addressBook.getEntry(contractName) : {} + addressBook.setEntry(contractName, { + ...entry, + address: (contract as any).target, + proxy, + }) + } + + // Proxy admin contracts + if (ignitionContractName.includes('_ProxyAdmin_')) { + const contractName = ignitionContractName.replace(/(Transparent_ProxyAdmin_|Graph_ProxyAdmin_)/, '') as ContractName + const proxy = ignitionContractName.includes('Transparent_ProxyAdmin_') ? 'transparent' : 'graph' + const entry = addressBook.entryExists(contractName) ? addressBook.getEntry(contractName) : {} + addressBook.setEntry(contractName, { + ...entry, + proxy, + proxyAdmin: (contract as any).target, + }) + } + + // Implementation contracts + if (ignitionContractName.startsWith('Implementation_')) { + const contractName = ignitionContractName.replace('Implementation_', '') as ContractName + const entry = addressBook.entryExists(contractName) ? addressBook.getEntry(contractName) : {} + addressBook.setEntry(contractName, { + ...entry, + implementation: (contract as any).target, + }) + } + + // Non proxied contracts + if (addressBook.isContractName(ignitionContractName)) { + const entry = addressBook.entryExists(ignitionContractName) ? addressBook.getEntry(ignitionContractName) : {} + addressBook.setEntry(ignitionContractName, { + ...entry, + address: (contract as any).target, + }) + } + } + + return addressBook +} + +// Ignition requires "n" suffix for bigints, but not here +function removeNFromBigInts(obj: any): any { + if (typeof obj === 'string') { + return obj.replace(/(\d+)n/g, '$1') + } else if (Array.isArray(obj)) { + return obj.map(removeNFromBigInts) + } else if (typeof obj === 'object' && obj !== null) { + for (const key in obj) { + obj[key] = removeNFromBigInts(obj[key]) + } + } + return obj +} diff --git a/packages/hardhat-graph-protocol/src/sdk/index.ts b/packages/hardhat-graph-protocol/src/sdk/index.ts new file mode 100644 index 000000000..4bdc82681 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/index.ts @@ -0,0 +1,5 @@ +import { loadConfig, patchConfig, saveToAddressBook } from './ignition/ignition' +import { hardhatBaseConfig } from './hardhat.base.config' + +const IgnitionHelper = { saveToAddressBook, loadConfig, patchConfig } +export { hardhatBaseConfig, IgnitionHelper } diff --git a/packages/hardhat-graph-protocol/src/sdk/lib/artifact.ts b/packages/hardhat-graph-protocol/src/sdk/lib/artifact.ts new file mode 100644 index 000000000..eff3fc601 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/lib/artifact.ts @@ -0,0 +1,41 @@ +import { Artifacts } from 'hardhat/internal/artifacts' +import { logError } from '../../logger' + +import type { Artifact } from 'hardhat/types' + +/** + * Load a contract's artifact from the build output folder + * If multiple build output folders are provided, they will be searched in order + * @param name Name of the contract + * @param buildDir Path to the build output folder(s). Defaults to `build/contracts`. + * @returns The artifact corresponding to the contract name + */ +export const loadArtifact = (name: string, buildDir?: string[] | string): Artifact => { + let artifacts: Artifacts + let artifact: Artifact | undefined + buildDir = buildDir ?? ['build/contracts'] + + if (typeof buildDir === 'string') { + buildDir = [buildDir] + } + + for (const dir of buildDir) { + try { + artifacts = new Artifacts(dir) + artifact = artifacts.readArtifactSync(name) + break + } catch (error) { + if (error instanceof Error) { + logError(`Could not load artifact ${name} from ${dir} - ${error.message}`) + } else { + logError(`Could not load artifact ${name} from ${dir}`) + } + } + } + + if (artifact === undefined) { + throw new Error(`Could not load artifact ${name}`) + } + + return artifact +} diff --git a/packages/hardhat-graph-protocol/src/sdk/lib/contract.ts b/packages/hardhat-graph-protocol/src/sdk/lib/contract.ts new file mode 100644 index 000000000..827237756 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/lib/contract.ts @@ -0,0 +1,39 @@ +import { Contract, Provider, Signer } from 'ethers' +import { loadArtifact } from './artifact' + +export type ContractList = Partial> + +/** + * Loads a contract from an address book + * + * @param name Name of the contract + * @param addressBook Address book to use + * @param signerOrProvider Signer or provider to use + * @param enableTxLogging Enable transaction logging to console and output file. Defaults to `true` + * @param optional If true, the contract is optional and will not throw if it cannot be loaded + * @returns the loaded contract + * + * @throws Error if the contract could not be loaded + */ +export function loadContract( + name: ContractName, + address: string, + artifactsPath: string | string[], + signerOrProvider?: Signer | Provider, +): Contract { + try { + let contract = new Contract(address, loadArtifact(name, artifactsPath).abi, signerOrProvider) + + if (signerOrProvider) { + contract = contract.connect(signerOrProvider) as Contract + } + + return contract + } catch (err: unknown) { + if (err instanceof Error) { + throw new Error(`Could not load contract ${name} - ${err.message}`) + } else { + throw new Error(`Could not load contract ${name}`) + } + } +} diff --git a/packages/hardhat-graph-protocol/src/sdk/utils/abi.ts b/packages/hardhat-graph-protocol/src/sdk/utils/abi.ts new file mode 100644 index 000000000..97b28973c --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/utils/abi.ts @@ -0,0 +1,8 @@ +export function mergeABIs(abi1: any[], abi2: any[]) { + for (const item of abi2) { + if (abi1.find((v) => v.name === item.name) === undefined) { + abi1.push(item) + } + } + return abi1 +} \ No newline at end of file diff --git a/packages/hardhat-graph-protocol/src/sdk/utils/assertion.ts b/packages/hardhat-graph-protocol/src/sdk/utils/assertion.ts new file mode 100644 index 000000000..f215a9497 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/utils/assertion.ts @@ -0,0 +1,11 @@ +import { AssertionError } from 'assert' + +export function assertObject( + value: unknown, + errorMessage?: string, +): asserts value is Record { + if (typeof value !== 'object' || value == null) + throw new AssertionError({ + message: errorMessage ?? 'Not an object', + }) +} diff --git a/packages/hardhat-graph-protocol/src/sdk/utils/error.ts b/packages/hardhat-graph-protocol/src/sdk/utils/error.ts new file mode 100644 index 000000000..09df0ac3d --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/utils/error.ts @@ -0,0 +1,9 @@ +import { HardhatPluginError } from 'hardhat/plugins' +import { logError } from '../../logger' + +export class GraphPluginError extends HardhatPluginError { + constructor(message: string) { + super('GraphRuntimeEnvironment', message) + logError(message) + } +} diff --git a/packages/hardhat-graph-protocol/src/sdk/utils/path.ts b/packages/hardhat-graph-protocol/src/sdk/utils/path.ts new file mode 100644 index 000000000..3fcf810d7 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/sdk/utils/path.ts @@ -0,0 +1,8 @@ +import path from 'path' + +export function normalizePath(_path: string, graphPath?: string): string { + if (!path.isAbsolute(_path) && graphPath !== undefined) { + _path = path.join(graphPath, _path) + } + return _path +} diff --git a/packages/hardhat-graph-protocol/src/type-extensions.ts b/packages/hardhat-graph-protocol/src/type-extensions.ts new file mode 100644 index 000000000..f42df8eaa --- /dev/null +++ b/packages/hardhat-graph-protocol/src/type-extensions.ts @@ -0,0 +1,44 @@ +// To extend one of Hardhat's types, you need to import the module where it has been defined, and redeclare it. +import 'hardhat/types/config' +import 'hardhat/types/runtime' +import type { GraphDeployments, GraphRuntimeEnvironment, GraphRuntimeEnvironmentOptions } from './types' + +declare module 'hardhat/types/runtime' { + interface HardhatRuntimeEnvironment { + graph: (opts?: GraphRuntimeEnvironmentOptions) => GraphRuntimeEnvironment + } +} + +declare module 'hardhat/types/config' { + interface HardhatConfig { + graph: GraphRuntimeEnvironmentOptions + } + + interface HardhatUserConfig { + graph: GraphRuntimeEnvironmentOptions + } + + interface HardhatNetworkConfig { + deployments?: GraphDeployments + } + + interface HardhatNetworkUserConfig { + deployments?: GraphDeployments + } + + interface HttpNetworkConfig { + deployments?: GraphDeployments + } + + interface HttpNetworkUserConfig { + deployments?: GraphDeployments + } + + interface ProjectPathsConfig { + graph?: string + } + + interface ProjectPathsUserConfig { + graph?: string + } +} diff --git a/packages/hardhat-graph-protocol/src/types.ts b/packages/hardhat-graph-protocol/src/types.ts new file mode 100644 index 000000000..b15045849 --- /dev/null +++ b/packages/hardhat-graph-protocol/src/types.ts @@ -0,0 +1,57 @@ +import { type GraphDeploymentRuntimeEnvironmentMap, GraphDeploymentsList } from './deployment-list' +import type { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider' + +export type GraphDeployment = (typeof GraphDeploymentsList)[number] + +export type GraphDeployments = { + [deployment in GraphDeployment]?: string | { + addressBook: string + } +} + +export type GraphRuntimeEnvironmentOptions = { + deployments?: { + [deployment in GraphDeployment]?: string | { + addressBook: string + } + } +} + +export type GraphRuntimeEnvironment = { + [deployment in keyof GraphDeploymentRuntimeEnvironmentMap]?: GraphDeploymentRuntimeEnvironmentMap[deployment] +} & { + provider: HardhatEthersProvider + chainId: number +} + +export function isGraphDeployment(deployment: unknown): deployment is GraphDeployment { + return ( + typeof deployment === 'string' + && GraphDeploymentsList.includes(deployment as GraphDeployment) + ) +} + +export function assertGraphRuntimeEnvironment( + obj: unknown, +): obj is GraphRuntimeEnvironment { + if (typeof obj !== 'object' || obj === null) return false + + const deployments = obj as Partial + + for (const deployment in deployments) { + const environment = deployments[deployment as keyof GraphDeploymentRuntimeEnvironmentMap] + if (!environment || typeof environment !== 'object') { + return false + } + } + + if (typeof (obj as GraphRuntimeEnvironment).provider !== 'object') { + return false + } + + if (typeof (obj as GraphRuntimeEnvironment).chainId !== 'function') { + return false + } + + return true +} diff --git a/packages/hardhat-graph-protocol/test/config.test.ts b/packages/hardhat-graph-protocol/test/config.test.ts new file mode 100644 index 000000000..3cd005c31 --- /dev/null +++ b/packages/hardhat-graph-protocol/test/config.test.ts @@ -0,0 +1,75 @@ +import path from 'path' + +import { expect } from 'chai' +import { getAddressBookPath } from '../src/config' +import { loadHardhatContext } from './helpers' + +describe('GRE init functions', function () { + // No address book - should throw + describe('getAddressBookPath', function () { + it('should throw if no address book is specified', function () { + this.hre = loadHardhatContext('default-config', 'mainnet') + expect(() => getAddressBookPath('horizon', this.hre, {})).to.throw('Must set a an addressBook path!') + }) + + it('should throw if address book doesn\'t exist', function () { + this.hre = loadHardhatContext('invalid-address-book', 'mainnet') + expect(() => getAddressBookPath('horizon', this.hre, {})).to.throw(/Address book not found: /) + }) + + // Address book via opts should be used + it('should use opts parameter if available', function () { + this.hre = loadHardhatContext('network-address-book', 'mainnet') + const addressBook = getAddressBookPath('horizon', this.hre, { + deployments: { + horizon: { + addressBook: 'addresses-opt.json', + }, + }, + }) + expect(path.basename(addressBook)).to.equal('addresses-opt.json') + }) + + it('should use opts parameter if available - shortcut syntax', function () { + this.hre = loadHardhatContext('network-address-book', 'mainnet') + const addressBook = getAddressBookPath('horizon', this.hre, { + deployments: { + horizon: 'addresses-opt.json', + }, + }) + expect(path.basename(addressBook)).to.equal('addresses-opt.json') + }) + + // Address book via network config should be used + it('should use HH network config', function () { + this.hre = loadHardhatContext('network-address-book', 'mainnet') + const addressBook = getAddressBookPath('horizon', this.hre, {}) + expect(path.basename(addressBook)).to.equal('addresses-network.json') + }) + + it('should use HH network config - shortcut syntax', function () { + this.hre = loadHardhatContext('network-address-book', 'mainnet') + if (this.hre.network.config.deployments) { + this.hre.network.config.deployments.horizon = 'addresses-network-short.json' + } + const addressBook = getAddressBookPath('horizon', this.hre, {}) + expect(path.basename(addressBook)).to.equal('addresses-network-short.json') + }) + + // Address book via global config should be used + it('should use HH global config', function () { + this.hre = loadHardhatContext('global-address-book', 'mainnet') + const addressBook = getAddressBookPath('horizon', this.hre, {}) + expect(path.basename(addressBook)).to.equal('addresses-global.json') + }) + + it('should use HH global config - shortcut syntax', function () { + this.hre = loadHardhatContext('global-address-book', 'mainnet') + if (this.hre.config.graph.deployments) { + this.hre.config.graph.deployments.horizon = 'addresses-global-short.json' + } + const addressBook = getAddressBookPath('horizon', this.hre, {}) + expect(path.basename(addressBook)).to.equal('addresses-global-short.json') + }) + }) +}) diff --git a/packages/hardhat-graph-protocol/test/fixtures/default-config/hardhat.config.ts b/packages/hardhat-graph-protocol/test/fixtures/default-config/hardhat.config.ts new file mode 100644 index 000000000..17de4a585 --- /dev/null +++ b/packages/hardhat-graph-protocol/test/fixtures/default-config/hardhat.config.ts @@ -0,0 +1,8 @@ +module.exports = { + networks: { + mainnet: { + chainId: 1, + url: `https://mainnet.infura.io/v3/123456`, + }, + }, +} diff --git a/packages/hardhat-graph-protocol/test/fixtures/files/addresses-arbsep.json b/packages/hardhat-graph-protocol/test/fixtures/files/addresses-arbsep.json new file mode 100644 index 000000000..15dc74b27 --- /dev/null +++ b/packages/hardhat-graph-protocol/test/fixtures/files/addresses-arbsep.json @@ -0,0 +1,226 @@ +{ + "421614": { + "HorizonStaking": { + "address": "0xFf2Ee30de92F276018642A59Fb7Be95b3F9088Af" + }, + "GraphPayments": { + "address": "0xf5B3661BbB8CD48571C7f41ba2D896a3589C9753" + }, + "PaymentsEscrow": { + "address": "0x09B985a2042848A08bA59060EaF0f07c6F5D4d54" + }, + "GraphTallyCollector": { + "address": "0xacC71844EF6beEF70106ABe6E51013189A1f3738" + }, + "GraphProxyAdmin": { + "address": "0x7474a6cc5fAeDEc620Db0fa8E4da6eD58477042C", + "creationCodeHash": "0x68b304ac6bce7380d5e0f6b14a122f628bffebcc75f8205cb60f0baf578b79c3", + "runtimeCodeHash": "0x8d9ba87a745cf82ab407ebabe6c1490197084d320efb6c246d94bcc80e804417", + "txHash": "0x71b6defab0d3d7b711b7f6769f20a8c85bc9686eb5939b2a86dfaf587fceab17" + }, + "Controller": { + "address": "0x9DB3ee191681f092607035d9BDA6e59FbEaCa695", + "creationCodeHash": "0x5bde9a87bc4e8dd24d41900f0a19321c1dc6d3373d51bba093b130bb5b80a677", + "runtimeCodeHash": "0x7f0479db1d60ecf6295d92ea2359ebdd223640795613558b0594680f5d4922c9", + "txHash": "0xf7b4faa14f9d29bb62dec73fd163d1253184233012bcadf7ae78af7995017f29" + }, + "EpochManager": { + "address": "0x88b3C7f37253bAA1A9b95feAd69bD5320585826D", + "initArgs": ["0x9DB3ee191681f092607035d9BDA6e59FbEaCa695", "554"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x587ea6f421a08ab3a390103f63daba0529901f2e662ca7f6fe575674a439fa79", + "proxy": true, + "implementation": { + "address": "0x646627fa39ec6f6E757Cb4189bC54c92FFBb71da", + "creationCodeHash": "0x9947bd0a1f46027123b8fb4aec8b11af540aea587eb79642475d57b4e347078f", + "runtimeCodeHash": "0xe45a27197726de0e3149014823794708edd432ee56ec8358554c0d2365674ca0", + "txHash": "0x82653a0bd83e0541379b920415af94e4be1d732dfab720e5dead839062781c07" + } + }, + "L2GraphToken": { + "address": "0xf8c05dCF59E8B28BFD5eed176C562bEbcfc7Ac04", + "initArgs": ["0xadE6B8EB69a49B56929C1d4F4b428d791861dB6f"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0xbb27939a4e4b5d92da8a10add4b7d0126e907da30b07b5f3d439f1c32a6c8e2c", + "proxy": true, + "implementation": { + "address": "0x4cf968bA38b43dd10be114daa7959C1b369479e5", + "creationCodeHash": "0x6c4146427aafa7375a569154be95c8c931bf83aab0315706dd78bdf79c889e4c", + "runtimeCodeHash": "0x004371d1d80011906953dcba17c648503fc94b94e1e0365c8d8c706ff91f93e9", + "txHash": "0x3fab5697addf0c0e16b8e2249f2b833c6f256e699b293d184089c96de8deaa44" + } + }, + "GraphCurationToken": { + "address": "0x00FBd5D46FFAc54862c1Dd27BE08924BB17f5CDa", + "creationCodeHash": "0x1ee42ee271cefe20c33c0de904501e618ac4b56debca67c634d0564cecea9ff2", + "runtimeCodeHash": "0x340e8f378c0117b300f3ec255bc5c3a273f9ab5bd2940fa8eb3b5065b21f86dc", + "txHash": "0x045d64dc3ebb7ae6c4976854ce0a797a04524d22a6ef5f526bfc27f744bc68e5" + }, + "ServiceRegistry": { + "address": "0x888541878CbDDEd880Cd58c728f1Af5C47343F86", + "initArgs": ["0x9DB3ee191681f092607035d9BDA6e59FbEaCa695"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0xdaa1228e8cd8569c1e5562b63d2fd89caf897ab67da05922636d3309b838e289", + "proxy": true, + "implementation": { + "address": "0x05E732280bf9F37054346Cb83f5Fd58C5B44F6A8", + "creationCodeHash": "0xec9cb879003a06609541ad87efd4bc5dfc8ea60e4e77cfa5ae2cb5208742e7bc", + "runtimeCodeHash": "0x5161b534164413a88d851832f9c9d1dd1bca32fe2bbb62bb35d112c1dc8b69ab", + "txHash": "0xe1fce867f5dd708e60518b7f257fdbcb28f460d1e3b82045b82d03e64345b210" + } + }, + "L2Curation": { + "address": "0xDe761f075200E75485F4358978FB4d1dC8644FD5", + "initArgs": [ + "0x9DB3ee191681f092607035d9BDA6e59FbEaCa695", + "0x00FBd5D46FFAc54862c1Dd27BE08924BB17f5CDa", + "10000", + "1" + ], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0xe9298239bcb3c386cf66e6dd493cf6e7cdd9771c65fa2225e0b34d17550d6805", + "proxy": true, + "implementation": { + "address": "0xd90022aB67920212D0F902F5c427DE82732DE136", + "creationCodeHash": "0x2287d9023bf7d91e688e1eb029eff7657ef3b87e37b5222b01fd50985d0928f9", + "runtimeCodeHash": "0xd799b2b74e9634d6b6ef15b5710409264bed04a60f9519b9d8f05ac183199d16", + "txHash": "0x436bcf91fed712dc8d54f449726b2078fb63cd770f90b492a9622efac5817762" + } + }, + "SubgraphNFTDescriptor": { + "address": "0x4032F7B6b27FfC9862106f826379DaB1716C71d7", + "creationCodeHash": "0xf16e8ff11d852eea165195ac9e0dfa00f98e48f6ce3c77c469c7df9bf195b651", + "runtimeCodeHash": "0x39583196f2bcb85789b6e64692d8c0aa56f001c46f0ca3d371abbba2c695860f", + "txHash": "0xb7e7aeeecc693f34f491b01c56950533119810a4e3e2642081efc127f11cb782" + }, + "SubgraphNFT": { + "address": "0xF21Df5BbA7EB9b54D8F60C560aFb9bA63e6aED1A", + "constructorArgs": ["0xadE6B8EB69a49B56929C1d4F4b428d791861dB6f"], + "creationCodeHash": "0xc1e58864302084de282dffe54c160e20dd96c6cfff45e00e6ebfc15e04136982", + "runtimeCodeHash": "0x7216e736a8a8754e88688fbf5c0c7e9caf35c55ecc3a0c5a597b951c56cf7458", + "txHash": "0x1309c1caea76f4014ba612de092cc746816119b1440d635d11b6bc7e361a32b0" + }, + "L2GNS": { + "address": "0x3133948342F35b8699d8F94aeE064AbB76eDe965", + "initArgs": [ + "0x9DB3ee191681f092607035d9BDA6e59FbEaCa695", + "0xF21Df5BbA7EB9b54D8F60C560aFb9bA63e6aED1A" + ], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x137140783a99a3e9a60048d607124626ca87e2b972e8cc05efb41ac87c3cbcc4", + "proxy": true, + "implementation": { + "address": "0x00CBF5024d454255577Bf2b0fB6A43328a6828c9", + "creationCodeHash": "0xd71f45e6c194920a26f90fcec96d8c3375f02c5aef8ad90c1be24e906ffe8342", + "runtimeCodeHash": "0x68ec24512fedb866d7ba7ba6c02160317d0ca34eaacd23bddcc62d2cbcd9869c", + "txHash": "0x54619944731edec530b7b0cd587f9c2faae332aa1671fe5e8d7e7e5c7e291a77" + } + }, + "StakingExtension": { + "address": "0x05709dd705A5674346B7206a2bC657C8bAb3301B", + "creationCodeHash": "0x7ae74140871330ecabb7040182dc8288c2c84693393a519230036f39c2281138", + "runtimeCodeHash": "0x4994aa74e9e29c36a8158af690a245ccd1cf4d955223e5fcb1ca62810b37ed57", + "txHash": "0xbe1ff9cb949a53209b778708265740dfa2a08a93cfce4c897a53989a5d93f8c1" + }, + "L2Staking": { + "address": "0x865365C425f3A593Ffe698D9c4E6707D14d51e08", + "initArgs": [ + "0x9DB3ee191681f092607035d9BDA6e59FbEaCa695", + "100000000000000000000000", + "6646", + "10000", + "100000", + "8", + "12", + "16", + "100,100,60,100", + "0x876fB4B13D7Ed146757D3664B7E962b36936001C" + ], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x326cf1f2849da4bb4d7e39f2783779e3c99fa48e4ee8ef004cfdd50c62e775df", + "proxy": true, + "implementation": { + "address": "0xD07dFD514dc1b57020e6C1F49e05c48d0658C99f", + "creationCodeHash": "0x6a763345e5f166ea4e73ce9a116a49c9fc0833d9ea235a86fa5a997e91cf09e5", + "runtimeCodeHash": "0xb4c31859ac132241f04c802d4add70a94c7f2c6eb9dfd4bf224048d249dbc7bc", + "txHash": "0x68b34eda64287b84582c8f005c4e96162252d36c9c5c9b84332336a7c2e3d6d3", + "libraries": { + "LibExponential": "0xd844116f6d79a280b117Bb6d9EBf4121D4e8B44b" + } + } + }, + "RewardsManager": { + "address": "0x1F49caE7669086c8ba53CC35d1E9f80176d67E79", + "initArgs": ["0x9DB3ee191681f092607035d9BDA6e59FbEaCa695"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0xd8765fb87e11e8d41951f9071188b888829022a889cf66fdc2357f1f9f15c8e2", + "proxy": true, + "implementation": { + "address": "0x714B54e5249C90414fecA240e2F5B618C243F0aE", + "creationCodeHash": "0x59c1680da2d19124daaf95fd66acc5eae68e6f46dfe2ff0b3ccb777daf9949b2", + "runtimeCodeHash": "0xe33080183ec49ab1ec8d78b80b90158f0f3ac6f2deedf6115a32a9d11d3e4d9b", + "txHash": "0x8192f6c0e63a9beede3b025878af6a49367564c8bc32cb11a64f5f1e8351c7cd" + } + }, + "DisputeManager": { + "address": "0x7C9B82717f9433932507dF6EdA93A9678b258698", + "initArgs": [ + "0x9DB3ee191681f092607035d9BDA6e59FbEaCa695", + "0xF89688d5d44d73cc4dE880857A3940487076e5A4", + "10000000000000000000000", + "500000", + "25000", + "25000" + ], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0xb3764f4b576b46ee8dc6cbf680cad650b3ba80aa93dc6cf099862cfe8efc8a68", + "proxy": true, + "implementation": { + "address": "0x887aC2f58D62Ac86d4E9aEc07c953991e3ca1bA3", + "creationCodeHash": "0xce4c47d94a33d69e03d607dd13a9ad1ed7fa730ef4a2308eb56ddd646ebaa0aa", + "runtimeCodeHash": "0x18d4a1659ccecede3d4d305ef1db4653d8f3dcbd4012f4e52200ae9f0c6c322c", + "txHash": "0x59d99afb9cefbb5c2275d9ac2d7230ac7f4a4cfb2440636408988a66075c032a" + } + }, + "AllocationExchange": { + "address": "0x9BD4FBDa981D628AbA16F261f810dD59E5bAf9eA", + "constructorArgs": [ + "0xf8c05dCF59E8B28BFD5eed176C562bEbcfc7Ac04", + "0x865365C425f3A593Ffe698D9c4E6707D14d51e08", + "0x72ee30d43Fb5A90B3FE983156C5d2fBE6F6d07B3", + "0x49D4CFC037430cA9355B422bAeA7E9391e1d3215" + ], + "creationCodeHash": "0x2963baeedb2d0f5a95fa41f6c89e48e5bf177ca439379fc6becd54870d330ab0", + "runtimeCodeHash": "0xd8b53b3f65b49198d35392e0fd11da229a40d15a96151bca2976cbbe36b909d5", + "txHash": "0xa1a9410662d43463c39802e887f33a1401ed0fc35bf22c5be275e62141eae442" + }, + "L2GraphTokenGateway": { + "address": "0xB24Ce0f8c18c4DdDa584A7EeC132F49C966813bb", + "initArgs": ["0x9DB3ee191681f092607035d9BDA6e59FbEaCa695"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x90949db305a73b85e7208aa6b8d03c5181945eedc3df38e90f215a0dec8b02ae", + "proxy": true, + "implementation": { + "address": "0x3C2eB5E561f70c0573E5f6c92358e988E32cb5eC", + "creationCodeHash": "0x90253be19d23d542b29e95e6faf52304fcff91b21edfdb5f79e165051740d1ab", + "runtimeCodeHash": "0x3a7fab6792b4dad58c7b59da19c5b65b3985d1be77024a9f86cb135965e9b462", + "txHash": "0x78ff2e39d5c33ddfb89b1dbee89bdbc24452843a051f860c94e4e9dd75ded9c3" + } + }, + "EthereumDIDRegistry": { + "address": "0xF5f4cA61481558709AFa94AdEDa7B5F180f4AD59", + "creationCodeHash": "0x20cd202f7991716a84c097da5fbd365fd27f7f35f241f82c529ad7aba18b814b", + "runtimeCodeHash": "0x5f396ffd54b6cd6b3faded0f366c5d7e148cc54743926061be2dfd12a75391de", + "txHash": "0x2cefbc169b8ae51c263d0298956d86a397b05f11f076b71c918551f63fe33784" + } + } +} diff --git a/packages/hardhat-graph-protocol/test/fixtures/files/addresses-global-short.json b/packages/hardhat-graph-protocol/test/fixtures/files/addresses-global-short.json new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hardhat-graph-protocol/test/fixtures/files/addresses-global.json b/packages/hardhat-graph-protocol/test/fixtures/files/addresses-global.json new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hardhat-graph-protocol/test/fixtures/files/addresses-hre.json b/packages/hardhat-graph-protocol/test/fixtures/files/addresses-hre.json new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hardhat-graph-protocol/test/fixtures/files/addresses-network-short.json b/packages/hardhat-graph-protocol/test/fixtures/files/addresses-network-short.json new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hardhat-graph-protocol/test/fixtures/files/addresses-network.json b/packages/hardhat-graph-protocol/test/fixtures/files/addresses-network.json new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hardhat-graph-protocol/test/fixtures/files/addresses-opt.json b/packages/hardhat-graph-protocol/test/fixtures/files/addresses-opt.json new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hardhat-graph-protocol/test/fixtures/global-address-book/hardhat.config.ts b/packages/hardhat-graph-protocol/test/fixtures/global-address-book/hardhat.config.ts new file mode 100644 index 000000000..1f7718d54 --- /dev/null +++ b/packages/hardhat-graph-protocol/test/fixtures/global-address-book/hardhat.config.ts @@ -0,0 +1,52 @@ +import '../../../src/index' + +import type { HardhatUserConfig } from 'hardhat/config' + +const config: HardhatUserConfig = { + paths: { + graph: '../files', + }, + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + 'hardhat': { + chainId: 1337, + accounts: { + mnemonic: 'pumpkin orient can short never warm truth legend cereal tourist craft skin', + }, + }, + 'mainnet': { + chainId: 1, + url: `https://mainnet.infura.io/v3/123456`, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + }, + 'goerli': { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + }, + 'localhost': { + chainId: 1337, + url: 'http://127.0.0.1:8545', + }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: 'http://127.0.0.1:8545', + }, + }, + graph: { + deployments: { + horizon: { + addressBook: 'addresses-global.json', + }, + }, + }, +} + +export default config diff --git a/packages/hardhat-graph-protocol/test/fixtures/invalid-address-book/hardhat.config.ts b/packages/hardhat-graph-protocol/test/fixtures/invalid-address-book/hardhat.config.ts new file mode 100644 index 000000000..e4569c04a --- /dev/null +++ b/packages/hardhat-graph-protocol/test/fixtures/invalid-address-book/hardhat.config.ts @@ -0,0 +1,52 @@ +import '../../../src/index' + +import type { HardhatUserConfig } from 'hardhat/config' + +const config: HardhatUserConfig = { + paths: { + graph: '../files', + }, + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + 'hardhat': { + chainId: 1337, + accounts: { + mnemonic: 'pumpkin orient can short never warm truth legend cereal tourist craft skin', + }, + }, + 'mainnet': { + chainId: 1, + url: `https://mainnet.infura.io/v3/123456`, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + }, + 'goerli': { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + }, + 'localhost': { + chainId: 1337, + url: 'http://127.0.0.1:8545', + }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: 'http://127.0.0.1:8545', + }, + }, + graph: { + deployments: { + horizon: { + addressBook: 'addresses-invalid.json', + }, + }, + }, +} + +export default config diff --git a/packages/hardhat-graph-protocol/test/fixtures/network-address-book/hardhat.config.ts b/packages/hardhat-graph-protocol/test/fixtures/network-address-book/hardhat.config.ts new file mode 100644 index 000000000..c75c71e4e --- /dev/null +++ b/packages/hardhat-graph-protocol/test/fixtures/network-address-book/hardhat.config.ts @@ -0,0 +1,57 @@ +import '../../../src/index' + +import type { HardhatUserConfig } from 'hardhat/config' + +const config: HardhatUserConfig = { + paths: { + graph: '../files', + }, + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + 'hardhat': { + chainId: 1337, + accounts: { + mnemonic: 'pumpkin orient can short never warm truth legend cereal tourist craft skin', + }, + }, + 'mainnet': { + chainId: 1, + url: `https://mainnet.infura.io/v3/123456`, + deployments: { + horizon: { + addressBook: 'addresses-network.json', + }, + }, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + }, + 'goerli': { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + }, + 'localhost': { + chainId: 1337, + url: 'http://127.0.0.1:8545', + }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: 'http://127.0.0.1:8545', + }, + }, + graph: { + deployments: { + horizon: { + addressBook: 'addresses-global.json', + }, + }, + }, +} + +export default config diff --git a/packages/hardhat-graph-protocol/test/fixtures/no-path-config/addresses.json b/packages/hardhat-graph-protocol/test/fixtures/no-path-config/addresses.json new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hardhat-graph-protocol/test/fixtures/no-path-config/hardhat.config.ts b/packages/hardhat-graph-protocol/test/fixtures/no-path-config/hardhat.config.ts new file mode 100644 index 000000000..c9199325a --- /dev/null +++ b/packages/hardhat-graph-protocol/test/fixtures/no-path-config/hardhat.config.ts @@ -0,0 +1,49 @@ +import '../../../src/index' + +import type { HardhatUserConfig } from 'hardhat/config' + +const config: HardhatUserConfig = { + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + 'hardhat': { + chainId: 1337, + accounts: { + mnemonic: 'pumpkin orient can short never warm truth legend cereal tourist craft skin', + }, + }, + 'mainnet': { + chainId: 1, + url: `https://mainnet.infura.io/v3/123456`, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + }, + 'goerli': { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + }, + 'localhost': { + chainId: 1337, + url: 'http://127.0.0.1:8545', + }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: 'http://127.0.0.1:8545', + }, + }, + graph: { + deployments: { + horizon: { + addressBook: 'addresses-hre.json', + }, + }, + }, +} + +export default config diff --git a/packages/hardhat-graph-protocol/test/fixtures/path-config/hardhat.config.ts b/packages/hardhat-graph-protocol/test/fixtures/path-config/hardhat.config.ts new file mode 100644 index 000000000..0d689a80e --- /dev/null +++ b/packages/hardhat-graph-protocol/test/fixtures/path-config/hardhat.config.ts @@ -0,0 +1,59 @@ +import '../../../src/index' + +import type { HardhatUserConfig } from 'hardhat/config' + +const config: HardhatUserConfig = { + paths: { + graph: '../files', + }, + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + 'hardhat': { + chainId: 1337, + accounts: { + mnemonic: 'pumpkin orient can short never warm truth legend cereal tourist craft skin', + }, + }, + 'mainnet': { + chainId: 1, + url: `https://mainnet.infura.io/v3/123456`, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + }, + 'goerli': { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + }, + 'arbitrumSepolia': { + chainId: 421614, + url: 'https://goerli-rollup.arbitrum.io/rpc', + deployments: { + horizon: 'addresses-arbsep.json', + }, + }, + 'localhost': { + chainId: 1337, + url: 'http://127.0.0.1:8545', + }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: 'http://127.0.0.1:8545', + }, + }, + graph: { + deployments: { + horizon: { + addressBook: 'addresses-hre.json', + }, + }, + }, +} + +export default config diff --git a/packages/hardhat-graph-protocol/test/gre.test.ts b/packages/hardhat-graph-protocol/test/gre.test.ts new file mode 100644 index 000000000..d1fbbd6cd --- /dev/null +++ b/packages/hardhat-graph-protocol/test/gre.test.ts @@ -0,0 +1,53 @@ +import path from 'path' + +import { assert, expect } from 'chai' +import { loadHardhatContext, useHardhatProject } from './helpers' +import { GraphHorizonAddressBook } from '../src/sdk/deployments/horizon' + +describe('GRE usage', function () { + describe('Project not using GRE', function () { + useHardhatProject('default-config', 'mainnet') + + it('should throw when accessing hre.graph()', function () { + expect(() => this.hre.graph()).to.throw() + }) + }) + + describe(`Project using GRE - graph path`, function () { + it('should add the graph path to the config', function () { + this.hre = loadHardhatContext('no-path-config', 'mainnet') + assert.equal( + this.hre.config.paths.graph, + path.join(__dirname, 'fixtures/no-path-config'), + ) + }) + + it('should add the graph path to the config from custom path', function () { + this.hre = loadHardhatContext('path-config', 'mainnet') + assert.equal( + this.hre.config.paths.graph, + path.join(__dirname, 'fixtures/files'), + ) + }) + }) + + describe(`Project using GRE - deployments`, function () { + useHardhatProject('path-config', 'arbitrumSepolia') + + it('should load Horizon deployment', function () { + const graph = this.hre.graph() + assert.isDefined(graph.horizon) + assert.isObject(graph.horizon) + + assert.isDefined(graph.horizon.contracts) + assert.isObject(graph.horizon.contracts) + + assert.isDefined(graph.horizon.addressBook) + assert.isObject(graph.horizon.addressBook) + assert.instanceOf( + graph.horizon.addressBook, + GraphHorizonAddressBook, + ) + }) + }) +}) diff --git a/packages/hardhat-graph-protocol/test/helpers.ts b/packages/hardhat-graph-protocol/test/helpers.ts new file mode 100644 index 000000000..1c7050e05 --- /dev/null +++ b/packages/hardhat-graph-protocol/test/helpers.ts @@ -0,0 +1,38 @@ +import path from 'path' + +import { resetHardhatContext as _resetHardhatContext } from 'hardhat/plugins-testing' +import type { HardhatRuntimeEnvironment } from 'hardhat/types' + +declare module 'mocha' { + interface Context { + hre: HardhatRuntimeEnvironment + } +} + +export function useHardhatProject(fixtureProjectName: string, network?: string): void { + beforeEach('Loading hardhat environment', function () { + this.hre = loadHardhatContext(fixtureProjectName, network) + }) + + afterEach('Resetting hardhat', function () { + resetHardhatContext() + }) +} + +export function loadHardhatContext(fixtureProjectName: string, network?: string): HardhatRuntimeEnvironment { + resetHardhatContext() + delete process.env.HARDHAT_NETWORK + + process.chdir(path.join(__dirname, 'fixtures', fixtureProjectName)) + + if (network !== undefined) { + process.env.HARDHAT_NETWORK = network + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return require('hardhat') +} + +export function resetHardhatContext(): void { + _resetHardhatContext() + delete process.env.HARDHAT_NETWORK +} diff --git a/packages/hardhat-graph-protocol/tsconfig.json b/packages/hardhat-graph-protocol/tsconfig.json new file mode 100644 index 000000000..41ba7e8bc --- /dev/null +++ b/packages/hardhat-graph-protocol/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist", + }, + "include": ["eslint.config.js", "src/**/*.ts", "test/**/*.ts"], +} diff --git a/packages/horizon/.solhintignore b/packages/horizon/.solhintignore new file mode 100644 index 000000000..a0367e1af --- /dev/null +++ b/packages/horizon/.solhintignore @@ -0,0 +1 @@ +contracts/mocks/* \ No newline at end of file diff --git a/packages/horizon/README.md b/packages/horizon/README.md new file mode 100644 index 000000000..0adc20818 --- /dev/null +++ b/packages/horizon/README.md @@ -0,0 +1,52 @@ +# 🌅 Graph Horizon 🌅 + +Graph Horizon is the next evolution of the Graph Protocol. + +## Configuration + +The following environment variables might be required: + +| Variable | Description | +|----------|-------------| +| `ARBISCAN_API_KEY` | Arbiscan API key - for contract verification| +| `ARBITRUM_ONE_RPC` | Arbitrum One RPC URL - defaults to `https://arb1.arbitrum.io/rpc` | +| `ARBITRUM_SEPOLIA_RPC` | Arbitrum Sepolia RPC URL - defaults to `https://sepolia-rollup.arbitrum.io/rpc` | +| `LOCALHOST_RPC` | Localhost RPC URL - defaults to `http://localhost:8545` | +| `LOCALHOST_CHAIN_ID` | Localhost chain ID - defaults to `31337` | +| `LOCALHOST_ACCOUNTS_MNEMONIC` | Localhost accounts mnemonic - no default value. Note that setting this will override any secure accounts configuration. | + +You can set them using Hardhat: + +```bash +npx hardhat vars set +``` + +## Build + +```bash +yarn install +yarn build +``` + +## Deployment + +Note that this instructions will help you deploy Graph Horizon contracts, but no data service will be deployed. If you want to deploy the Subgraph Service please refer to the [Subgraph Service README](../subgraph-service/README.md) for deploy instructions. + +### New deployment +To deploy Graph Horizon from scratch run the following command: + +```bash +npx hardhat deploy:protocol --network hardhat +``` + +### Upgrade deployment +Usually you would run this against a network (or a fork) where the original Graph Protocol was previously deployed. To upgrade an existing deployment of the original Graph Protocol to Graph Horizon, run the following commands. Note that some steps might need to be run by different accounts (deployer vs governor): + +```bash +npx hardhat deploy:migrate --network hardhat --step 1 +npx hardhat deploy:migrate --network hardhat --step 2 # Run with governor. Optionally add --patch-config +npx hardhat deploy:migrate --network hardhat --step 3 # Optionally add --patch-config +npx hardhat deploy:migrate --network hardhat --step 4 # Run with governor. Optionally add --patch-config +``` + +Steps 2, 3 and 4 require patching the configuration file with addresses from previous steps. The files are located in the `ignition/configs` directory and need to be manually edited. You can also pass `--patch-config` flag to the deploy command to automatically patch the configuration reading values from the address book. Note that this will NOT update the configuration file. \ No newline at end of file diff --git a/packages/horizon/addresses.json b/packages/horizon/addresses.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/packages/horizon/addresses.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/horizon/contracts/data-service/DataService.sol b/packages/horizon/contracts/data-service/DataService.sol new file mode 100644 index 000000000..fc72e54ab --- /dev/null +++ b/packages/horizon/contracts/data-service/DataService.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataService } from "./interfaces/IDataService.sol"; + +import { DataServiceV1Storage } from "./DataServiceStorage.sol"; +import { GraphDirectory } from "../utilities/GraphDirectory.sol"; +import { ProvisionManager } from "./utilities/ProvisionManager.sol"; + +/** + * @title DataService contract + * @dev Implementation of the {IDataService} interface. + * @notice This implementation provides base functionality for a data service: + * - GraphDirectory, allows the data service to interact with Graph Horizon contracts + * - ProvisionManager, provides functionality to manage provisions + * + * The derived contract MUST implement all the interfaces described in {IDataService} and in + * accordance with the Data Service framework. + * @dev A note on upgradeability: this base contract can be inherited by upgradeable or non upgradeable + * contracts. + * - If the data service implementation is upgradeable, it must initialize the contract via an external + * initializer function with the `initializer` modifier that calls {__DataService_init} or + * {__DataService_init_unchained}. It's recommended the implementation constructor to also call + * {_disableInitializers} to prevent the implementation from being initialized. + * - If the data service implementation is NOT upgradeable, it must initialize the contract by calling + * {__DataService_init} or {__DataService_init_unchained} in the constructor. Note that the `initializer` + * will be required in the constructor. + * - Note that in both cases if using {__DataService_init_unchained} variant the corresponding parent + * initializers must be called in the implementation. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract DataService is GraphDirectory, ProvisionManager, DataServiceV1Storage, IDataService { + /** + * @dev Addresses in GraphDirectory are immutables, they can only be set in this constructor. + * @param controller The address of the Graph Horizon controller contract. + */ + constructor(address controller) GraphDirectory(controller) {} + + /// @inheritdoc IDataService + function getThawingPeriodRange() external view returns (uint64, uint64) { + return _getThawingPeriodRange(); + } + + /// @inheritdoc IDataService + function getVerifierCutRange() external view returns (uint32, uint32) { + return _getVerifierCutRange(); + } + + /// @inheritdoc IDataService + function getProvisionTokensRange() external view returns (uint256, uint256) { + return _getProvisionTokensRange(); + } + + /// @inheritdoc IDataService + function getDelegationRatio() external view returns (uint32) { + return _getDelegationRatio(); + } + + /** + * @notice Initializes the contract and any parent contracts. + */ + function __DataService_init() internal onlyInitializing { + __ProvisionManager_init_unchained(); + __DataService_init_unchained(); + } + + /** + * @notice Initializes the contract. + */ + function __DataService_init_unchained() internal onlyInitializing {} +} diff --git a/packages/horizon/contracts/data-service/DataServiceStorage.sol b/packages/horizon/contracts/data-service/DataServiceStorage.sol new file mode 100644 index 000000000..df759b892 --- /dev/null +++ b/packages/horizon/contracts/data-service/DataServiceStorage.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title DataServiceStorage + * @dev This contract holds the storage variables for the DataService contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract DataServiceV1Storage { + /// @dev Gap to allow adding variables in future upgrades + /// Note that this contract is not upgradeable but might be inherited by an upgradeable contract + uint256[50] private __gap; +} diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol new file mode 100644 index 000000000..875752df1 --- /dev/null +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataServiceFees } from "../interfaces/IDataServiceFees.sol"; + +import { ProvisionTracker } from "../libraries/ProvisionTracker.sol"; +import { LinkedList } from "../../libraries/LinkedList.sol"; + +import { DataService } from "../DataService.sol"; +import { DataServiceFeesV1Storage } from "./DataServiceFeesStorage.sol"; + +/** + * @title DataServiceFees contract + * @dev Implementation of the {IDataServiceFees} interface. + * @notice Extension for the {IDataService} contract to handle payment collateralization + * using a Horizon provision. See {IDataServiceFees} for more details. + * @dev This contract inherits from {DataService} which needs to be initialized, please see + * {DataService} for detailed instructions. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDataServiceFees { + using ProvisionTracker for mapping(address => uint256); + using LinkedList for LinkedList.List; + + /// @inheritdoc IDataServiceFees + function releaseStake(uint256 numClaimsToRelease) external virtual override { + _releaseStake(msg.sender, numClaimsToRelease); + } + + /** + * @notice Locks stake for a service provider to back a payment. + * Creates a stake claim, which is stored in a linked list by service provider. + * @dev Requirements: + * - The associated provision must have enough available tokens to lock the stake. + * + * Emits a {StakeClaimLocked} event. + * + * @param _serviceProvider The address of the service provider + * @param _tokens The amount of tokens to lock in the claim + * @param _unlockTimestamp The timestamp when the tokens can be released + */ + function _lockStake(address _serviceProvider, uint256 _tokens, uint256 _unlockTimestamp) internal { + require(_tokens != 0, DataServiceFeesZeroTokens()); + feesProvisionTracker.lock(_graphStaking(), _serviceProvider, _tokens, _delegationRatio); + + LinkedList.List storage claimsList = claimsLists[_serviceProvider]; + + // Save item and add to list + bytes32 claimId = _buildStakeClaimId(_serviceProvider, claimsList.nonce); + claims[claimId] = StakeClaim({ + tokens: _tokens, + createdAt: block.timestamp, + releasableAt: _unlockTimestamp, + nextClaim: bytes32(0) + }); + if (claimsList.count != 0) claims[claimsList.tail].nextClaim = claimId; + claimsList.addTail(claimId); + + emit StakeClaimLocked(_serviceProvider, claimId, _tokens, _unlockTimestamp); + } + + /** + * @notice Releases expired stake claims for a service provider. + * @dev This function can be overriden and/or disabled. + * @dev Emits a {StakeClaimsReleased} event, and a {StakeClaimReleased} event for each claim released. + * @param _serviceProvider The address of the service provider + * @param _numClaimsToRelease Amount of stake claims to process. If 0, all stake claims are processed. + */ + function _releaseStake(address _serviceProvider, uint256 _numClaimsToRelease) internal { + LinkedList.List storage claimsList = claimsLists[_serviceProvider]; + (uint256 claimsReleased, bytes memory data) = claimsList.traverse( + _getNextStakeClaim, + _processStakeClaim, + _deleteStakeClaim, + abi.encode(0, _serviceProvider), + _numClaimsToRelease + ); + + emit StakeClaimsReleased(_serviceProvider, claimsReleased, abi.decode(data, (uint256))); + } + + /** + * @notice Processes a stake claim, releasing the tokens if the claim has expired. + * @dev This function is used as a callback in the stake claims linked list traversal. + * @param _claimId The id of the stake claim + * @param _acc The accumulator for the stake claims being processed + * @return Wether the stake claim is still locked, indicating that the traversal should continue or stop. + * @return The updated accumulator data + */ + function _processStakeClaim(bytes32 _claimId, bytes memory _acc) private returns (bool, bytes memory) { + StakeClaim memory claim = _getStakeClaim(_claimId); + + // early exit + if (claim.releasableAt > block.timestamp) { + return (true, LinkedList.NULL_BYTES); + } + + // decode + (uint256 tokensClaimed, address serviceProvider) = abi.decode(_acc, (uint256, address)); + + // process + feesProvisionTracker.release(serviceProvider, claim.tokens); + emit StakeClaimReleased(serviceProvider, _claimId, claim.tokens, claim.releasableAt); + + // encode + _acc = abi.encode(tokensClaimed + claim.tokens, serviceProvider); + return (false, _acc); + } + + /** + * @notice Deletes a stake claim. + * @dev This function is used as a callback in the stake claims linked list traversal. + * @param _claimId The ID of the stake claim to delete + */ + function _deleteStakeClaim(bytes32 _claimId) private { + delete claims[_claimId]; + } + + /** + * @notice Gets the details of a stake claim + * @param _claimId The ID of the stake claim + * @return The stake claim details + */ + function _getStakeClaim(bytes32 _claimId) private view returns (StakeClaim memory) { + StakeClaim memory claim = claims[_claimId]; + require(claim.createdAt != 0, DataServiceFeesClaimNotFound(_claimId)); + return claim; + } + + /** + * @notice Gets the next stake claim in the linked list + * @dev This function is used as a callback in the stake claims linked list traversal. + * @param _claimId The ID of the stake claim + * @return The next stake claim ID + */ + function _getNextStakeClaim(bytes32 _claimId) private view returns (bytes32) { + return claims[_claimId].nextClaim; + } + + /** + * @notice Builds a stake claim ID + * @param _serviceProvider The address of the service provider + * @param _nonce A nonce of the stake claim + * @return The stake claim ID + */ + function _buildStakeClaimId(address _serviceProvider, uint256 _nonce) private view returns (bytes32) { + return keccak256(abi.encodePacked(address(this), _serviceProvider, _nonce)); + } +} diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol new file mode 100644 index 000000000..03d9d55e5 --- /dev/null +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataServiceFees } from "../interfaces/IDataServiceFees.sol"; + +import { LinkedList } from "../../libraries/LinkedList.sol"; + +/** + * @title Storage layout for the {DataServiceFees} extension contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract DataServiceFeesV1Storage { + mapping(address serviceProvider => uint256 tokens) public feesProvisionTracker; + + /// @notice List of all locked stake claims to be released to service providers + mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) public claims; + + /// @notice Service providers registered in the data service + mapping(address serviceProvider => LinkedList.List list) public claimsLists; + + /// @dev Gap to allow adding variables in future upgrades + /// Note that this contract is not upgradeable but might be inherited by an upgradeable contract + uint256[50] private __gap; +} diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol new file mode 100644 index 000000000..3a42bc8f4 --- /dev/null +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataServicePausable } from "../interfaces/IDataServicePausable.sol"; + +import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; +import { DataService } from "../DataService.sol"; + +/** + * @title DataServicePausable contract + * @dev Implementation of the {IDataServicePausable} interface. + * @notice Extension for the {IDataService} contract, adds pausing functionality + * to the data service. Pausing is controlled by privileged accounts called + * pause guardians. + * @dev Note that this extension does not provide an external function to set pause + * guardians. This should be implemented in the derived contract. + * @dev This contract inherits from {DataService} which needs to be initialized, please see + * {DataService} for detailed instructions. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract DataServicePausable is Pausable, DataService, IDataServicePausable { + /// @notice List of pause guardians and their allowed status + mapping(address pauseGuardian => bool allowed) public pauseGuardians; + + /** + * @notice Checks if the caller is a pause guardian. + */ + modifier onlyPauseGuardian() { + require(pauseGuardians[msg.sender], DataServicePausableNotPauseGuardian(msg.sender)); + _; + } + + /// @inheritdoc IDataServicePausable + function pause() external override onlyPauseGuardian whenNotPaused { + _pause(); + } + + /// @inheritdoc IDataServicePausable + function unpause() external override onlyPauseGuardian whenPaused { + _unpause(); + } + + /** + * @notice Sets a pause guardian. + * @dev Internal function to be used by the derived contract to set pause guardians. + * @param _pauseGuardian The address of the pause guardian + * @param _allowed The allowed status of the pause guardian + */ + function _setPauseGuardian(address _pauseGuardian, bool _allowed) internal { + require( + pauseGuardians[_pauseGuardian] == !_allowed, + DataServicePausablePauseGuardianNoChange(_pauseGuardian, _allowed) + ); + pauseGuardians[_pauseGuardian] = _allowed; + emit PauseGuardianSet(_pauseGuardian, _allowed); + } +} diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol new file mode 100644 index 000000000..6946a0091 --- /dev/null +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataServicePausable } from "../interfaces/IDataServicePausable.sol"; + +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { DataService } from "../DataService.sol"; + +/** + * @title DataServicePausableUpgradeable contract + * @dev Implementation of the {IDataServicePausable} interface. + * @dev Upgradeable version of the {DataServicePausable} contract. + * @dev This contract inherits from {DataService} which needs to be initialized, please see + * {DataService} for detailed instructions. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract DataServicePausableUpgradeable is PausableUpgradeable, DataService, IDataServicePausable { + /// @notice List of pause guardians and their allowed status + mapping(address pauseGuardian => bool allowed) public pauseGuardians; + + /// @dev Gap to allow adding variables in future upgrades + uint256[50] private __gap; + + /** + * @notice Checks if the caller is a pause guardian. + */ + modifier onlyPauseGuardian() { + require(pauseGuardians[msg.sender], DataServicePausableNotPauseGuardian(msg.sender)); + _; + } + + /// @inheritdoc IDataServicePausable + function pause() external override onlyPauseGuardian whenNotPaused { + _pause(); + } + + /// @inheritdoc IDataServicePausable + function unpause() external override onlyPauseGuardian whenPaused { + _unpause(); + } + + /** + * @notice Initializes the contract and parent contracts + */ + function __DataServicePausable_init() internal onlyInitializing { + __Pausable_init_unchained(); + __DataServicePausable_init_unchained(); + } + + /** + * @notice Initializes the contract + */ + function __DataServicePausable_init_unchained() internal onlyInitializing {} + + /** + * @notice Sets a pause guardian. + * @dev Internal function to be used by the derived contract to set pause guardians. + * + * Emits a {PauseGuardianSet} event. + * + * @param _pauseGuardian The address of the pause guardian + * @param _allowed The allowed status of the pause guardian + */ + function _setPauseGuardian(address _pauseGuardian, bool _allowed) internal { + require( + pauseGuardians[_pauseGuardian] == !_allowed, + DataServicePausablePauseGuardianNoChange(_pauseGuardian, _allowed) + ); + pauseGuardians[_pauseGuardian] = _allowed; + emit PauseGuardianSet(_pauseGuardian, _allowed); + } +} diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol b/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol new file mode 100644 index 000000000..13ef7d4df --- /dev/null +++ b/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IDataServiceRescuable } from "../interfaces/IDataServiceRescuable.sol"; + +import { DataService } from "../DataService.sol"; + +import { Denominations } from "../../libraries/Denominations.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title Rescuable contract + * @dev Allows a contract to have a function to rescue tokens sent by mistake. + * The contract must implement the external rescueTokens function or similar, + * that calls this contract's _rescueTokens. + * @dev Note that this extension does not provide an external function to set + * rescuers. This should be implemented in the derived contract. + * @dev This contract inherits from {DataService} which needs to be initialized, please see + * {DataService} for detailed instructions. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract DataServiceRescuable is DataService, IDataServiceRescuable { + /// @notice List of rescuers and their allowed status + mapping(address rescuer => bool allowed) public rescuers; + + /// @dev Gap to allow adding variables in future upgrades + /// Note that this contract is not upgradeable but might be inherited by an upgradeable contract + uint256[50] private __gap; + + /** + * @notice Checks if the caller is a rescuer. + */ + modifier onlyRescuer() { + require(rescuers[msg.sender], DataServiceRescuableNotRescuer(msg.sender)); + _; + } + + /// @inheritdoc IDataServiceRescuable + function rescueGRT(address to, uint256 tokens) external virtual onlyRescuer { + _rescueTokens(to, address(_graphToken()), tokens); + } + + /// @inheritdoc IDataServiceRescuable + function rescueETH(address payable to, uint256 tokens) external virtual onlyRescuer { + _rescueTokens(to, Denominations.NATIVE_TOKEN, tokens); + } + + /** + * @notice Sets a rescuer. + * @dev Internal function to be used by the derived contract to set rescuers. + * + * Emits a {RescuerSet} event. + * + * @param _rescuer Address of the rescuer + * @param _allowed Allowed status of the rescuer + */ + function _setRescuer(address _rescuer, bool _allowed) internal { + rescuers[_rescuer] = _allowed; + emit RescuerSet(_rescuer, _allowed); + } + + /** + * @dev Allows rescuing tokens sent to this contract + * @param _to Destination address to send the tokens + * @param _token Address of the token being rescued + * @param _tokens Amount of tokens to pull + */ + function _rescueTokens(address _to, address _token, uint256 _tokens) internal { + require(_tokens != 0, DataServiceRescuableCannotRescueZero()); + + if (Denominations.isNativeToken(_token)) Address.sendValue(payable(_to), _tokens); + else SafeERC20.safeTransfer(IERC20(_token), _to, _tokens); + + emit TokensRescued(msg.sender, _to, _token, _tokens); + } +} diff --git a/packages/horizon/contracts/data-service/interfaces/IDataService.sol b/packages/horizon/contracts/data-service/interfaces/IDataService.sol new file mode 100644 index 000000000..017d90b80 --- /dev/null +++ b/packages/horizon/contracts/data-service/interfaces/IDataService.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IGraphPayments } from "../../interfaces/IGraphPayments.sol"; + +/** + * @title Interface of the base {DataService} contract as defined by the Graph Horizon specification. + * @notice This interface provides a guardrail for contracts that use the Data Service framework + * to implement a data service on Graph Horizon. Much of the specification is intentionally loose + * to allow for greater flexibility when designing a data service. It's not possible to guarantee that + * an implementation will honor the Data Service framework guidelines so it's advised to always review + * the implementation code and the documentation. + * @dev This interface is expected to be inherited and extended by a data service interface. It can be + * used to interact with it however it's advised to use the more specific parent interface. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IDataService { + /** + * @notice Emitted when a service provider is registered with the data service. + * @param serviceProvider The address of the service provider. + * @param data Custom data, usage defined by the data service. + */ + event ServiceProviderRegistered(address indexed serviceProvider, bytes data); + + /** + * @notice Emitted when a service provider accepts a provision in {Graph Horizon staking contract}. + * @param serviceProvider The address of the service provider. + */ + event ProvisionPendingParametersAccepted(address indexed serviceProvider); + + /** + * @notice Emitted when a service provider starts providing the service. + * @param serviceProvider The address of the service provider. + * @param data Custom data, usage defined by the data service. + */ + event ServiceStarted(address indexed serviceProvider, bytes data); + + /** + * @notice Emitted when a service provider stops providing the service. + * @param serviceProvider The address of the service provider. + * @param data Custom data, usage defined by the data service. + */ + event ServiceStopped(address indexed serviceProvider, bytes data); + + /** + * @notice Emitted when a service provider collects payment. + * @param serviceProvider The address of the service provider. + * @param feeType The type of fee to collect as defined in {GraphPayments}. + * @param tokens The amount of tokens collected. + */ + event ServicePaymentCollected( + address indexed serviceProvider, + IGraphPayments.PaymentTypes indexed feeType, + uint256 tokens + ); + + /** + * @notice Emitted when a service provider is slashed. + * @param serviceProvider The address of the service provider. + * @param tokens The amount of tokens slashed. + */ + event ServiceProviderSlashed(address indexed serviceProvider, uint256 tokens); + + /** + * @notice Registers a service provider with the data service. The service provider can now + * start providing the service. + * @dev Before registering, the service provider must have created a provision in the + * Graph Horizon staking contract with parameters that are compatible with the data service. + * + * Verifies provision parameters and rejects registration in the event they are not valid. + * + * Emits a {ServiceProviderRegistered} event. + * + * NOTE: Failing to accept the provision will result in the service provider operating + * on an unverified provision. Depending on of the data service this can be a security + * risk as the protocol won't be able to guarantee economic security for the consumer. + * @param serviceProvider The address of the service provider. + * @param data Custom data, usage defined by the data service. + */ + function register(address serviceProvider, bytes calldata data) external; + + /** + * @notice Accepts pending parameters in the provision of a service provider in the {Graph Horizon staking + * contract}. + * @dev Provides a way for the data service to validate and accept provision parameter changes. Call {_acceptProvision}. + * + * Emits a {ProvisionPendingParametersAccepted} event. + * + * @param serviceProvider The address of the service provider. + * @param data Custom data, usage defined by the data service. + */ + function acceptProvisionPendingParameters(address serviceProvider, bytes calldata data) external; + + /** + * @notice Service provider starts providing the service. + * @dev Emits a {ServiceStarted} event. + * @param serviceProvider The address of the service provider. + * @param data Custom data, usage defined by the data service. + */ + function startService(address serviceProvider, bytes calldata data) external; + + /** + * @notice Service provider stops providing the service. + * @dev Emits a {ServiceStopped} event. + * @param serviceProvider The address of the service provider. + * @param data Custom data, usage defined by the data service. + */ + function stopService(address serviceProvider, bytes calldata data) external; + + /** + * @notice Collects payment earnt by the service provider. + * @dev The implementation of this function is expected to interact with {GraphPayments} + * to collect payment from the service payer, which is done via {IGraphPayments-collect}. + * + * Emits a {ServicePaymentCollected} event. + * + * NOTE: Data services that are vetted by the Graph Council might qualify for a portion of + * protocol issuance to cover for these payments. In this case, the funds are taken by + * interacting with the rewards manager contract instead of the {GraphPayments} contract. + * @param serviceProvider The address of the service provider. + * @param feeType The type of fee to collect as defined in {GraphPayments}. + * @param data Custom data, usage defined by the data service. + * @return The amount of tokens collected. + */ + function collect( + address serviceProvider, + IGraphPayments.PaymentTypes feeType, + bytes calldata data + ) external returns (uint256); + + /** + * @notice Slash a service provider for misbehaviour. + * @dev To slash the service provider's provision the function should call + * {Staking-slash}. + * + * Emits a {ServiceProviderSlashed} event. + * + * @param serviceProvider The address of the service provider. + * @param data Custom data, usage defined by the data service. + */ + function slash(address serviceProvider, bytes calldata data) external; + + /** + * @notice External getter for the thawing period range + * @return Minimum thawing period allowed + * @return Maximum thawing period allowed + */ + function getThawingPeriodRange() external view returns (uint64, uint64); + + /** + * @notice External getter for the verifier cut range + * @return Minimum verifier cut allowed + * @return Maximum verifier cut allowed + */ + function getVerifierCutRange() external view returns (uint32, uint32); + + /** + * @notice External getter for the provision tokens range + * @return Minimum provision tokens allowed + * @return Maximum provision tokens allowed + */ + function getProvisionTokensRange() external view returns (uint256, uint256); + + /** + * @notice External getter for the delegation ratio + * @return The delegation ratio + */ + function getDelegationRatio() external view returns (uint32); +} diff --git a/packages/horizon/contracts/data-service/interfaces/IDataServiceFees.sol b/packages/horizon/contracts/data-service/interfaces/IDataServiceFees.sol new file mode 100644 index 000000000..9d235f4f7 --- /dev/null +++ b/packages/horizon/contracts/data-service/interfaces/IDataServiceFees.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataService } from "./IDataService.sol"; + +/** + * @title Interface for the {DataServiceFees} contract. + * @notice Extension for the {IDataService} contract to handle payment collateralization + * using a Horizon provision. + * + * It's designed to be used with the Data Service framework: + * - When a service provider collects payment with {IDataService.collect} the data service should lock + * stake to back the payment using {_lockStake}. + * - Every time there is a payment collection with {IDataService.collect}, the data service should + * attempt to release any expired stake claims by calling {_releaseStake}. + * - Stake claims can also be manually released by calling {releaseStake} directly. + * + * @dev Note that this implementation uses the entire provisioned stake as collateral for the payment. + * It can be used to provide economic security for the payments collected as long as the provisioned + * stake is not being used for other purposes. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IDataServiceFees is IDataService { + /** + * @notice A stake claim, representing provisioned stake that gets locked + * to be released to a service provider. + * @dev StakeClaims are stored in linked lists by service provider, ordered by + * creation timestamp. + * @param tokens The amount of tokens to be locked in the claim + * @param createdAt The timestamp when the claim was created + * @param releasableAt The timestamp when the tokens can be released + * @param nextClaim The next claim in the linked list + */ + struct StakeClaim { + uint256 tokens; + uint256 createdAt; + uint256 releasableAt; + bytes32 nextClaim; + } + + /** + * @notice Emitted when a stake claim is created and stake is locked. + * @param serviceProvider The address of the service provider + * @param claimId The id of the stake claim + * @param tokens The amount of tokens to lock in the claim + * @param unlockTimestamp The timestamp when the tokens can be released + */ + event StakeClaimLocked( + address indexed serviceProvider, + bytes32 indexed claimId, + uint256 tokens, + uint256 unlockTimestamp + ); + + /** + * @notice Emitted when a stake claim is released and stake is unlocked. + * @param serviceProvider The address of the service provider + * @param claimId The id of the stake claim + * @param tokens The amount of tokens released + * @param releasableAt The timestamp when the tokens were released + */ + event StakeClaimReleased( + address indexed serviceProvider, + bytes32 indexed claimId, + uint256 tokens, + uint256 releasableAt + ); + + /** + * @notice Emitted when a series of stake claims are released. + * @param serviceProvider The address of the service provider + * @param claimsCount The number of stake claims being released + * @param tokensReleased The total amount of tokens being released + */ + event StakeClaimsReleased(address indexed serviceProvider, uint256 claimsCount, uint256 tokensReleased); + + /** + * @notice Thrown when attempting to get a stake claim that does not exist. + * @param claimId The id of the stake claim + */ + error DataServiceFeesClaimNotFound(bytes32 claimId); + + /** + * @notice Emitted when trying to lock zero tokens in a stake claim + */ + error DataServiceFeesZeroTokens(); + + /** + * @notice Releases expired stake claims for the caller. + * @dev This function is only meant to be called if the service provider has enough + * stake claims that releasing them all at once would exceed the block gas limit. + * @dev This function can be overriden and/or disabled. + * @dev Emits a {StakeClaimsReleased} event, and a {StakeClaimReleased} event for each claim released. + * @param numClaimsToRelease Amount of stake claims to process. If 0, all stake claims are processed. + */ + function releaseStake(uint256 numClaimsToRelease) external; +} diff --git a/packages/horizon/contracts/data-service/interfaces/IDataServicePausable.sol b/packages/horizon/contracts/data-service/interfaces/IDataServicePausable.sol new file mode 100644 index 000000000..906e864a8 --- /dev/null +++ b/packages/horizon/contracts/data-service/interfaces/IDataServicePausable.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataService } from "./IDataService.sol"; + +/** + * @title Interface for the {DataServicePausable} contract. + * @notice Extension for the {IDataService} contract, adds pausing functionality + * to the data service. Pausing is controlled by privileged accounts called + * pause guardians. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IDataServicePausable is IDataService { + /** + * @notice Emitted when a pause guardian is set. + * @param account The address of the pause guardian + * @param allowed The allowed status of the pause guardian + */ + event PauseGuardianSet(address indexed account, bool allowed); + + /** + * @notice Emitted when a the caller is not a pause guardian + * @param account The address of the pause guardian + */ + error DataServicePausableNotPauseGuardian(address account); + + /** + * @notice Emitted when a pause guardian is set to the same allowed status + * @param account The address of the pause guardian + * @param allowed The allowed status of the pause guardian + */ + error DataServicePausablePauseGuardianNoChange(address account, bool allowed); + + /** + * @notice Pauses the data service. + * @dev Note that only functions using the modifiers `whenNotPaused` + * and `whenPaused` will be affected by the pause. + * + * Requirements: + * - The contract must not be already paused + */ + function pause() external; + + /** + * @notice Unpauses the data service. + * @dev Note that only functions using the modifiers `whenNotPaused` + * and `whenPaused` will be affected by the pause. + * + * Requirements: + * - The contract must be paused + */ + function unpause() external; +} diff --git a/packages/horizon/contracts/data-service/interfaces/IDataServiceRescuable.sol b/packages/horizon/contracts/data-service/interfaces/IDataServiceRescuable.sol new file mode 100644 index 000000000..f2cd7b06e --- /dev/null +++ b/packages/horizon/contracts/data-service/interfaces/IDataServiceRescuable.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataService } from "./IDataService.sol"; + +/** + * @title Interface for the {IDataServicePausable} contract. + * @notice Extension for the {IDataService} contract, adds the ability to rescue + * any ERC20 token or ETH from the contract, controlled by a rescuer privileged role. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IDataServiceRescuable is IDataService { + /** + * @notice Emitted when tokens are rescued from the contract. + * @param from The address initiating the rescue + * @param to The address receiving the rescued tokens + * @param token The address of the token being rescued + * @param tokens The amount of tokens rescued + */ + event TokensRescued(address indexed from, address indexed to, address indexed token, uint256 tokens); + + /** + * @notice Emitted when a rescuer is set. + * @param account The address of the rescuer + * @param allowed Whether the rescuer is allowed to rescue tokens + */ + event RescuerSet(address indexed account, bool allowed); + + /** + * @notice Thrown when trying to rescue zero tokens. + */ + error DataServiceRescuableCannotRescueZero(); + + /** + * @notice Thrown when the caller is not a rescuer. + * @param account The address of the account that attempted the rescue + */ + error DataServiceRescuableNotRescuer(address account); + + /** + * @notice Rescues GRT tokens from the contract. + * @dev Declared as virtual to allow disabling the function via override. + * + * Requirements: + * - Cannot rescue zero tokens. + * + * Emits a {TokensRescued} event. + * + * @param to Address of the tokens recipient. + * @param tokens Amount of tokens to rescue. + */ + function rescueGRT(address to, uint256 tokens) external; + + /** + * @notice Rescues ether from the contract. + * @dev Declared as virtual to allow disabling the function via override. + * + * Requirements: + * - Cannot rescue zeroether. + * + * Emits a {TokensRescued} event. + * + * @param to Address of the tokens recipient. + * @param tokens Amount of tokens to rescue. + */ + function rescueETH(address payable to, uint256 tokens) external; +} diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol new file mode 100644 index 000000000..2fe271833 --- /dev/null +++ b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IHorizonStaking } from "../../interfaces/IHorizonStaking.sol"; + +/** + * @title ProvisionTracker library + * @notice A library to facilitate tracking of "used tokens" on Graph Horizon provisions. This can be used to + * ensure data services have enough economic security (provisioned stake) to back the payments they collect for + * their services. + * The library provides two primitives, lock and release to signal token usage and free up tokens respectively. It + * does not make any assumptions about the conditions under which tokens are locked or released. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library ProvisionTracker { + /** + * @notice Thrown when trying to lock more tokens than available + * @param tokensAvailable The amount of tokens available + * @param tokensRequired The amount of tokens required + */ + error ProvisionTrackerInsufficientTokens(uint256 tokensAvailable, uint256 tokensRequired); + + /** + * @notice Locks tokens for a service provider + * @dev Requirements: + * - `tokens` must be less than or equal to the amount of tokens available, as reported by the HorizonStaking contract + * @param self The provision tracker mapping + * @param graphStaking The HorizonStaking contract + * @param serviceProvider The service provider address + * @param tokens The amount of tokens to lock + * @param delegationRatio A delegation ratio to limit the amount of delegation that's usable + */ + function lock( + mapping(address => uint256) storage self, + IHorizonStaking graphStaking, + address serviceProvider, + uint256 tokens, + uint32 delegationRatio + ) internal { + if (tokens == 0) return; + + uint256 tokensRequired = self[serviceProvider] + tokens; + uint256 tokensAvailable = graphStaking.getTokensAvailable(serviceProvider, address(this), delegationRatio); + require(tokensRequired <= tokensAvailable, ProvisionTrackerInsufficientTokens(tokensAvailable, tokensRequired)); + self[serviceProvider] += tokens; + } + + /** + * @notice Releases tokens for a service provider + * @dev Requirements: + * - `tokens` must be less than or equal to the amount of tokens locked for the service provider + * @param self The provision tracker mapping + * @param serviceProvider The service provider address + * @param tokens The amount of tokens to release + */ + function release(mapping(address => uint256) storage self, address serviceProvider, uint256 tokens) internal { + if (tokens == 0) return; + require(self[serviceProvider] >= tokens, ProvisionTrackerInsufficientTokens(self[serviceProvider], tokens)); + self[serviceProvider] -= tokens; + } + + /** + * @notice Checks if a service provider has enough tokens available to lock + * @param self The provision tracker mapping + * @param graphStaking The HorizonStaking contract + * @param serviceProvider The service provider address + * @param delegationRatio A delegation ratio to limit the amount of delegation that's usable + * @return true if the service provider has enough tokens available to lock, false otherwise + */ + function check( + mapping(address => uint256) storage self, + IHorizonStaking graphStaking, + address serviceProvider, + uint32 delegationRatio + ) internal view returns (bool) { + uint256 tokensAvailable = graphStaking.getTokensAvailable(serviceProvider, address(this), delegationRatio); + return self[serviceProvider] <= tokensAvailable; + } +} diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol new file mode 100644 index 000000000..3ead88a09 --- /dev/null +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IHorizonStaking } from "../../interfaces/IHorizonStaking.sol"; + +import { UintRange } from "../../libraries/UintRange.sol"; +import { PPMMath } from "../../libraries/PPMMath.sol"; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; +import { ProvisionManagerV1Storage } from "./ProvisionManagerStorage.sol"; + +/** + * @title ProvisionManager contract + * @notice A helper contract that implements several provision management functions. + * @dev Provides utilities to verify provision parameters are within an acceptable range. Each + * parameter has an overridable setter and getter for the validity range, and a checker that reverts + * if the parameter is out of range. + * The parameters are: + * - Provision parameters (thawing period and verifier cut) + * - Provision tokens + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionManagerV1Storage { + using UintRange for uint256; + + /// @notice The default minimum verifier cut. + uint32 internal constant DEFAULT_MIN_VERIFIER_CUT = type(uint32).min; + + /// @notice The default maximum verifier cut. + uint32 internal constant DEFAULT_MAX_VERIFIER_CUT = uint32(PPMMath.MAX_PPM); + + /// @notice The default minimum thawing period. + uint64 internal constant DEFAULT_MIN_THAWING_PERIOD = type(uint64).min; + + /// @notice The default maximum thawing period. + uint64 internal constant DEFAULT_MAX_THAWING_PERIOD = type(uint64).max; + + /// @notice The default minimum provision tokens. + uint256 internal constant DEFAULT_MIN_PROVISION_TOKENS = type(uint256).min; + + /// @notice The default maximum provision tokens. + uint256 internal constant DEFAULT_MAX_PROVISION_TOKENS = type(uint256).max; + + /** + * @notice Emitted when the provision tokens range is set. + * @param min The minimum allowed value for the provision tokens. + * @param max The maximum allowed value for the provision tokens. + */ + event ProvisionTokensRangeSet(uint256 min, uint256 max); + + /** + * @notice Emitted when the delegation ratio is set. + * @param ratio The delegation ratio + */ + event DelegationRatioSet(uint32 ratio); + + /** + * @notice Emitted when the verifier cut range is set. + * @param min The minimum allowed value for the max verifier cut. + * @param max The maximum allowed value for the max verifier cut. + */ + event VerifierCutRangeSet(uint32 min, uint32 max); + + /** + * @notice Emitted when the thawing period range is set. + * @param min The minimum allowed value for the thawing period. + * @param max The maximum allowed value for the thawing period. + */ + event ThawingPeriodRangeSet(uint64 min, uint64 max); + + /** + * @notice Thrown when a provision parameter is out of range. + * @param message The error message. + * @param value The value that is out of range. + * @param min The minimum allowed value. + * @param max The maximum allowed value. + */ + error ProvisionManagerInvalidValue(bytes message, uint256 value, uint256 min, uint256 max); + + /** + * @notice Thrown when attempting to set a range where min is greater than max. + * @param min The minimum value. + * @param max The maximum value. + */ + error ProvisionManagerInvalidRange(uint256 min, uint256 max); + + /** + * @notice Thrown when the caller is not authorized to manage the provision of a service provider. + * @param serviceProvider The address of the serviceProvider. + * @param caller The address of the caller. + */ + error ProvisionManagerNotAuthorized(address serviceProvider, address caller); + + /** + * @notice Thrown when a provision is not found. + * @param serviceProvider The address of the service provider. + */ + error ProvisionManagerProvisionNotFound(address serviceProvider); + + /** + * @notice Checks if the caller is authorized to manage the provision of a service provider. + * @param serviceProvider The address of the service provider. + */ + modifier onlyAuthorizedForProvision(address serviceProvider) { + require( + _graphStaking().isAuthorized(serviceProvider, address(this), msg.sender), + ProvisionManagerNotAuthorized(serviceProvider, msg.sender) + ); + _; + } + + /** + * @notice Checks if a provision of a service provider is valid according + * to the parameter ranges established. + * @param serviceProvider The address of the service provider. + */ + modifier onlyValidProvision(address serviceProvider) virtual { + IHorizonStaking.Provision memory provision = _getProvision(serviceProvider); + _checkProvisionTokens(provision); + _checkProvisionParameters(provision, false); + _; + } + + /** + * @notice Initializes the contract and any parent contracts. + */ + function __ProvisionManager_init() internal onlyInitializing { + __ProvisionManager_init_unchained(); + } + + /** + * @notice Initializes the contract. + * @dev All parameters set to their entire range as valid. + */ + function __ProvisionManager_init_unchained() internal onlyInitializing { + _setProvisionTokensRange(DEFAULT_MIN_PROVISION_TOKENS, DEFAULT_MAX_PROVISION_TOKENS); + _setVerifierCutRange(DEFAULT_MIN_VERIFIER_CUT, DEFAULT_MAX_VERIFIER_CUT); + _setThawingPeriodRange(DEFAULT_MIN_THAWING_PERIOD, DEFAULT_MAX_THAWING_PERIOD); + } + + /** + * @notice Verifies and accepts the provision parameters of a service provider in + * the {HorizonStaking} contract. + * @dev Checks the pending provision parameters, not the current ones. + * + * Emits a {ProvisionPendingParametersAccepted} event. + * + * @param _serviceProvider The address of the service provider. + */ + function _acceptProvisionParameters(address _serviceProvider) internal { + _checkProvisionParameters(_serviceProvider, true); + _graphStaking().acceptProvisionParameters(_serviceProvider); + } + + // -- setters -- + /** + * @notice Sets the delegation ratio. + * @param _ratio The delegation ratio to be set + */ + function _setDelegationRatio(uint32 _ratio) internal { + _delegationRatio = _ratio; + emit DelegationRatioSet(_ratio); + } + + /** + * @notice Sets the range for the provision tokens. + * @param _min The minimum allowed value for the provision tokens. + * @param _max The maximum allowed value for the provision tokens. + */ + function _setProvisionTokensRange(uint256 _min, uint256 _max) internal { + require(_min <= _max, ProvisionManagerInvalidRange(_min, _max)); + _minimumProvisionTokens = _min; + _maximumProvisionTokens = _max; + emit ProvisionTokensRangeSet(_min, _max); + } + + /** + * @notice Sets the range for the verifier cut. + * @param _min The minimum allowed value for the max verifier cut. + * @param _max The maximum allowed value for the max verifier cut. + */ + function _setVerifierCutRange(uint32 _min, uint32 _max) internal { + require(_min <= _max, ProvisionManagerInvalidRange(_min, _max)); + require(PPMMath.isValidPPM(_max), ProvisionManagerInvalidRange(_min, _max)); + _minimumVerifierCut = _min; + _maximumVerifierCut = _max; + emit VerifierCutRangeSet(_min, _max); + } + + /** + * @notice Sets the range for the thawing period. + * @param _min The minimum allowed value for the thawing period. + * @param _max The maximum allowed value for the thawing period. + */ + function _setThawingPeriodRange(uint64 _min, uint64 _max) internal { + require(_min <= _max, ProvisionManagerInvalidRange(_min, _max)); + _minimumThawingPeriod = _min; + _maximumThawingPeriod = _max; + emit ThawingPeriodRangeSet(_min, _max); + } + + // -- checks -- + + /** + * @notice Checks if the provision tokens of a service provider are within the valid range. + * @param _serviceProvider The address of the service provider. + */ + function _checkProvisionTokens(address _serviceProvider) internal view virtual { + IHorizonStaking.Provision memory provision = _getProvision(_serviceProvider); + _checkProvisionTokens(provision); + } + + /** + * @notice Checks if the provision tokens of a service provider are within the valid range. + * Note that thawing tokens are not considered in this check. + * @param _provision The provision to check. + */ + function _checkProvisionTokens(IHorizonStaking.Provision memory _provision) internal view virtual { + _checkValueInRange( + _provision.tokens - _provision.tokensThawing, + _minimumProvisionTokens, + _maximumProvisionTokens, + "tokens" + ); + } + + /** + * @notice Checks if the provision parameters of a service provider are within the valid range. + * @param _serviceProvider The address of the service provider. + * @param _checkPending If true, checks the pending provision parameters. + */ + function _checkProvisionParameters(address _serviceProvider, bool _checkPending) internal view virtual { + IHorizonStaking.Provision memory provision = _getProvision(_serviceProvider); + _checkProvisionParameters(provision, _checkPending); + } + + /** + * @notice Checks if the provision parameters of a service provider are within the valid range. + * @param _provision The provision to check. + * @param _checkPending If true, checks the pending provision parameters instead of the current ones. + */ + function _checkProvisionParameters( + IHorizonStaking.Provision memory _provision, + bool _checkPending + ) internal view virtual { + (uint64 thawingPeriodMin, uint64 thawingPeriodMax) = _getThawingPeriodRange(); + uint64 thawingPeriodToCheck = _checkPending ? _provision.thawingPeriodPending : _provision.thawingPeriod; + _checkValueInRange(thawingPeriodToCheck, thawingPeriodMin, thawingPeriodMax, "thawingPeriod"); + + (uint32 verifierCutMin, uint32 verifierCutMax) = _getVerifierCutRange(); + uint32 maxVerifierCutToCheck = _checkPending ? _provision.maxVerifierCutPending : _provision.maxVerifierCut; + _checkValueInRange(maxVerifierCutToCheck, verifierCutMin, verifierCutMax, "maxVerifierCut"); + } + + // -- getters -- + + /** + * @notice Gets the delegation ratio. + * @return The delegation ratio + */ + function _getDelegationRatio() internal view returns (uint32) { + return _delegationRatio; + } + + /** + * @notice Gets the range for the provision tokens. + * @return The minimum allowed value for the provision tokens. + * @return The maximum allowed value for the provision tokens. + */ + function _getProvisionTokensRange() internal view virtual returns (uint256, uint256) { + return (_minimumProvisionTokens, _maximumProvisionTokens); + } + + /** + * @notice Gets the range for the thawing period. + * @return The minimum allowed value for the thawing period. + * @return The maximum allowed value for the thawing period. + */ + function _getThawingPeriodRange() internal view virtual returns (uint64, uint64) { + return (_minimumThawingPeriod, _maximumThawingPeriod); + } + + /** + * @notice Gets the range for the verifier cut. + * @return The minimum allowed value for the max verifier cut. + * @return The maximum allowed value for the max verifier cut. + */ + function _getVerifierCutRange() internal view virtual returns (uint32, uint32) { + return (_minimumVerifierCut, _maximumVerifierCut); + } + + /** + * @notice Gets a provision from the {HorizonStaking} contract. + * @dev Requirements: + * - The provision must exist. + * @param _serviceProvider The address of the service provider. + * @return The provision. + */ + function _getProvision(address _serviceProvider) internal view returns (IHorizonStaking.Provision memory) { + IHorizonStaking.Provision memory provision = _graphStaking().getProvision(_serviceProvider, address(this)); + require(provision.createdAt != 0, ProvisionManagerProvisionNotFound(_serviceProvider)); + return provision; + } + + /** + * @notice Checks if a value is within a valid range. + * @param _value The value to check. + * @param _min The minimum allowed value. + * @param _max The maximum allowed value. + * @param _revertMessage The revert message to display if the value is out of range. + */ + function _checkValueInRange(uint256 _value, uint256 _min, uint256 _max, bytes memory _revertMessage) private pure { + require(_value.isInRange(_min, _max), ProvisionManagerInvalidValue(_revertMessage, _value, _min, _max)); + } +} diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol new file mode 100644 index 000000000..5931c66e5 --- /dev/null +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title Storage layout for the {ProvisionManager} helper contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract ProvisionManagerV1Storage { + /// @notice The minimum amount of tokens required to register a provision in the data service + uint256 internal _minimumProvisionTokens; + + /// @notice The maximum amount of tokens allowed to register a provision in the data service + uint256 internal _maximumProvisionTokens; + + /// @notice The minimum thawing period required to register a provision in the data service + uint64 internal _minimumThawingPeriod; + + /// @notice The maximum thawing period allowed to register a provision in the data service + uint64 internal _maximumThawingPeriod; + + /// @notice The minimum verifier cut required to register a provision in the data service (in PPM) + uint32 internal _minimumVerifierCut; + + /// @notice The maximum verifier cut allowed to register a provision in the data service (in PPM) + uint32 internal _maximumVerifierCut; + + /// @notice How much delegation the service provider can effectively use + /// @dev Max calculated as service provider's stake * delegationRatio + uint32 internal _delegationRatio; + + /// @dev Gap to allow adding variables in future upgrades + /// Note that this contract is not upgradeable but might be inherited by an upgradeable contract + uint256[50] private __gap; +} diff --git a/packages/horizon/contracts/interfaces/IAuthorizable.sol b/packages/horizon/contracts/interfaces/IAuthorizable.sol new file mode 100644 index 000000000..1ba46341c --- /dev/null +++ b/packages/horizon/contracts/interfaces/IAuthorizable.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title Interface for the {Authorizable} contract + * @notice Implements an authorization scheme that allows authorizers to + * authorize signers to sign on their behalf. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IAuthorizable { + /** + * @notice Details for an authorizer-signer pair + * @dev Authorizations can be removed only after a thawing period + * @param authorizer The address of the authorizer - resource owner + * @param thawEndTimestamp The timestamp at which the thawing period ends (zero if not thawing) + * @param revoked Whether the signer authorization was revoked + */ + struct Authorization { + address authorizer; + uint256 thawEndTimestamp; + bool revoked; + } + + /** + * @notice Emitted when a signer is authorized to sign for a authorizer + * @param authorizer The address of the authorizer + * @param signer The address of the signer + */ + event SignerAuthorized(address indexed authorizer, address indexed signer); + + /** + * @notice Emitted when a signer is thawed to be de-authorized + * @param authorizer The address of the authorizer thawing the signer + * @param signer The address of the signer to thaw + * @param thawEndTimestamp The timestamp at which the thawing period ends + */ + event SignerThawing(address indexed authorizer, address indexed signer, uint256 thawEndTimestamp); + + /** + * @notice Emitted when the thawing of a signer is cancelled + * @param authorizer The address of the authorizer cancelling the thawing + * @param signer The address of the signer + * @param thawEndTimestamp The timestamp at which the thawing period was scheduled to end + */ + event SignerThawCanceled(address indexed authorizer, address indexed signer, uint256 thawEndTimestamp); + + /** + * @notice Emitted when a signer has been revoked after thawing + * @param authorizer The address of the authorizer revoking the signer + * @param signer The address of the signer + */ + event SignerRevoked(address indexed authorizer, address indexed signer); + + /** + * @notice Thrown when attempting to authorize a signer that is already authorized + * @param authorizer The address of the authorizer + * @param signer The address of the signer + * @param revoked The revoked status of the authorization + */ + error AuthorizableSignerAlreadyAuthorized(address authorizer, address signer, bool revoked); + + /** + * @notice Thrown when the signer proof deadline is invalid + * @param proofDeadline The deadline for the proof provided + * @param currentTimestamp The current timestamp + */ + error AuthorizableInvalidSignerProofDeadline(uint256 proofDeadline, uint256 currentTimestamp); + + /** + * @notice Thrown when the signer proof is invalid + */ + error AuthorizableInvalidSignerProof(); + + /** + * @notice Thrown when the signer is not authorized by the authorizer + * @param authorizer The address of the authorizer + * @param signer The address of the signer + */ + error AuthorizableSignerNotAuthorized(address authorizer, address signer); + + /** + * @notice Thrown when the signer is not thawing + * @param signer The address of the signer + */ + error AuthorizableSignerNotThawing(address signer); + + /** + * @notice Thrown when the signer is still thawing + * @param currentTimestamp The current timestamp + * @param thawEndTimestamp The timestamp at which the thawing period ends + */ + error AuthorizableSignerStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp); + + /** + * @notice Authorize a signer to sign on behalf of the authorizer + * @dev Requirements: + * - `signer` must not be already authorized + * - `proofDeadline` must be greater than the current timestamp + * - `proof` must be a valid signature from the signer being authorized + * + * Emits a {SignerAuthorized} event + * @param signer The addres of the signer + * @param proofDeadline The deadline for the proof provided by the signer + * @param proof The proof provided by the signer to be authorized by the authorizer + * consists of (chain id, verifying contract address, domain, proof deadline, authorizer address) + */ + function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external; + + /** + * @notice Starts thawing a signer to be de-authorized + * @dev Thawing a signer signals that signatures from that signer will soon be deemed invalid. + * Once a signer is thawed, they should be viewed as revoked regardless of their revocation status. + * If a signer is already thawing and this function is called, the thawing period is reset. + * Requirements: + * - `signer` must be authorized by the authorizer calling this function + * + * Emits a {SignerThawing} event + * @param signer The address of the signer to thaw + */ + function thawSigner(address signer) external; + + /** + * @notice Stops thawing a signer. + * @dev Requirements: + * - `signer` must be thawing and authorized by the function caller + * + * Emits a {SignerThawCanceled} event + * @param signer The address of the signer to cancel thawing + */ + function cancelThawSigner(address signer) external; + + /** + * @notice Revokes a signer if thawed. + * @dev Requirements: + * - `signer` must be thawed and authorized by the function caller + * + * Emits a {SignerRevoked} event + * @param signer The address of the signer + */ + function revokeAuthorizedSigner(address signer) external; + + /** + * @notice Returns the timestamp at which the thawing period ends for a signer + * @param signer The address of the signer + * @return The timestamp at which the thawing period ends + */ + function getThawEnd(address signer) external view returns (uint256); + + /** + * @notice Returns true if the signer is authorized by the authorizer + * @param authorizer The address of the authorizer + * @param signer The address of the signer + * @return true if the signer is authorized by the authorizer, false otherwise + */ + function isAuthorized(address authorizer, address signer) external view returns (bool); +} diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol new file mode 100644 index 000000000..8ca7464d1 --- /dev/null +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title Interface for the {GraphPayments} contract + * @notice This contract is part of the Graph Horizon payments protocol. It's designed + * to pull funds (GRT) from the {PaymentsEscrow} and distribute them according to a + * set of pre established rules. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IGraphPayments { + /** + * @notice Types of payments that are supported by the payments protocol + * @dev + */ + enum PaymentTypes { + QueryFee, + IndexingFee, + IndexingRewards + } + + /** + * @notice Emitted when a payment is collected + * @param paymentType The type of payment as defined in {IGraphPayments} + * @param payer The address of the payer + * @param receiver The address of the receiver + * @param dataService The address of the data service + * @param tokens The total amount of tokens being collected + * @param tokensProtocol Amount of tokens charged as protocol tax + * @param tokensDataService Amount of tokens for the data service + * @param tokensDelegationPool Amount of tokens for delegators + * @param tokensReceiver Amount of tokens for the receiver + */ + event GraphPaymentCollected( + PaymentTypes indexed paymentType, + address indexed payer, + address receiver, + address indexed dataService, + uint256 tokens, + uint256 tokensProtocol, + uint256 tokensDataService, + uint256 tokensDelegationPool, + uint256 tokensReceiver + ); + + /** + * @notice Thrown when the protocol payment cut is invalid + * @param protocolPaymentCut The protocol payment cut + */ + error GraphPaymentsInvalidProtocolPaymentCut(uint256 protocolPaymentCut); + + /** + * @notice Thrown when trying to use a cut that is not expressed in PPM + * @param cut The cut + */ + error GraphPaymentsInvalidCut(uint256 cut); + + /** + * @notice Initialize the contract + */ + function initialize() external; + + /** + * @notice Collects funds from a payer. + * It will pay cuts to all relevant parties and forward the rest to the receiver. + * Note that the collected amount can be zero. + * @param paymentType The type of payment as defined in {IGraphPayments} + * @param receiver The address of the receiver + * @param tokens The amount of tokens being collected. + * @param dataService The address of the data service + * @param dataServiceCut The data service cut in PPM + */ + function collect( + PaymentTypes paymentType, + address receiver, + uint256 tokens, + address dataService, + uint256 dataServiceCut + ) external; +} diff --git a/packages/horizon/contracts/interfaces/IGraphProxyAdmin.sol b/packages/horizon/contracts/interfaces/IGraphProxyAdmin.sol new file mode 100644 index 000000000..7ffe0487f --- /dev/null +++ b/packages/horizon/contracts/interfaces/IGraphProxyAdmin.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +/** + * @title IGraphProxyAdmin + * @dev Empty interface to allow the GraphProxyAdmin contract to be used + * in the GraphDirectory contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IGraphProxyAdmin {} diff --git a/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol b/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol new file mode 100644 index 000000000..8a51c7adb --- /dev/null +++ b/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IPaymentsCollector } from "./IPaymentsCollector.sol"; +import { IGraphPayments } from "./IGraphPayments.sol"; + +/** + * @title Interface for the {GraphTallyCollector} contract + * @dev Implements the {IPaymentCollector} interface as defined by the Graph + * Horizon payments protocol. + * @notice Implements a payments collector contract that can be used to collect + * payments using a GraphTally RAV (Receipt Aggregate Voucher). + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IGraphTallyCollector is IPaymentsCollector { + /** + * @notice The Receipt Aggregate Voucher (RAV) struct + * @param collectionId The ID of the collection "bucket" the RAV belongs to. Note that multiple RAVs can be collected for the same collection id. + * @param payer The address of the payer the RAV was issued by + * @param serviceProvider The address of the service provider the RAV was issued to + * @param dataService The address of the data service the RAV was issued to + * @param timestampNs The RAV timestamp, indicating the latest GraphTally Receipt in the RAV + * @param valueAggregate The total amount owed to the service provider since the beginning of the payer-service provider relationship, including all debt that is already paid for. + * @param metadata Arbitrary metadata to extend functionality if a data service requires it + */ + struct ReceiptAggregateVoucher { + bytes32 collectionId; + address payer; + address serviceProvider; + address dataService; + uint64 timestampNs; + uint128 valueAggregate; + bytes metadata; + } + + /** + * @notice A struct representing a signed RAV + * @param rav The RAV + * @param signature The signature of the RAV - 65 bytes: r (32 Bytes) || s (32 Bytes) || v (1 Byte) + */ + struct SignedRAV { + ReceiptAggregateVoucher rav; + bytes signature; + } + + /** + * @notice Emitted when a RAV is collected + * @param collectionId The ID of the collection "bucket" the RAV belongs to. + * @param payer The address of the payer + * @param dataService The address of the data service + * @param serviceProvider The address of the service provider + * @param timestampNs The timestamp of the RAV + * @param valueAggregate The total amount owed to the service provider + * @param metadata Arbitrary metadata + * @param signature The signature of the RAV + */ + event RAVCollected( + bytes32 indexed collectionId, + address indexed payer, + address serviceProvider, + address indexed dataService, + uint64 timestampNs, + uint128 valueAggregate, + bytes metadata, + bytes signature + ); + + /** + * @notice Thrown when the RAV signer is invalid + */ + error GraphTallyCollectorInvalidRAVSigner(); + + /** + * @notice Thrown when the RAV is for a data service the service provider has no provision for + * @param dataService The address of the data service + */ + error GraphTallyCollectorUnauthorizedDataService(address dataService); + + /** + * @notice Thrown when the caller is not the data service the RAV was issued to + * @param caller The address of the caller + * @param dataService The address of the data service + */ + error GraphTallyCollectorCallerNotDataService(address caller, address dataService); + + /** + * @notice Thrown when the tokens collected are inconsistent with the collection history + * Each RAV should have a value greater than the previous one + * @param tokens The amount of tokens in the RAV + * @param tokensCollected The amount of tokens already collected + */ + error GraphTallyCollectorInconsistentRAVTokens(uint256 tokens, uint256 tokensCollected); + + /** + * @notice Thrown when the attempting to collect more tokens than what it's owed + * @param tokensToCollect The amount of tokens to collect + * @param maxTokensToCollect The maximum amount of tokens to collect + */ + error GraphTallyCollectorInvalidTokensToCollectAmount(uint256 tokensToCollect, uint256 maxTokensToCollect); + + /** + * @notice See {IPaymentsCollector.collect} + * This variant adds the ability to partially collect a RAV by specifying the amount of tokens to collect. + * + * Requirements: + * - The amount of tokens to collect must be less than or equal to the total amount of tokens in the RAV minus + * the tokens already collected. + * @param paymentType The payment type to collect + * @param data Additional data required for the payment collection + * @param tokensToCollect The amount of tokens to collect + * @return The amount of tokens collected + */ + function collect( + IGraphPayments.PaymentTypes paymentType, + bytes calldata data, + uint256 tokensToCollect + ) external returns (uint256); + + /** + * @dev Recovers the signer address of a signed ReceiptAggregateVoucher (RAV). + * @param signedRAV The SignedRAV containing the RAV and its signature. + * @return The address of the signer. + */ + function recoverRAVSigner(SignedRAV calldata signedRAV) external view returns (address); + + /** + * @dev Computes the hash of a ReceiptAggregateVoucher (RAV). + * @param rav The RAV for which to compute the hash. + * @return The hash of the RAV. + */ + function encodeRAV(ReceiptAggregateVoucher calldata rav) external view returns (bytes32); +} diff --git a/packages/horizon/contracts/interfaces/IHorizonStaking.sol b/packages/horizon/contracts/interfaces/IHorizonStaking.sol new file mode 100644 index 000000000..0ba4e26b3 --- /dev/null +++ b/packages/horizon/contracts/interfaces/IHorizonStaking.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IHorizonStakingTypes } from "./internal/IHorizonStakingTypes.sol"; +import { IHorizonStakingMain } from "./internal/IHorizonStakingMain.sol"; +import { IHorizonStakingBase } from "./internal/IHorizonStakingBase.sol"; +import { IHorizonStakingExtension } from "./internal/IHorizonStakingExtension.sol"; + +/** + * @title Complete interface for the Horizon Staking contract + * @notice This interface exposes all functions implemented by the {HorizonStaking} contract and its extension + * {HorizonStakingExtension} as well as the custom data types used by the contract. + * @dev Use this interface to interact with the Horizon Staking contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IHorizonStaking is IHorizonStakingTypes, IHorizonStakingBase, IHorizonStakingMain, IHorizonStakingExtension {} diff --git a/packages/horizon/contracts/interfaces/IPaymentsCollector.sol b/packages/horizon/contracts/interfaces/IPaymentsCollector.sol new file mode 100644 index 000000000..d37688462 --- /dev/null +++ b/packages/horizon/contracts/interfaces/IPaymentsCollector.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IGraphPayments } from "./IGraphPayments.sol"; + +/** + * @title Interface for a payments collector contract as defined by Graph Horizon payments protocol + * @notice Contracts implementing this interface can be used with the payments protocol. First, a payer must + * approve the collector to collect payments on their behalf. Only then can payment collection be initiated + * using the collector contract. + * + * @dev It's important to note that it's the collector contract's responsibility to validate the payment + * request is legitimate. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IPaymentsCollector { + /** + * @notice Emitted when a payment is collected + * @param paymentType The payment type collected as defined by {IGraphPayments} + * @param collectionId The id for the collection. Can be used at the discretion of the collector to group multiple payments. + * @param payer The address of the payer + * @param receiver The address of the receiver + * @param dataService The address of the data service + * @param tokens The amount of tokens being collected + */ + event PaymentCollected( + IGraphPayments.PaymentTypes paymentType, + bytes32 indexed collectionId, + address indexed payer, + address receiver, + address indexed dataService, + uint256 tokens + ); + + /** + * @notice Initiate a payment collection through the payments protocol + * @dev This function should require the caller to present some form of evidence of the payer's debt to + * the receiver. The collector should validate this evidence and, if valid, collect the payment. + * + * Emits a {PaymentCollected} event + * + * @param paymentType The payment type to collect, as defined by {IGraphPayments} + * @param data Additional data required for the payment collection. Will vary depending on the collector + * implementation. + * @return The amount of tokens collected + */ + function collect(IGraphPayments.PaymentTypes paymentType, bytes memory data) external returns (uint256); +} diff --git a/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol b/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol new file mode 100644 index 000000000..f26030487 --- /dev/null +++ b/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IGraphPayments } from "./IGraphPayments.sol"; + +/** + * @title Interface for the {PaymentsEscrow} contract + * @notice This contract is part of the Graph Horizon payments protocol. It holds the funds (GRT) + * for payments made through the payments protocol for services provided + * via a Graph Horizon data service. + * + * Payers deposit funds on the escrow, signalling their ability to pay for a service, and only + * being able to retrieve them after a thawing period. Receivers collect funds from the escrow, + * provided the payer has authorized them. The payer authorization is delegated to a payment + * collector contract which implements the {IPaymentsCollector} interface. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IPaymentsEscrow { + /** + * @notice Escrow account for a payer-collector-receiver tuple + * @param balance The total token balance for the payer-collector-receiver tuple + * @param tokensThawing The amount of tokens currently being thawed + * @param thawEndTimestamp The timestamp at which thawing period ends (zero if not thawing) + */ + struct EscrowAccount { + uint256 balance; + uint256 tokensThawing; + uint256 thawEndTimestamp; + } + + /** + * @notice Emitted when a payer deposits funds into the escrow for a payer-collector-receiver tuple + * @param payer The address of the payer + * @param collector The address of the collector + * @param receiver The address of the receiver + * @param tokens The amount of tokens deposited + */ + event Deposit(address indexed payer, address indexed collector, address indexed receiver, uint256 tokens); + + /** + * @notice Emitted when a payer cancels an escrow thawing + * @param payer The address of the payer + * @param collector The address of the collector + * @param receiver The address of the receiver + * @param tokensThawing The amount of tokens that were being thawed + * @param thawEndTimestamp The timestamp at which the thawing period was ending + */ + event CancelThaw( + address indexed payer, + address indexed collector, + address indexed receiver, + uint256 tokensThawing, + uint256 thawEndTimestamp + ); + + /** + * @notice Emitted when a payer thaws funds from the escrow for a payer-collector-receiver tuple + * @param payer The address of the payer + * @param collector The address of the collector + * @param receiver The address of the receiver + * @param tokens The amount of tokens being thawed + * @param thawEndTimestamp The timestamp at which the thawing period ends + */ + event Thaw( + address indexed payer, + address indexed collector, + address indexed receiver, + uint256 tokens, + uint256 thawEndTimestamp + ); + + /** + * @notice Emitted when a payer withdraws funds from the escrow for a payer-collector-receiver tuple + * @param payer The address of the payer + * @param collector The address of the collector + * @param receiver The address of the receiver + * @param tokens The amount of tokens withdrawn + */ + event Withdraw(address indexed payer, address indexed collector, address indexed receiver, uint256 tokens); + + /** + * @notice Emitted when a collector collects funds from the escrow for a payer-collector-receiver tuple + * @param paymentType The type of payment being collected as defined in the {IGraphPayments} interface + * @param payer The address of the payer + * @param collector The address of the collector + * @param receiver The address of the receiver + * @param tokens The amount of tokens collected + */ + event EscrowCollected( + IGraphPayments.PaymentTypes indexed paymentType, + address indexed payer, + address indexed collector, + address receiver, + uint256 tokens + ); + + // -- Errors -- + + /** + * @notice Thrown when a protected function is called and the contract is paused. + */ + error PaymentsEscrowIsPaused(); + + /** + * @notice Thrown when the available balance is insufficient to perform an operation + * @param balance The current balance + * @param minBalance The minimum required balance + */ + error PaymentsEscrowInsufficientBalance(uint256 balance, uint256 minBalance); + + /** + * @notice Thrown when a thawing is expected to be in progress but it is not + */ + error PaymentsEscrowNotThawing(); + + /** + * @notice Thrown when a thawing is still in progress + * @param currentTimestamp The current timestamp + * @param thawEndTimestamp The timestamp at which the thawing period ends + */ + error PaymentsEscrowStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp); + + /** + * @notice Thrown when setting the thawing period to a value greater than the maximum + * @param thawingPeriod The thawing period + * @param maxWaitPeriod The maximum wait period + */ + error PaymentsEscrowThawingPeriodTooLong(uint256 thawingPeriod, uint256 maxWaitPeriod); + + /** + * @notice Thrown when the contract balance is not consistent with the collection amount + * @param balanceBefore The balance before the collection + * @param balanceAfter The balance after the collection + * @param tokens The amount of tokens collected + */ + error PaymentsEscrowInconsistentCollection(uint256 balanceBefore, uint256 balanceAfter, uint256 tokens); + + /** + * @notice Thrown when operating a zero token amount is not allowed. + */ + error PaymentsEscrowInvalidZeroTokens(); + + /** + * @notice Initialize the contract + */ + function initialize() external; + + /** + * @notice Deposits funds into the escrow for a payer-collector-receiver tuple, where + * the payer is the transaction caller. + * @dev Emits a {Deposit} event + * @param collector The address of the collector + * @param receiver The address of the receiver + * @param tokens The amount of tokens to deposit + */ + function deposit(address collector, address receiver, uint256 tokens) external; + + /** + * @notice Deposits funds into the escrow for a payer-collector-receiver tuple, where + * the payer can be specified. + * @dev Emits a {Deposit} event + * @param payer The address of the payer + * @param collector The address of the collector + * @param receiver The address of the receiver + * @param tokens The amount of tokens to deposit + */ + function depositTo(address payer, address collector, address receiver, uint256 tokens) external; + + /** + * @notice Thaw a specific amount of escrow from a payer-collector-receiver's escrow account. + * The payer is the transaction caller. + * Note that repeated calls to this function will overwrite the previous thawing amount + * and reset the thawing period. + * @dev Requirements: + * - `tokens` must be less than or equal to the available balance + * + * Emits a {Thaw} event. + * + * @param collector The address of the collector + * @param receiver The address of the receiver + * @param tokens The amount of tokens to thaw + */ + function thaw(address collector, address receiver, uint256 tokens) external; + + /** + * @notice Cancels the thawing of escrow from a payer-collector-receiver's escrow account. + * @param collector The address of the collector + * @param receiver The address of the receiver + * @dev Requirements: + * - The payer must be thawing funds + * Emits a {CancelThaw} event. + */ + function cancelThaw(address collector, address receiver) external; + + /** + * @notice Withdraws all thawed escrow from a payer-collector-receiver's escrow account. + * The payer is the transaction caller. + * Note that the withdrawn funds might be less than the thawed amount if there were + * payment collections in the meantime. + * @dev Requirements: + * - Funds must be thawed + * + * Emits a {Withdraw} event + * + * @param collector The address of the collector + * @param receiver The address of the receiver + */ + function withdraw(address collector, address receiver) external; + + /** + * @notice Collects funds from the payer-collector-receiver's escrow and sends them to {GraphPayments} for + * distribution using the Graph Horizon Payments protocol. + * The function will revert if there are not enough funds in the escrow. + * + * Emits an {EscrowCollected} event + * + * @param paymentType The type of payment being collected as defined in the {IGraphPayments} interface + * @param payer The address of the payer + * @param receiver The address of the receiver + * @param tokens The amount of tokens to collect + * @param dataService The address of the data service + * @param dataServiceCut The data service cut in PPM that {GraphPayments} should send + */ + function collect( + IGraphPayments.PaymentTypes paymentType, + address payer, + address receiver, + uint256 tokens, + address dataService, + uint256 dataServiceCut + ) external; + + /** + * @notice Get the balance of a payer-collector-receiver tuple + * This function will return 0 if the current balance is less than the amount of funds being thawed. + * @param payer The address of the payer + * @param collector The address of the collector + * @param receiver The address of the receiver + * @return The balance of the payer-collector-receiver tuple + */ + function getBalance(address payer, address collector, address receiver) external view returns (uint256); +} diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingBase.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingBase.sol new file mode 100644 index 000000000..280facaf7 --- /dev/null +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingBase.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; +import { IGraphPayments } from "../IGraphPayments.sol"; + +import { LinkedList } from "../../libraries/LinkedList.sol"; + +/** + * @title Interface for the {HorizonStakingBase} contract. + * @notice Provides getters for {HorizonStaking} and {HorizonStakingExtension} storage variables. + * @dev Most functions operate over {HorizonStaking} provisions. To uniquely identify a provision + * functions take `serviceProvider` and `verifier` addresses. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IHorizonStakingBase { + /** + * @notice Emitted when a service provider stakes tokens. + * @dev TRANSITION PERIOD: After transition period move to IHorizonStakingMain. Temporarily it + * needs to be here since it's emitted by {_stake} which is used by both {HorizonStaking} + * and {HorizonStakingExtension}. + * @param serviceProvider The address of the service provider. + * @param tokens The amount of tokens staked. + */ + event HorizonStakeDeposited(address indexed serviceProvider, uint256 tokens); + + /** + * @notice Thrown when using an invalid thaw request type. + */ + error HorizonStakingInvalidThawRequestType(); + + /** + * @notice Gets the details of a service provider. + * @param serviceProvider The address of the service provider. + * @return The service provider details. + */ + function getServiceProvider( + address serviceProvider + ) external view returns (IHorizonStakingTypes.ServiceProvider memory); + + /** + * @notice Gets the stake of a service provider. + * @param serviceProvider The address of the service provider. + * @return The amount of tokens staked. + */ + function getStake(address serviceProvider) external view returns (uint256); + + /** + * @notice Gets the service provider's idle stake which is the stake that is not being + * used for any provision. Note that this only includes service provider's self stake. + * @param serviceProvider The address of the service provider. + * @return The amount of tokens that are idle. + */ + function getIdleStake(address serviceProvider) external view returns (uint256); + + /** + * @notice Gets the details of delegation pool. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @return The delegation pool details. + */ + function getDelegationPool( + address serviceProvider, + address verifier + ) external view returns (IHorizonStakingTypes.DelegationPool memory); + + /** + * @notice Gets the details of a delegation. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @param delegator The address of the delegator. + * @return The delegation details. + */ + function getDelegation( + address serviceProvider, + address verifier, + address delegator + ) external view returns (IHorizonStakingTypes.Delegation memory); + + /** + * @notice Gets the delegation fee cut for a payment type. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @param paymentType The payment type as defined by {IGraphPayments.PaymentTypes}. + * @return The delegation fee cut in PPM. + */ + function getDelegationFeeCut( + address serviceProvider, + address verifier, + IGraphPayments.PaymentTypes paymentType + ) external view returns (uint256); + + /** + * @notice Gets the details of a provision. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @return The provision details. + */ + function getProvision( + address serviceProvider, + address verifier + ) external view returns (IHorizonStakingTypes.Provision memory); + + /** + * @notice Gets the tokens available in a provision. + * Tokens available are the tokens in a provision that are not thawing. Includes service + * provider's and delegator's stake. + * + * Allows specifying a `delegationRatio` which caps the amount of delegated tokens that are + * considered available. + * + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @param delegationRatio The delegation ratio. + * @return The amount of tokens available. + */ + function getTokensAvailable( + address serviceProvider, + address verifier, + uint32 delegationRatio + ) external view returns (uint256); + + /** + * @notice Gets the service provider's tokens available in a provision. + * @dev Calculated as the tokens available minus the tokens thawing. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @return The amount of tokens available. + */ + function getProviderTokensAvailable(address serviceProvider, address verifier) external view returns (uint256); + + /** + * @notice Gets the delegator's tokens available in a provision. + * @dev Calculated as the tokens available minus the tokens thawing. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @return The amount of tokens available. + */ + function getDelegatedTokensAvailable(address serviceProvider, address verifier) external view returns (uint256); + + /** + * @notice Gets a thaw request. + * @param thawRequestType The type of thaw request. + * @param thawRequestId The id of the thaw request. + * @return The thaw request details. + */ + function getThawRequest( + IHorizonStakingTypes.ThawRequestType thawRequestType, + bytes32 thawRequestId + ) external view returns (IHorizonStakingTypes.ThawRequest memory); + + /** + * @notice Gets the metadata of a thaw request list. + * Service provider and delegators each have their own thaw request list per provision. + * Metadata includes the head and tail of the list, plus the total number of thaw requests. + * @param thawRequestType The type of thaw request. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @param owner The owner of the thaw requests. Use either the service provider or delegator address. + * @return The thaw requests list metadata. + */ + function getThawRequestList( + IHorizonStakingTypes.ThawRequestType thawRequestType, + address serviceProvider, + address verifier, + address owner + ) external view returns (LinkedList.List memory); + + /** + * @notice Gets the amount of thawed tokens for a given provision. + * @param thawRequestType The type of thaw request. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + * @param owner The owner of the thaw requests. Use either the service provider or delegator address. + * @return The amount of thawed tokens. + */ + function getThawedTokens( + IHorizonStakingTypes.ThawRequestType thawRequestType, + address serviceProvider, + address verifier, + address owner + ) external view returns (uint256); + + /** + * @notice Gets the maximum allowed thawing period for a provision. + * @return The maximum allowed thawing period in seconds. + */ + function getMaxThawingPeriod() external view returns (uint64); + + /** + * @notice Return true if the verifier is an allowed locked verifier. + * @param verifier Address of the verifier + * @return True if verifier is allowed locked verifier, false otherwise + */ + function isAllowedLockedVerifier(address verifier) external view returns (bool); + + /** + * @notice Return true if delegation slashing is enabled, false otherwise. + * @return True if delegation slashing is enabled, false otherwise + */ + function isDelegationSlashingEnabled() external view returns (bool); +} diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingExtension.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingExtension.sol new file mode 100644 index 000000000..77ce2cc03 --- /dev/null +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingExtension.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IRewardsIssuer } from "@graphprotocol/contracts/contracts/rewards/IRewardsIssuer.sol"; + +/** + * @title Interface for {HorizonStakingExtension} contract. + * @notice Provides functions for managing legacy allocations. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IHorizonStakingExtension is IRewardsIssuer { + /** + * @dev Allocate GRT tokens for the purpose of serving queries of a subgraph deployment + * An allocation is created in the allocate() function and closed in closeAllocation() + * @param indexer The indexer address + * @param subgraphDeploymentID The subgraph deployment ID + * @param tokens The amount of tokens allocated to the subgraph deployment + * @param createdAtEpoch The epoch when the allocation was created + * @param closedAtEpoch The epoch when the allocation was closed + * @param collectedFees The amount of collected fees for the allocation + * @param __DEPRECATED_effectiveAllocation Deprecated field. + * @param accRewardsPerAllocatedToken Snapshot used for reward calculation + * @param distributedRebates The amount of collected rebates that have been rebated + */ + struct Allocation { + address indexer; + bytes32 subgraphDeploymentID; + uint256 tokens; + uint256 createdAtEpoch; + uint256 closedAtEpoch; + uint256 collectedFees; + uint256 __DEPRECATED_effectiveAllocation; + uint256 accRewardsPerAllocatedToken; + uint256 distributedRebates; + } + + /** + * @dev Possible states an allocation can be. + * States: + * - Null = indexer == address(0) + * - Active = not Null && tokens > 0 + * - Closed = Active && closedAtEpoch != 0 + */ + enum AllocationState { + Null, + Active, + Closed + } + + /** + * @dev Emitted when `indexer` close an allocation in `epoch` for `allocationID`. + * An amount of `tokens` get unallocated from `subgraphDeploymentID`. + * This event also emits the POI (proof of indexing) submitted by the indexer. + * `isPublic` is true if the sender was someone other than the indexer. + * @param indexer The indexer address + * @param subgraphDeploymentID The subgraph deployment ID + * @param epoch The protocol epoch the allocation was closed on + * @param tokens The amount of tokens unallocated from the allocation + * @param allocationID The allocation identifier + * @param sender The address closing the allocation + * @param poi The proof of indexing submitted by the sender + * @param isPublic True if the allocation was force closed by someone other than the indexer/operator + */ + event AllocationClosed( + address indexed indexer, + bytes32 indexed subgraphDeploymentID, + uint256 epoch, + uint256 tokens, + address indexed allocationID, + address sender, + bytes32 poi, + bool isPublic + ); + + /** + * @dev Emitted when `indexer` collects a rebate on `subgraphDeploymentID` for `allocationID`. + * `epoch` is the protocol epoch the rebate was collected on + * The rebate is for `tokens` amount which are being provided by `assetHolder`; `queryFees` + * is the amount up for rebate after `curationFees` are distributed and `protocolTax` is burnt. + * `queryRebates` is the amount distributed to the `indexer` with `delegationFees` collected + * and sent to the delegation pool. + * @param assetHolder The address of the asset holder, the entity paying the query fees + * @param indexer The indexer address + * @param subgraphDeploymentID The subgraph deployment ID + * @param allocationID The allocation identifier + * @param epoch The protocol epoch the rebate was collected on + * @param tokens The amount of tokens collected + * @param protocolTax The amount of tokens burnt as protocol tax + * @param curationFees The amount of tokens distributed to the curation pool + * @param queryFees The amount of tokens collected as query fees + * @param queryRebates The amount of tokens distributed to the indexer + * @param delegationRewards The amount of tokens collected from the delegation pool + */ + event RebateCollected( + address assetHolder, + address indexed indexer, + bytes32 indexed subgraphDeploymentID, + address indexed allocationID, + uint256 epoch, + uint256 tokens, + uint256 protocolTax, + uint256 curationFees, + uint256 queryFees, + uint256 queryRebates, + uint256 delegationRewards + ); + + /** + * @dev Emitted when `indexer` was slashed for a total of `tokens` amount. + * Tracks `reward` amount of tokens given to `beneficiary`. + * @param indexer The indexer address + * @param tokens The amount of tokens slashed + * @param reward The amount of reward tokens to send to a beneficiary + * @param beneficiary The address of a beneficiary to receive a reward for the slashing + */ + event StakeSlashed(address indexed indexer, uint256 tokens, uint256 reward, address beneficiary); + + /** + * @notice Close an allocation and free the staked tokens. + * To be eligible for rewards a proof of indexing must be presented. + * Presenting a bad proof is subject to slashable condition. + * To opt out of rewards set _poi to 0x0 + * @param allocationID The allocation identifier + * @param poi Proof of indexing submitted for the allocated period + */ + function closeAllocation(address allocationID, bytes32 poi) external; + + /** + * @dev Collect and rebate query fees to the indexer + * This function will accept calls with zero tokens. + * We use an exponential rebate formula to calculate the amount of tokens to rebate to the indexer. + * This implementation allows collecting multiple times on the same allocation, keeping track of the + * total amount rebated, the total amount collected and compensating the indexer for the difference. + * @param tokens Amount of tokens to collect + * @param allocationID Allocation where the tokens will be assigned + */ + function collect(uint256 tokens, address allocationID) external; + + /** + * @notice Slash the indexer stake. Delegated tokens are not subject to slashing. + * @dev Can only be called by the slasher role. + * @param indexer Address of indexer to slash + * @param tokens Amount of tokens to slash from the indexer stake + * @param reward Amount of reward tokens to send to a beneficiary + * @param beneficiary Address of a beneficiary to receive a reward for the slashing + */ + function legacySlash(address indexer, uint256 tokens, uint256 reward, address beneficiary) external; + + /** + * @notice (Legacy) Return true if operator is allowed for the service provider on the subgraph data service. + * @param operator Address of the operator + * @param indexer Address of the service provider + * @return True if operator is allowed for indexer, false otherwise + */ + function isOperator(address operator, address indexer) external view returns (bool); + + /** + * @notice Getter that returns if an indexer has any stake. + * @param indexer Address of the indexer + * @return True if indexer has staked tokens + */ + function hasStake(address indexer) external view returns (bool); + + /** + * @notice Get the total amount of tokens staked by the indexer. + * @param indexer Address of the indexer + * @return Amount of tokens staked by the indexer + */ + function getIndexerStakedTokens(address indexer) external view returns (uint256); + + /** + * @notice Return the allocation by ID. + * @param allocationID Address used as allocation identifier + * @return Allocation data + */ + function getAllocation(address allocationID) external view returns (Allocation memory); + + /** + * @notice Return the current state of an allocation + * @param allocationID Allocation identifier + * @return AllocationState enum with the state of the allocation + */ + function getAllocationState(address allocationID) external view returns (AllocationState); + + /** + * @notice Return if allocationID is used. + * @param allocationID Address used as signer by the indexer for an allocation + * @return True if allocationID already used + */ + function isAllocation(address allocationID) external view returns (bool); + + /** + * @notice Return the time in blocks to unstake + * Deprecated, now enforced by each data service (verifier) + * @return Thawing period in blocks + */ + function __DEPRECATED_getThawingPeriod() external view returns (uint64); + + /** + * @notice Return the address of the subgraph data service. + * @dev TRANSITION PERIOD: After transition period move to main HorizonStaking contract + * @return Address of the subgraph data service + */ + function getSubgraphService() external view returns (address); +} diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol new file mode 100644 index 000000000..4fc677b83 --- /dev/null +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol @@ -0,0 +1,991 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IGraphPayments } from "../../interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; + +/** + * @title Inferface for the {HorizonStaking} contract. + * @notice Provides functions for managing stake, provisions, delegations, and slashing. + * @dev Note that this interface only includes the functions implemented by {HorizonStaking} contract, + * and not those implemented by {HorizonStakingExtension}. + * Do not use this interface to interface with the {HorizonStaking} contract, use {IHorizonStaking} for + * the complete interface. + * @dev Most functions operate over {HorizonStaking} provisions. To uniquely identify a provision + * functions take `serviceProvider` and `verifier` addresses. + * @dev TRANSITION PERIOD: After transition period rename to IHorizonStaking. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IHorizonStakingMain { + // -- Events: stake -- + + /** + * @notice Emitted when a service provider unstakes tokens during the transition period. + * @param serviceProvider The address of the service provider + * @param tokens The amount of tokens unstaked + * @param until The block number until the stake is locked + */ + event HorizonStakeLocked(address indexed serviceProvider, uint256 tokens, uint256 until); + + /** + * @notice Emitted when a service provider withdraws tokens during the transition period. + * @param serviceProvider The address of the service provider + * @param tokens The amount of tokens withdrawn + */ + event HorizonStakeWithdrawn(address indexed serviceProvider, uint256 tokens); + + // -- Events: provision -- + + /** + * @notice Emitted when a service provider provisions staked tokens to a verifier. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param tokens The amount of tokens provisioned + * @param maxVerifierCut The maximum cut, expressed in PPM of the slashed amount, that a verifier can take for themselves when slashing + * @param thawingPeriod The period in seconds that the tokens will be thawing before they can be removed from the provision + */ + event ProvisionCreated( + address indexed serviceProvider, + address indexed verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ); + + /** + * @notice Emitted whenever staked tokens are added to an existing provision + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param tokens The amount of tokens added to the provision + */ + event ProvisionIncreased(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + /** + * @notice Emitted when a service provider thaws tokens from a provision. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param tokens The amount of tokens thawed + */ + event ProvisionThawed(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + /** + * @notice Emitted when a service provider removes tokens from a provision. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param tokens The amount of tokens removed + */ + event TokensDeprovisioned(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + /** + * @notice Emitted when a service provider stages a provision parameter update. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param maxVerifierCut The proposed maximum cut, expressed in PPM of the slashed amount, that a verifier can take for + * themselves when slashing + * @param thawingPeriod The proposed period in seconds that the tokens will be thawing before they can be removed from + * the provision + */ + event ProvisionParametersStaged( + address indexed serviceProvider, + address indexed verifier, + uint32 maxVerifierCut, + uint64 thawingPeriod + ); + + /** + * @notice Emitted when a service provider accepts a staged provision parameter update. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param maxVerifierCut The new maximum cut, expressed in PPM of the slashed amount, that a verifier can take for themselves + * when slashing + * @param thawingPeriod The new period in seconds that the tokens will be thawing before they can be removed from the provision + */ + event ProvisionParametersSet( + address indexed serviceProvider, + address indexed verifier, + uint32 maxVerifierCut, + uint64 thawingPeriod + ); + + /** + * @dev Emitted when an operator is allowed or denied by a service provider for a particular verifier + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param operator The address of the operator + * @param allowed Whether the operator is allowed or denied + */ + event OperatorSet( + address indexed serviceProvider, + address indexed verifier, + address indexed operator, + bool allowed + ); + + // -- Events: slashing -- + + /** + * @notice Emitted when a provision is slashed by a verifier. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param tokens The amount of tokens slashed (note this only represents service provider's slashed stake) + */ + event ProvisionSlashed(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + /** + * @notice Emitted when a delegation pool is slashed by a verifier. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param tokens The amount of tokens slashed (note this only represents delegation pool's slashed stake) + */ + event DelegationSlashed(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + /** + * @notice Emitted when a delegation pool would have been slashed by a verifier, but the slashing was skipped + * because delegation slashing global parameter is not enabled. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param tokens The amount of tokens that would have been slashed (note this only represents delegation pool's slashed stake) + */ + event DelegationSlashingSkipped(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + /** + * @notice Emitted when the verifier cut is sent to the verifier after slashing a provision. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param destination The address where the verifier cut is sent + * @param tokens The amount of tokens sent to the verifier + */ + event VerifierTokensSent( + address indexed serviceProvider, + address indexed verifier, + address indexed destination, + uint256 tokens + ); + + // -- Events: delegation -- + + /** + * @notice Emitted when tokens are delegated to a provision. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param delegator The address of the delegator + * @param tokens The amount of tokens delegated + * @param shares The amount of shares delegated + */ + event TokensDelegated( + address indexed serviceProvider, + address indexed verifier, + address indexed delegator, + uint256 tokens, + uint256 shares + ); + + /** + * @notice Emitted when a delegator undelegates tokens from a provision and starts + * thawing them. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param delegator The address of the delegator + * @param tokens The amount of tokens undelegated + */ + event TokensUndelegated( + address indexed serviceProvider, + address indexed verifier, + address indexed delegator, + uint256 tokens + ); + + /** + * @notice Emitted when a delegator withdraws tokens from a provision after thawing. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param delegator The address of the delegator + * @param tokens The amount of tokens withdrawn + */ + event DelegatedTokensWithdrawn( + address indexed serviceProvider, + address indexed verifier, + address indexed delegator, + uint256 tokens + ); + + /** + * @notice Emitted when `delegator` withdrew delegated `tokens` from `indexer` using `withdrawDelegated`. + * @dev This event is for the legacy `withdrawDelegated` function. + * @param indexer The address of the indexer + * @param delegator The address of the delegator + * @param tokens The amount of tokens withdrawn + */ + event StakeDelegatedWithdrawn(address indexed indexer, address indexed delegator, uint256 tokens); + + /** + * @notice Emitted when tokens are added to a delegation pool's reserve. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param tokens The amount of tokens withdrawn + */ + event TokensToDelegationPoolAdded(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + /** + * @notice Emitted when a service provider sets delegation fee cuts for a verifier. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param paymentType The payment type for which the fee cut is set, as defined in {IGraphPayments} + * @param feeCut The fee cut set, in PPM + */ + event DelegationFeeCutSet( + address indexed serviceProvider, + address indexed verifier, + IGraphPayments.PaymentTypes indexed paymentType, + uint256 feeCut + ); + + // -- Events: thawing -- + + /** + * @notice Emitted when a thaw request is created. + * @dev Can be emitted by the service provider when thawing stake or by the delegator when undelegating. + * @param requestType The type of thaw request + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param owner The address of the owner of the thaw request. + * @param shares The amount of shares being thawed + * @param thawingUntil The timestamp until the stake is thawed + * @param thawRequestId The ID of the thaw request + */ + event ThawRequestCreated( + IHorizonStakingTypes.ThawRequestType indexed requestType, + address indexed serviceProvider, + address indexed verifier, + address owner, + uint256 shares, + uint64 thawingUntil, + bytes32 thawRequestId + ); + + /** + * @notice Emitted when a thaw request is fulfilled, meaning the stake is released. + * @param requestType The type of thaw request + * @param thawRequestId The ID of the thaw request + * @param tokens The amount of tokens being released + * @param shares The amount of shares being released + * @param thawingUntil The timestamp until the stake has thawed + * @param valid Whether the thaw request was valid at the time of fulfillment + */ + event ThawRequestFulfilled( + IHorizonStakingTypes.ThawRequestType indexed requestType, + bytes32 indexed thawRequestId, + uint256 tokens, + uint256 shares, + uint64 thawingUntil, + bool valid + ); + + /** + * @notice Emitted when a series of thaw requests are fulfilled. + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param owner The address of the owner of the thaw requests + * @param thawRequestsFulfilled The number of thaw requests fulfilled + * @param tokens The total amount of tokens being released + * @param requestType The type of thaw request + */ + event ThawRequestsFulfilled( + IHorizonStakingTypes.ThawRequestType indexed requestType, + address indexed serviceProvider, + address indexed verifier, + address owner, + uint256 thawRequestsFulfilled, + uint256 tokens + ); + + // -- Events: governance -- + + /** + * @notice Emitted when the global maximum thawing period allowed for provisions is set. + * @param maxThawingPeriod The new maximum thawing period + */ + event MaxThawingPeriodSet(uint64 maxThawingPeriod); + + /** + * @notice Emitted when a verifier is allowed or disallowed to be used for locked provisions. + * @param verifier The address of the verifier + * @param allowed Whether the verifier is allowed or disallowed + */ + event AllowedLockedVerifierSet(address indexed verifier, bool allowed); + + /** + * @notice Emitted when the legacy global thawing period is set to zero. + * @dev This marks the end of the transition period. + */ + event ThawingPeriodCleared(); + + /** + * @notice Emitted when the delegation slashing global flag is set. + */ + event DelegationSlashingEnabled(); + + // -- Errors: tokens + + /** + * @notice Thrown when operating a zero token amount is not allowed. + */ + error HorizonStakingInvalidZeroTokens(); + + /** + * @notice Thrown when a minimum token amount is required to operate but it's not met. + * @param tokens The actual token amount + * @param minRequired The minimum required token amount + */ + error HorizonStakingInsufficientTokens(uint256 tokens, uint256 minRequired); + + /** + * @notice Thrown when the amount of tokens exceeds the maximum allowed to operate. + * @param tokens The actual token amount + * @param maxTokens The maximum allowed token amount + */ + error HorizonStakingTooManyTokens(uint256 tokens, uint256 maxTokens); + + // -- Errors: provision -- + + /** + * @notice Thrown when attempting to operate with a provision that does not exist. + * @param serviceProvider The service provider address + * @param verifier The verifier address + */ + error HorizonStakingInvalidProvision(address serviceProvider, address verifier); + + /** + * @notice Thrown when the caller is not authorized to operate on a provision. + * @param caller The caller address + * @param serviceProvider The service provider address + * @param verifier The verifier address + */ + error HorizonStakingNotAuthorized(address serviceProvider, address verifier, address caller); + + /** + * @notice Thrown when attempting to create a provision with a verifier other than the + * subgraph data service. This restriction only applies during the transition period. + * @param verifier The verifier address + */ + error HorizonStakingInvalidVerifier(address verifier); + + /** + * @notice Thrown when attempting to create a provision with an invalid maximum verifier cut. + * @param maxVerifierCut The maximum verifier cut + */ + error HorizonStakingInvalidMaxVerifierCut(uint32 maxVerifierCut); + + /** + * @notice Thrown when attempting to create a provision with an invalid thawing period. + * @param thawingPeriod The thawing period + * @param maxThawingPeriod The maximum `thawingPeriod` allowed + */ + error HorizonStakingInvalidThawingPeriod(uint64 thawingPeriod, uint64 maxThawingPeriod); + + /** + * @notice Thrown when attempting to create a provision for a data service that already has a provision. + */ + error HorizonStakingProvisionAlreadyExists(); + + // -- Errors: stake -- + + /** + * @notice Thrown when the service provider has insufficient idle stake to operate. + * @param tokens The actual token amount + * @param minTokens The minimum required token amount + */ + error HorizonStakingInsufficientIdleStake(uint256 tokens, uint256 minTokens); + + /** + * @notice Thrown during the transition period when the service provider has insufficient stake to + * cover their existing legacy allocations. + * @param tokens The actual token amount + * @param minTokens The minimum required token amount + */ + error HorizonStakingInsufficientStakeForLegacyAllocations(uint256 tokens, uint256 minTokens); + + // -- Errors: delegation -- + + /** + * @notice Thrown when delegation shares obtained are below the expected amount. + * @param shares The actual share amount + * @param minShares The minimum required share amount + */ + error HorizonStakingSlippageProtection(uint256 shares, uint256 minShares); + + /** + * @notice Thrown when operating a zero share amount is not allowed. + */ + error HorizonStakingInvalidZeroShares(); + + /** + * @notice Thrown when a minimum share amount is required to operate but it's not met. + * @param shares The actual share amount + * @param minShares The minimum required share amount + */ + error HorizonStakingInsufficientShares(uint256 shares, uint256 minShares); + + /** + * @notice Thrown when as a result of slashing delegation pool has no tokens but has shares. + * @param serviceProvider The service provider address + * @param verifier The verifier address + */ + error HorizonStakingInvalidDelegationPoolState(address serviceProvider, address verifier); + + /** + * @notice Thrown when attempting to operate with a delegation pool that does not exist. + * @param serviceProvider The service provider address + * @param verifier The verifier address + */ + error HorizonStakingInvalidDelegationPool(address serviceProvider, address verifier); + + /** + * @notice Thrown when the minimum token amount required for delegation is not met. + * @param tokens The actual token amount + * @param minTokens The minimum required token amount + */ + error HorizonStakingInsufficientDelegationTokens(uint256 tokens, uint256 minTokens); + + /** + * @notice Thrown when attempting to redelegate with a serivce provider that is the zero address. + */ + error HorizonStakingInvalidServiceProviderZeroAddress(); + + /** + * @notice Thrown when attempting to redelegate with a verifier that is the zero address. + */ + error HorizonStakingInvalidVerifierZeroAddress(); + + // -- Errors: thaw requests -- + + /** + * @notice Thrown when attempting to fulfill a thaw request but there is nothing thawing. + */ + error HorizonStakingNothingThawing(); + + /** + * @notice Thrown when a service provider has too many thaw requests. + */ + error HorizonStakingTooManyThawRequests(); + + /** + * @notice Thrown when attempting to withdraw tokens that have not thawed (legacy undelegate). + */ + error HorizonStakingNothingToWithdraw(); + + // -- Errors: misc -- + /** + * @notice Thrown during the transition period when attempting to withdraw tokens that are still thawing. + * @dev Note this thawing refers to the global thawing period applied to legacy allocated tokens, + * it does not refer to thaw requests. + * @param until The block number until the stake is locked + */ + error HorizonStakingStillThawing(uint256 until); + + /** + * @notice Thrown when a service provider attempts to operate on verifiers that are not allowed. + * @dev Only applies to stake from locked wallets. + * @param verifier The verifier address + */ + error HorizonStakingVerifierNotAllowed(address verifier); + + /** + * @notice Thrown when a service provider attempts to change their own operator access. + */ + error HorizonStakingCallerIsServiceProvider(); + + /** + * @notice Thrown when trying to set a delegation fee cut that is not valid. + * @param feeCut The fee cut + */ + error HorizonStakingInvalidDelegationFeeCut(uint256 feeCut); + + /** + * @notice Thrown when a legacy slash fails. + */ + error HorizonStakingLegacySlashFailed(); + + // -- Functions -- + + /** + * @notice Deposit tokens on the staking contract. + * @dev Pulls tokens from the caller. + * + * Requirements: + * - `_tokens` cannot be zero. + * - Caller must have previously approved this contract to pull tokens from their balance. + * + * Emits a {HorizonStakeDeposited} event. + * + * @param tokens Amount of tokens to stake + */ + function stake(uint256 tokens) external; + + /** + * @notice Deposit tokens on the service provider stake, on behalf of the service provider. + * @dev Pulls tokens from the caller. + * + * Requirements: + * - `_tokens` cannot be zero. + * - Caller must have previously approved this contract to pull tokens from their balance. + * + * Emits a {HorizonStakeDeposited} event. + * + * @param serviceProvider Address of the service provider + * @param tokens Amount of tokens to stake + */ + function stakeTo(address serviceProvider, uint256 tokens) external; + + // can be called by anyone if the service provider has provisioned stake to this verifier + /** + * @notice Deposit tokens on the service provider stake, on behalf of the service provider, + * provisioned to a specific verifier. + * @dev Requirements: + * - The `serviceProvider` must have previously provisioned stake to `verifier`. + * - `_tokens` cannot be zero. + * - Caller must have previously approved this contract to pull tokens from their balance. + * + * Emits {HorizonStakeDeposited} and {ProvisionIncreased} events. + * + * @param serviceProvider Address of the service provider + * @param verifier Address of the verifier + * @param tokens Amount of tokens to stake + */ + function stakeToProvision(address serviceProvider, address verifier, uint256 tokens) external; + + /** + * @notice Move idle stake back to the owner's account. + * Stake is removed from the protocol: + * - During the transition period it's locked for a period of time before it can be withdrawn + * by calling {withdraw}. + * - After the transition period it's immediately withdrawn. + * Note that after the transition period if there are tokens still locked they will have to be + * withdrawn by calling {withdraw}. + * @dev Requirements: + * - `_tokens` cannot be zero. + * - `_serviceProvider` must have enough idle stake to cover the staking amount and any + * legacy allocation. + * + * Emits a {HorizonStakeLocked} event during the transition period. + * Emits a {HorizonStakeWithdrawn} event after the transition period. + * + * @param tokens Amount of tokens to unstake + */ + function unstake(uint256 tokens) external; + + /** + * @notice Withdraw service provider tokens once the thawing period (initiated by {unstake}) has passed. + * All thawed tokens are withdrawn. + * @dev This is only needed during the transition period while we still have + * a global lock. After that, unstake() will automatically withdraw. + */ + function withdraw() external; + + /** + * @notice Provision stake to a verifier. The tokens will be locked with a thawing period + * and will be slashable by the verifier. This is the main mechanism to provision stake to a data + * service, where the data service is the verifier. + * This function can be called by the service provider or by an operator authorized by the provider + * for this specific verifier. + * @dev During the transition period, only the subgraph data service can be used as a verifier. This + * prevents an escape hatch for legacy allocation stake. + * @dev Requirements: + * - `tokens` cannot be zero. + * - The `serviceProvider` must have enough idle stake to cover the tokens to provision. + * - `maxVerifierCut` must be a valid PPM. + * - `thawingPeriod` must be less than or equal to `_maxThawingPeriod`. + * + * Emits a {ProvisionCreated} event. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address for which the tokens are provisioned (who will be able to slash the tokens) + * @param tokens The amount of tokens that will be locked and slashable + * @param maxVerifierCut The maximum cut, expressed in PPM, that a verifier can transfer instead of burning when slashing + * @param thawingPeriod The period in seconds that the tokens will be thawing before they can be removed from the provision + */ + function provision( + address serviceProvider, + address verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) external; + + /** + * @notice Adds tokens from the service provider's idle stake to a provision + * @dev + * + * Requirements: + * - The `serviceProvider` must have previously provisioned stake to `verifier`. + * - `tokens` cannot be zero. + * - The `serviceProvider` must have enough idle stake to cover the tokens to add. + * + * Emits a {ProvisionIncreased} event. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address + * @param tokens The amount of tokens to add to the provision + */ + function addToProvision(address serviceProvider, address verifier, uint256 tokens) external; + + /** + * @notice Start thawing tokens to remove them from a provision. + * This function can be called by the service provider or by an operator authorized by the provider + * for this specific verifier. + * + * Note that removing tokens from a provision is a two step process: + * - First the tokens are thawed using this function. + * - Then after the thawing period, the tokens are removed from the provision using {deprovision} + * or {reprovision}. + * + * @dev Requirements: + * - The provision must have enough tokens available to thaw. + * - `tokens` cannot be zero. + * + * Emits {ProvisionThawed} and {ThawRequestCreated} events. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address for which the tokens are provisioned + * @param tokens The amount of tokens to thaw + * @return The ID of the thaw request + */ + function thaw(address serviceProvider, address verifier, uint256 tokens) external returns (bytes32); + + /** + * @notice Remove tokens from a provision and move them back to the service provider's idle stake. + * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw + * requests in the event that fulfilling all of them results in a gas limit error. + * + * Requirements: + * - Must have previously initiated a thaw request using {thaw}. + * + * Emits {ThawRequestFulfilled}, {ThawRequestsFulfilled} and {TokensDeprovisioned} events. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address + * @param nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests. + */ + function deprovision(address serviceProvider, address verifier, uint256 nThawRequests) external; + + /** + * @notice Move already thawed stake from one provision into another provision + * This function can be called by the service provider or by an operator authorized by the provider + * for the two corresponding verifiers. + * @dev Requirements: + * - Must have previously initiated a thaw request using {thaw}. + * - `tokens` cannot be zero. + * - The `serviceProvider` must have previously provisioned stake to `newVerifier`. + * - The `serviceProvider` must have enough idle stake to cover the tokens to add. + * + * Emits {ThawRequestFulfilled}, {ThawRequestsFulfilled}, {TokensDeprovisioned} and {ProvisionIncreased} + * events. + * + * @param serviceProvider The service provider address + * @param oldVerifier The verifier address for which the tokens are currently provisioned + * @param newVerifier The verifier address for which the tokens will be provisioned + * @param nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests. + */ + function reprovision( + address serviceProvider, + address oldVerifier, + address newVerifier, + uint256 nThawRequests + ) external; + + /** + * @notice Stages a provision parameter update. Note that the change is not effective until the verifier calls + * {acceptProvisionParameters}. + * @dev This two step update process prevents the service provider from changing the parameters + * without the verifier's consent. + * + * Emits a {ProvisionParametersStaged} event if at least one of the parameters changed. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address + * @param maxVerifierCut The proposed maximum cut, expressed in PPM of the slashed amount, that a verifier can take for + * themselves when slashing + * @param thawingPeriod The proposed period in seconds that the tokens will be thawing before they can be removed from + * the provision + */ + function setProvisionParameters( + address serviceProvider, + address verifier, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) external; + + /** + * @notice Accepts a staged provision parameter update. + * @dev Only the provision's verifier can call this function. + * + * Emits a {ProvisionParametersSet} event. + * + * @param serviceProvider The service provider address + */ + function acceptProvisionParameters(address serviceProvider) external; + + /** + * @notice Delegate tokens to a provision. + * @dev Requirements: + * - `tokens` cannot be zero. + * - Caller must have previously approved this contract to pull tokens from their balance. + * - The provision must exist. + * + * Emits a {TokensDelegated} event. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address + * @param tokens The amount of tokens to delegate + * @param minSharesOut The minimum amount of shares to accept, slippage protection. + */ + function delegate(address serviceProvider, address verifier, uint256 tokens, uint256 minSharesOut) external; + + /** + * @notice Add tokens to a delegation pool without issuing shares. + * Used by data services to pay delegation fees/rewards. + * Delegators SHOULD NOT call this function. + * + * @dev Requirements: + * - `tokens` cannot be zero. + * - Caller must have previously approved this contract to pull tokens from their balance. + * + * Emits a {TokensToDelegationPoolAdded} event. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address for which the tokens are provisioned + * @param tokens The amount of tokens to add to the delegation pool + */ + function addToDelegationPool(address serviceProvider, address verifier, uint256 tokens) external; + + /** + * @notice Undelegate tokens from a provision and start thawing them. + * Note that undelegating tokens from a provision is a two step process: + * - First the tokens are thawed using this function. + * - Then after the thawing period, the tokens are removed from the provision using {withdrawDelegated}. + * + * Requirements: + * - `shares` cannot be zero. + * + * Emits a {TokensUndelegated} and {ThawRequestCreated} event. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address + * @param shares The amount of shares to undelegate + * @return The ID of the thaw request + */ + function undelegate(address serviceProvider, address verifier, uint256 shares) external returns (bytes32); + + /** + * @notice Withdraw undelegated tokens from a provision after thawing. + * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw + * requests in the event that fulfilling all of them results in a gas limit error. + * @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill + * the thaw requests with an amount equal to zero. + * + * Requirements: + * - Must have previously initiated a thaw request using {undelegate}. + * + * Emits {ThawRequestFulfilled}, {ThawRequestsFulfilled} and {DelegatedTokensWithdrawn} events. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address + * @param nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests. + */ + function withdrawDelegated(address serviceProvider, address verifier, uint256 nThawRequests) external; + + /** + * @notice Re-delegate undelegated tokens from a provision after thawing to a `newServiceProvider` and `newVerifier`. + * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw + * requests in the event that fulfilling all of them results in a gas limit error. + * + * Requirements: + * - Must have previously initiated a thaw request using {undelegate}. + * - `newServiceProvider` and `newVerifier` must not be the zero address. + * - `newServiceProvider` must have previously provisioned stake to `newVerifier`. + * + * Emits {ThawRequestFulfilled}, {ThawRequestsFulfilled} and {DelegatedTokensWithdrawn} events. + * + * @param oldServiceProvider The old service provider address + * @param oldVerifier The old verifier address + * @param newServiceProvider The address of a new service provider + * @param newVerifier The address of a new verifier + * @param minSharesForNewProvider The minimum amount of shares to accept for the new service provider + * @param nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests. + */ + function redelegate( + address oldServiceProvider, + address oldVerifier, + address newServiceProvider, + address newVerifier, + uint256 minSharesForNewProvider, + uint256 nThawRequests + ) external; + + /** + * @notice Set the fee cut for a verifier on a specific payment type. + * @dev Emits a {DelegationFeeCutSet} event. + * @param serviceProvider The service provider address + * @param verifier The verifier address + * @param paymentType The payment type for which the fee cut is set, as defined in {IGraphPayments} + * @param feeCut The fee cut to set, in PPM + */ + function setDelegationFeeCut( + address serviceProvider, + address verifier, + IGraphPayments.PaymentTypes paymentType, + uint256 feeCut + ) external; + + /** + * @notice Delegate tokens to the subgraph data service provision. + * This function is for backwards compatibility with the legacy staking contract. + * It only allows delegating to the subgraph data service and DOES NOT have slippage protection. + * @dev See {delegate}. + * @param serviceProvider The service provider address + * @param tokens The amount of tokens to delegate + */ + function delegate(address serviceProvider, uint256 tokens) external; + + /** + * @notice Undelegate tokens from the subgraph data service provision and start thawing them. + * This function is for backwards compatibility with the legacy staking contract. + * It only allows undelegating from the subgraph data service. + * @dev See {undelegate}. + * @param serviceProvider The service provider address + * @param shares The amount of shares to undelegate + */ + function undelegate(address serviceProvider, uint256 shares) external; + + /** + * @notice Withdraw undelegated tokens from the subgraph data service provision after thawing. + * This function is for backwards compatibility with the legacy staking contract. + * It only allows withdrawing tokens undelegated before horizon upgrade. + * @dev See {delegate}. + * @param serviceProvider The service provider address + * @param deprecated Deprecated parameter kept for backwards compatibility + * @return The amount of tokens withdrawn + */ + function withdrawDelegated( + address serviceProvider, + address deprecated // kept for backwards compatibility + ) external returns (uint256); + + /** + * @notice Slash a service provider. This can only be called by a verifier to which + * the provider has provisioned stake, and up to the amount of tokens they have provisioned. + * If the service provider's stake is not enough, the associated delegation pool might be slashed + * depending on the value of the global delegation slashing flag. + * + * Part of the slashed tokens are sent to the `verifierDestination` as a reward. + * + * @dev Requirements: + * - `tokens` must be less than or equal to the amount of tokens provisioned by the service provider. + * - `tokensVerifier` must be less than the provision's tokens times the provision's maximum verifier cut. + * + * Emits a {ProvisionSlashed} and {VerifierTokensSent} events. + * Emits a {DelegationSlashed} or {DelegationSlashingSkipped} event depending on the global delegation slashing + * flag. + * + * @param serviceProvider The service provider to slash + * @param tokens The amount of tokens to slash + * @param tokensVerifier The amount of tokens to transfer instead of burning + * @param verifierDestination The address to transfer the verifier cut to + */ + function slash( + address serviceProvider, + uint256 tokens, + uint256 tokensVerifier, + address verifierDestination + ) external; + + /** + * @notice Provision stake to a verifier using locked tokens (i.e. from GraphTokenLockWallets). + * @dev See {provision}. + * + * Additional requirements: + * - The `verifier` must be allowed to be used for locked provisions. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address for which the tokens are provisioned (who will be able to slash the tokens) + * @param tokens The amount of tokens that will be locked and slashable + * @param maxVerifierCut The maximum cut, expressed in PPM, that a verifier can transfer instead of burning when slashing + * @param thawingPeriod The period in seconds that the tokens will be thawing before they can be removed from the provision + */ + function provisionLocked( + address serviceProvider, + address verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) external; + + /** + * @notice Authorize or unauthorize an address to be an operator for the caller on a verifier. + * + * @dev See {setOperator}. + * Additional requirements: + * - The `verifier` must be allowed to be used for locked provisions. + * + * @param verifier The verifier / data service on which they'll be allowed to operate + * @param operator Address to authorize or unauthorize + * @param allowed Whether the operator is authorized or not + */ + function setOperatorLocked(address verifier, address operator, bool allowed) external; + + /** + * @notice Sets a verifier as a globally allowed verifier for locked provisions. + * @dev This function can only be called by the contract governor, it's used to maintain + * a whitelist of verifiers that do not allow the stake from a locked wallet to escape the lock. + * @dev Emits a {AllowedLockedVerifierSet} event. + * @param verifier The verifier address + * @param allowed Whether the verifier is allowed or not + */ + function setAllowedLockedVerifier(address verifier, bool allowed) external; + + /** + * @notice Set the global delegation slashing flag to true. + * @dev This function can only be called by the contract governor. + */ + function setDelegationSlashingEnabled() external; + + /** + * @notice Clear the legacy global thawing period. + * This signifies the end of the transition period, after which no legacy allocations should be left. + * @dev This function can only be called by the contract governor. + * @dev Emits a {ThawingPeriodCleared} event. + */ + function clearThawingPeriod() external; + + /** + * @notice Sets the global maximum thawing period allowed for provisions. + * @param maxThawingPeriod The new maximum thawing period, in seconds + */ + function setMaxThawingPeriod(uint64 maxThawingPeriod) external; + + /** + * @notice Authorize or unauthorize an address to be an operator for the caller on a data service. + * @dev Emits a {OperatorSet} event. + * @param verifier The verifier / data service on which they'll be allowed to operate + * @param operator Address to authorize or unauthorize + * @param allowed Whether the operator is authorized or not + */ + function setOperator(address verifier, address operator, bool allowed) external; + + /** + * @notice Check if an operator is authorized for the caller on a specific verifier / data service. + * @param serviceProvider The service provider on behalf of whom they're claiming to act + * @param verifier The verifier / data service on which they're claiming to act + * @param operator The address to check for auth + * @return Whether the operator is authorized or not + */ + function isAuthorized(address serviceProvider, address verifier, address operator) external view returns (bool); + + /** + * @notice Get the address of the staking extension. + * @return The address of the staking extension + */ + function getStakingExtension() external view returns (address); +} diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol new file mode 100644 index 000000000..6c3420cc7 --- /dev/null +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +/** + * @title Defines the data types used in the Horizon staking contract + * @dev In order to preserve storage compatibility some data structures keep deprecated fields. + * These structures have then two representations, an internal one used by the contract storage and a public one. + * Getter functions should retrieve internal representations, remove deprecated fields and return the public representation. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IHorizonStakingTypes { + /** + * @notice Represents stake assigned to a specific verifier/data service. + * Provisioned stake is locked and can be used as economic security by a data service. + * @param tokens Service provider tokens in the provision (does not include delegated tokens) + * @param tokensThawing Service provider tokens that are being thawed (and will stop being slashable soon) + * @param sharesThawing Shares representing the thawing tokens + * @param maxVerifierCut Max amount that can be taken by the verifier when slashing, expressed in parts-per-million of the amount slashed + * @param thawingPeriod Time, in seconds, tokens must thaw before being withdrawn + * @param createdAt Timestamp when the provision was created + * @param maxVerifierCutPending Pending value for `maxVerifierCut`. Verifier needs to accept it before it becomes active. + * @param thawingPeriodPending Pending value for `thawingPeriod`. Verifier needs to accept it before it becomes active. + * @param thawingNonce Value of the current thawing nonce. Thaw requests with older nonces are invalid. + */ + struct Provision { + uint256 tokens; + uint256 tokensThawing; + uint256 sharesThawing; + uint32 maxVerifierCut; + uint64 thawingPeriod; + uint64 createdAt; + uint32 maxVerifierCutPending; + uint64 thawingPeriodPending; + uint256 thawingNonce; + } + + /** + * @notice Public representation of a service provider. + * @dev See {ServiceProviderInternal} for the actual storage representation + * @param tokensStaked Total amount of tokens on the provider stake (only staked by the provider, includes all provisions) + * @param tokensProvisioned Total amount of tokens locked in provisions (only staked by the provider) + */ + struct ServiceProvider { + uint256 tokensStaked; + uint256 tokensProvisioned; + } + + /** + * @notice Internal representation of a service provider. + * @dev It contains deprecated fields from the `Indexer` struct to maintain storage compatibility. + * @param tokensStaked Total amount of tokens on the provider stake (only staked by the provider, includes all provisions) + * @param __DEPRECATED_tokensAllocated (Deprecated) Tokens used in allocations + * @param __DEPRECATED_tokensLocked (Deprecated) Tokens locked for withdrawal subject to thawing period + * @param __DEPRECATED_tokensLockedUntil (Deprecated) Block when locked tokens can be withdrawn + * @param tokensProvisioned Total amount of tokens locked in provisions (only staked by the provider) + */ + struct ServiceProviderInternal { + uint256 tokensStaked; + uint256 __DEPRECATED_tokensAllocated; + uint256 __DEPRECATED_tokensLocked; + uint256 __DEPRECATED_tokensLockedUntil; + uint256 tokensProvisioned; + } + + /** + * @notice Public representation of a delegation pool. + * @dev See {DelegationPoolInternal} for the actual storage representation + * @param tokens Total tokens as pool reserves + * @param shares Total shares minted in the pool + * @param tokensThawing Tokens thawing in the pool + * @param sharesThawing Shares representing the thawing tokens + * @param thawingNonce Value of the current thawing nonce. Thaw requests with older nonces are invalid. + */ + struct DelegationPool { + uint256 tokens; + uint256 shares; + uint256 tokensThawing; + uint256 sharesThawing; + uint256 thawingNonce; + } + + /** + * @notice Internal representation of a delegation pool. + * @dev It contains deprecated fields from the previous version of the `DelegationPool` struct + * to maintain storage compatibility. + * @param __DEPRECATED_cooldownBlocks (Deprecated) Time, in blocks, an indexer must wait before updating delegation parameters + * @param __DEPRECATED_indexingRewardCut (Deprecated) Percentage of indexing rewards for the service provider, in PPM + * @param __DEPRECATED_queryFeeCut (Deprecated) Percentage of query fees for the service provider, in PPM + * @param __DEPRECATED_updatedAtBlock (Deprecated) Block when the delegation parameters were last updated + * @param tokens Total tokens as pool reserves + * @param shares Total shares minted in the pool + * @param delegators Delegation details by delegator + * @param tokensThawing Tokens thawing in the pool + * @param sharesThawing Shares representing the thawing tokens + * @param thawingNonce Value of the current thawing nonce. Thaw requests with older nonces are invalid. + */ + struct DelegationPoolInternal { + uint32 __DEPRECATED_cooldownBlocks; + uint32 __DEPRECATED_indexingRewardCut; + uint32 __DEPRECATED_queryFeeCut; + uint256 __DEPRECATED_updatedAtBlock; + uint256 tokens; + uint256 shares; + mapping(address delegator => DelegationInternal delegation) delegators; + uint256 tokensThawing; + uint256 sharesThawing; + uint256 thawingNonce; + } + + /** + * @notice Public representation of delegation details. + * @dev See {DelegationInternal} for the actual storage representation + * @param shares Shares owned by a delegator in the pool + */ + struct Delegation { + uint256 shares; + } + + /** + * @notice Internal representation of delegation details. + * @dev It contains deprecated fields from the previous version of the `Delegation` struct + * to maintain storage compatibility. + * @param shares Shares owned by the delegator in the pool + * @param __DEPRECATED_tokensLocked Tokens locked for undelegation + * @param __DEPRECATED_tokensLockedUntil Epoch when locked tokens can be withdrawn + */ + struct DelegationInternal { + uint256 shares; + uint256 __DEPRECATED_tokensLocked; + uint256 __DEPRECATED_tokensLockedUntil; + } + + /** + * @dev Enum to specify the type of thaw request. + * @param Provision Represents a thaw request for a provision. + * @param Delegation Represents a thaw request for a delegation. + */ + enum ThawRequestType { + Provision, + Delegation + } + + /** + * @notice Details of a stake thawing operation. + * @dev ThawRequests are stored in linked lists by service provider/delegator, + * ordered by creation timestamp. + * @param shares Shares that represent the tokens being thawed + * @param thawingUntil The timestamp when the thawed funds can be removed from the provision + * @param next Id of the next thaw request in the linked list + * @param thawingNonce Used to invalidate unfulfilled thaw requests + */ + struct ThawRequest { + uint256 shares; + uint64 thawingUntil; + bytes32 next; + uint256 thawingNonce; + } + + /** + * @notice Parameters to fulfill thaw requests. + * @dev This struct is used to avoid stack too deep error in the `fulfillThawRequests` function. + * @param requestType The type of thaw request (Provision or Delegation) + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param owner The address of the owner of the thaw request + * @param tokensThawing The current amount of tokens already thawing + * @param sharesThawing The current amount of shares already thawing + * @param nThawRequests The number of thaw requests to fulfill. If set to 0, all thaw requests are fulfilled. + * @param thawingNonce The current valid thawing nonce. Any thaw request with a different nonce is invalid and should be ignored. + */ + struct FulfillThawRequestsParams { + ThawRequestType requestType; + address serviceProvider; + address verifier; + address owner; + uint256 tokensThawing; + uint256 sharesThawing; + uint256 nThawRequests; + uint256 thawingNonce; + } + + /** + * @notice Results of the traversal of thaw requests. + * @dev This struct is used to avoid stack too deep error in the `fulfillThawRequests` function. + * @param requestsFulfilled The number of thaw requests fulfilled + * @param tokensThawed The total amount of tokens thawed + * @param tokensThawing The total amount of tokens thawing + * @param sharesThawing The total amount of shares thawing + */ + struct TraverseThawRequestsResults { + uint256 requestsFulfilled; + uint256 tokensThawed; + uint256 tokensThawing; + uint256 sharesThawing; + } +} diff --git a/packages/horizon/contracts/libraries/Denominations.sol b/packages/horizon/contracts/libraries/Denominations.sol new file mode 100644 index 000000000..46cff3516 --- /dev/null +++ b/packages/horizon/contracts/libraries/Denominations.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title Denominations library + * @dev Provides a list of ground denominations for those tokens that cannot be represented by an ERC20. + * For now, the only needed is the native token that could be ETH, MATIC, or other depending on the layer being operated. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library Denominations { + /// @notice The address of the native token, i.e ETH + /// @dev This convention is taken from https://eips.ethereum.org/EIPS/eip-7528 + address internal constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /** + * @notice Checks if a token is the native token + * @param token The token address to check + * @return True if the token is the native token, false otherwise + */ + function isNativeToken(address token) internal pure returns (bool) { + return token == NATIVE_TOKEN; + } +} diff --git a/packages/horizon/contracts/libraries/LibFixedMath.sol b/packages/horizon/contracts/libraries/LibFixedMath.sol new file mode 100644 index 000000000..704617b40 --- /dev/null +++ b/packages/horizon/contracts/libraries/LibFixedMath.sol @@ -0,0 +1,243 @@ +/* + + Copyright 2017 Bprotocol Foundation, 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.27; + +/** + * @title LibFixedMath + * @notice This library provides fixed-point arithmetic operations. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library LibFixedMath { + // 1 + int256 private constant FIXED_1 = int256(0x0000000000000000000000000000000080000000000000000000000000000000); + // 2**255 + int256 private constant MIN_FIXED_VAL = type(int256).min; + // 0 + int256 private constant EXP_MAX_VAL = 0; + // -63.875 + int256 private constant EXP_MIN_VAL = -int256(0x0000000000000000000000000000001ff0000000000000000000000000000000); + + /// @dev Get one as a fixed-point number. + function one() internal pure returns (int256 f) { + f = FIXED_1; + } + + /// @dev Returns the addition of two fixed point numbers, reverting on overflow. + function sub(int256 a, int256 b) internal pure returns (int256 c) { + if (b == MIN_FIXED_VAL) { + revert("out-of-bounds"); + } + c = _add(a, -b); + } + + /// @dev Returns the multiplication of two fixed point numbers, reverting on overflow. + function mul(int256 a, int256 b) internal pure returns (int256 c) { + c = _mul(a, b) / FIXED_1; + } + + /// @dev Performs (a * n) / d, without scaling for precision. + function mulDiv(int256 a, int256 n, int256 d) internal pure returns (int256 c) { + c = _div(_mul(a, n), d); + } + + /// @dev Returns the unsigned integer result of multiplying a fixed-point + /// number with an integer, reverting if the multiplication overflows. + /// Negative results are clamped to zero. + function uintMul(int256 f, uint256 u) internal pure returns (uint256) { + if (int256(u) < int256(0)) { + revert("out-of-bounds"); + } + int256 c = _mul(f, int256(u)); + if (c <= 0) { + return 0; + } + return uint256(uint256(c) >> 127); + } + + /// @dev Convert signed `n` / `d` to a fixed-point number. + function toFixed(int256 n, int256 d) internal pure returns (int256 f) { + f = _div(_mul(n, FIXED_1), d); + } + + /// @dev Convert a fixed-point number to an integer. + function toInteger(int256 f) internal pure returns (int256 n) { + return f / FIXED_1; + } + + /// @dev Compute the natural exponent for a fixed-point number EXP_MIN_VAL <= `x` <= 1 + function exp(int256 x) internal pure returns (int256 r) { + if (x < EXP_MIN_VAL) { + // Saturate to zero below EXP_MIN_VAL. + return 0; + } + if (x == 0) { + return FIXED_1; + } + if (x > EXP_MAX_VAL) { + revert("out-of-bounds"); + } + + // Rewrite the input as a product of natural exponents and a + // single residual q, where q is a number of small magnitude. + // For example: e^-34.419 = e^(-32 - 2 - 0.25 - 0.125 - 0.044) + // = e^-32 * e^-2 * e^-0.25 * e^-0.125 * e^-0.044 + // -> q = -0.044 + + // Multiply with the taylor series for e^q + int256 y; + int256 z; + // q = x % 0.125 (the residual) + z = y = x % 0x0000000000000000000000000000000010000000000000000000000000000000; + z = (z * y) / FIXED_1; + r += z * 0x10e1b3be415a0000; // add y^02 * (20! / 02!) + z = (z * y) / FIXED_1; + r += z * 0x05a0913f6b1e0000; // add y^03 * (20! / 03!) + z = (z * y) / FIXED_1; + r += z * 0x0168244fdac78000; // add y^04 * (20! / 04!) + z = (z * y) / FIXED_1; + r += z * 0x004807432bc18000; // add y^05 * (20! / 05!) + z = (z * y) / FIXED_1; + r += z * 0x000c0135dca04000; // add y^06 * (20! / 06!) + z = (z * y) / FIXED_1; + r += z * 0x0001b707b1cdc000; // add y^07 * (20! / 07!) + z = (z * y) / FIXED_1; + r += z * 0x000036e0f639b800; // add y^08 * (20! / 08!) + z = (z * y) / FIXED_1; + r += z * 0x00000618fee9f800; // add y^09 * (20! / 09!) + z = (z * y) / FIXED_1; + r += z * 0x0000009c197dcc00; // add y^10 * (20! / 10!) + z = (z * y) / FIXED_1; + r += z * 0x0000000e30dce400; // add y^11 * (20! / 11!) + z = (z * y) / FIXED_1; + r += z * 0x000000012ebd1300; // add y^12 * (20! / 12!) + z = (z * y) / FIXED_1; + r += z * 0x0000000017499f00; // add y^13 * (20! / 13!) + z = (z * y) / FIXED_1; + r += z * 0x0000000001a9d480; // add y^14 * (20! / 14!) + z = (z * y) / FIXED_1; + r += z * 0x00000000001c6380; // add y^15 * (20! / 15!) + z = (z * y) / FIXED_1; + r += z * 0x000000000001c638; // add y^16 * (20! / 16!) + z = (z * y) / FIXED_1; + r += z * 0x0000000000001ab8; // add y^17 * (20! / 17!) + z = (z * y) / FIXED_1; + r += z * 0x000000000000017c; // add y^18 * (20! / 18!) + z = (z * y) / FIXED_1; + r += z * 0x0000000000000014; // add y^19 * (20! / 19!) + z = (z * y) / FIXED_1; + r += z * 0x0000000000000001; // add y^20 * (20! / 20!) + r = r / 0x21c3677c82b40000 + y + FIXED_1; // divide by 20! and then add y^1 / 1! + y^0 / 0! + + // Multiply with the non-residual terms. + x = -x; + // e ^ -32 + if ((x & int256(0x0000000000000000000000000000001000000000000000000000000000000000)) != 0) { + r = + (r * int256(0x00000000000000000000000000000000000000f1aaddd7742e56d32fb9f99744)) / + int256(0x0000000000000000000000000043cbaf42a000812488fc5c220ad7b97bf6e99e); // * e ^ -32 + } + // e ^ -16 + if ((x & int256(0x0000000000000000000000000000000800000000000000000000000000000000)) != 0) { + r = + (r * int256(0x00000000000000000000000000000000000afe10820813d65dfe6a33c07f738f)) / + int256(0x000000000000000000000000000005d27a9f51c31b7c2f8038212a0574779991); // * e ^ -16 + } + // e ^ -8 + if ((x & int256(0x0000000000000000000000000000000400000000000000000000000000000000)) != 0) { + r = + (r * int256(0x0000000000000000000000000000000002582ab704279e8efd15e0265855c47a)) / + int256(0x0000000000000000000000000000001b4c902e273a58678d6d3bfdb93db96d02); // * e ^ -8 + } + // e ^ -4 + if ((x & int256(0x0000000000000000000000000000000200000000000000000000000000000000)) != 0) { + r = + (r * int256(0x000000000000000000000000000000001152aaa3bf81cb9fdb76eae12d029571)) / + int256(0x00000000000000000000000000000003b1cc971a9bb5b9867477440d6d157750); // * e ^ -4 + } + // e ^ -2 + if ((x & int256(0x0000000000000000000000000000000100000000000000000000000000000000)) != 0) { + r = + (r * int256(0x000000000000000000000000000000002f16ac6c59de6f8d5d6f63c1482a7c86)) / + int256(0x000000000000000000000000000000015bf0a8b1457695355fb8ac404e7a79e3); // * e ^ -2 + } + // e ^ -1 + if ((x & int256(0x0000000000000000000000000000000080000000000000000000000000000000)) != 0) { + r = + (r * int256(0x000000000000000000000000000000004da2cbf1be5827f9eb3ad1aa9866ebb3)) / + int256(0x00000000000000000000000000000000d3094c70f034de4b96ff7d5b6f99fcd8); // * e ^ -1 + } + // e ^ -0.5 + if ((x & int256(0x0000000000000000000000000000000040000000000000000000000000000000)) != 0) { + r = + (r * int256(0x0000000000000000000000000000000063afbe7ab2082ba1a0ae5e4eb1b479dc)) / + int256(0x00000000000000000000000000000000a45af1e1f40c333b3de1db4dd55f29a7); // * e ^ -0.5 + } + // e ^ -0.25 + if ((x & int256(0x0000000000000000000000000000000020000000000000000000000000000000)) != 0) { + r = + (r * int256(0x0000000000000000000000000000000070f5a893b608861e1f58934f97aea57d)) / + int256(0x00000000000000000000000000000000910b022db7ae67ce76b441c27035c6a1); // * e ^ -0.25 + } + // e ^ -0.125 + if ((x & int256(0x0000000000000000000000000000000010000000000000000000000000000000)) != 0) { + r = + (r * int256(0x00000000000000000000000000000000783eafef1c0a8f3978c7f81824d62ebf)) / + int256(0x0000000000000000000000000000000088415abbe9a76bead8d00cf112e4d4a8); // * e ^ -0.125 + } + } + + /// @dev Returns the multiplication two numbers, reverting on overflow. + function _mul(int256 a, int256 b) private pure returns (int256 c) { + if (a == 0 || b == 0) { + return 0; + } + unchecked { + c = a * b; + if (c / a != b || c / b != a) { + revert("overflow"); + } + } + } + + /// @dev Returns the division of two numbers, reverting on division by zero. + function _div(int256 a, int256 b) private pure returns (int256 c) { + if (b == 0) { + revert("overflow"); + } + if (a == MIN_FIXED_VAL && b == -1) { + revert("overflow"); + } + unchecked { + c = a / b; + } + } + + /// @dev Adds two numbers, reverting on overflow. + function _add(int256 a, int256 b) private pure returns (int256 c) { + unchecked { + c = a + b; + if ((a < 0 && b < 0 && c > a) || (a > 0 && b > 0 && c < a)) { + revert("overflow"); + } + } + } +} diff --git a/packages/horizon/contracts/libraries/LinkedList.sol b/packages/horizon/contracts/libraries/LinkedList.sol new file mode 100644 index 000000000..af0f1dad9 --- /dev/null +++ b/packages/horizon/contracts/libraries/LinkedList.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +/** + * @title LinkedList library + * @notice A library to manage singly linked lists. + * + * The library makes no assumptions about the contents of the items, the only + * requirements on the items are: + * - they must be represented by a unique bytes32 id + * - the id of the item must not be bytes32(0) + * - each item must have a reference to the next item in the list + * - the list cannot have more than `MAX_ITEMS` items + * + * A contract using this library must store: + * - a LinkedList.List to keep track of the list metadata + * - a mapping from bytes32 to the item data + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library LinkedList { + using LinkedList for List; + + /** + * @notice Represents a linked list + * @param head The head of the list + * @param tail The tail of the list + * @param nonce A nonce, which can optionally be used to generate unique ids + * @param count The number of items in the list + */ + struct List { + bytes32 head; + bytes32 tail; + uint256 nonce; + uint256 count; + } + + /// @notice Empty bytes constant + bytes internal constant NULL_BYTES = bytes(""); + + /// @notice Maximum amount of items allowed in the list + uint256 internal constant MAX_ITEMS = 10_000; + + /** + * @notice Thrown when trying to remove an item from an empty list + */ + error LinkedListEmptyList(); + + /** + * @notice Thrown when trying to add an item to a list that has reached the maximum number of elements + */ + error LinkedListMaxElementsExceeded(); + + /** + * @notice Thrown when trying to traverse a list with more iterations than elements + */ + error LinkedListInvalidIterations(); + + /** + * @notice Thrown when trying to add an item with id equal to bytes32(0) + */ + error LinkedListInvalidZeroId(); + + /** + * @notice Adds an item to the list. + * The item is added to the end of the list. + * @dev Note that this function will not take care of linking the + * old tail to the new item. The caller should take care of this. + * It will also not ensure id uniqueness. + * @dev There is a maximum number of elements that can be added to the list. + * @param self The list metadata + * @param id The id of the item to add + */ + function addTail(List storage self, bytes32 id) internal { + require(self.count < MAX_ITEMS, LinkedListMaxElementsExceeded()); + require(id != bytes32(0), LinkedListInvalidZeroId()); + self.tail = id; + self.nonce += 1; + if (self.count == 0) self.head = id; + self.count += 1; + } + + /** + * @notice Removes an item from the list. + * The item is removed from the beginning of the list. + * @param self The list metadata + * @param getNextItem A function to get the next item in the list. It should take + * the id of the current item and return the id of the next item. + * @param deleteItem A function to delete an item. This should delete the item from + * the contract storage. It takes the id of the item to delete. + * @return The id of the head of the list. + */ + function removeHead( + List storage self, + function(bytes32) view returns (bytes32) getNextItem, + function(bytes32) deleteItem + ) internal returns (bytes32) { + require(self.count > 0, LinkedListEmptyList()); + bytes32 nextItem = getNextItem(self.head); + deleteItem(self.head); + self.count -= 1; + self.head = nextItem; + if (self.count == 0) self.tail = bytes32(0); + return self.head; + } + + /** + * @notice Traverses the list and processes each item. + * It deletes the processed items from both the list and the storage mapping. + * @param self The list metadata + * @param getNextItem A function to get the next item in the list. It should take + * the id of the current item and return the id of the next item. + * @param processItem A function to process an item. The function should take the id of the item + * and an accumulator, and return: + * - a boolean indicating whether the traversal should stop + * - an accumulator to pass data between iterations + * @param deleteItem A function to delete an item. This should delete the item from + * the contract storage. It takes the id of the item to delete. + * @param processInitAcc The initial accumulator data + * @param iterations The maximum number of iterations to perform. If 0, the traversal will continue + * until the end of the list. + * @return The number of items processed + * @return The final accumulator data. + */ + function traverse( + List storage self, + function(bytes32) view returns (bytes32) getNextItem, + function(bytes32, bytes memory) returns (bool, bytes memory) processItem, + function(bytes32) deleteItem, + bytes memory processInitAcc, + uint256 iterations + ) internal returns (uint256, bytes memory) { + require(iterations <= self.count, LinkedListInvalidIterations()); + + uint256 itemCount = 0; + iterations = (iterations == 0) ? self.count : iterations; + + bytes32 cursor = self.head; + + while (cursor != bytes32(0) && iterations > 0) { + (bool shouldBreak, bytes memory acc_) = processItem(cursor, processInitAcc); + + if (shouldBreak) break; + + processInitAcc = acc_; + cursor = self.removeHead(getNextItem, deleteItem); + + iterations--; + itemCount++; + } + + return (itemCount, processInitAcc); + } +} diff --git a/packages/horizon/contracts/libraries/MathUtils.sol b/packages/horizon/contracts/libraries/MathUtils.sol new file mode 100644 index 000000000..7b023e556 --- /dev/null +++ b/packages/horizon/contracts/libraries/MathUtils.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +/** + * @title MathUtils Library + * @notice A collection of functions to perform math operations + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library MathUtils { + /** + * @dev Calculates the weighted average of two values pondering each of these + * values based on configured weights. The contribution of each value N is + * weightN/(weightA + weightB). The calculation rounds up to ensure the result + * is always greater than the smallest of the two values. + * @param valueA The amount for value A + * @param weightA The weight to use for value A + * @param valueB The amount for value B + * @param weightB The weight to use for value B + */ + function weightedAverageRoundingUp( + uint256 valueA, + uint256 weightA, + uint256 valueB, + uint256 weightB + ) internal pure returns (uint256) { + return ((valueA * weightA) + (valueB * weightB) + (weightA + weightB - 1)) / (weightA + weightB); + } + + /** + * @dev Returns the minimum of two numbers. + * @param x The first number + * @param y The second number + * @return The minimum of the two numbers + */ + function min(uint256 x, uint256 y) internal pure returns (uint256) { + return x <= y ? x : y; + } + + /** + * @dev Returns the difference between two numbers or zero if negative. + * @param x The first number + * @param y The second number + * @return The difference between the two numbers or zero if negative + */ + function diffOrZero(uint256 x, uint256 y) internal pure returns (uint256) { + return (x > y) ? x - y : 0; + } +} diff --git a/packages/horizon/contracts/libraries/PPMMath.sol b/packages/horizon/contracts/libraries/PPMMath.sol new file mode 100644 index 000000000..a7966c91d --- /dev/null +++ b/packages/horizon/contracts/libraries/PPMMath.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title PPMMath library + * @notice A library for handling calculations with parts per million (PPM) amounts. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library PPMMath { + /// @notice Maximum value (100%) in parts per million (PPM). + uint256 internal constant MAX_PPM = 1_000_000; + + /** + * @notice Thrown when a value is expected to be in PPM but is not. + * @param value The value that is not in PPM. + */ + error PPMMathInvalidPPM(uint256 value); + + /** + * @notice Thrown when no value in a multiplication is in PPM. + * @param a The first value in the multiplication. + * @param b The second value in the multiplication. + */ + error PPMMathInvalidMulPPM(uint256 a, uint256 b); + + /** + * @notice Multiplies two values, one of which must be in PPM. + * @param a The first value. + * @param b The second value. + * @return The result of the multiplication. + */ + function mulPPM(uint256 a, uint256 b) internal pure returns (uint256) { + require(isValidPPM(a) || isValidPPM(b), PPMMathInvalidMulPPM(a, b)); + return (a * b) / MAX_PPM; + } + + /** + * @notice Multiplies two values, the second one must be in PPM, and rounds up the result. + * @dev requirements: + * - The second value must be in PPM. + * @param a The first value. + * @param b The second value. + * @return The result of the multiplication. + */ + function mulPPMRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { + require(isValidPPM(b), PPMMathInvalidPPM(b)); + return a - mulPPM(a, MAX_PPM - b); + } + + /** + * @notice Checks if a value is in PPM. + * @dev A valid PPM value is between 0 and MAX_PPM. + * @param value The value to check. + * @return true if the value is in PPM, false otherwise. + */ + function isValidPPM(uint256 value) internal pure returns (bool) { + return value <= MAX_PPM; + } +} diff --git a/packages/horizon/contracts/libraries/UintRange.sol b/packages/horizon/contracts/libraries/UintRange.sol new file mode 100644 index 000000000..54552b549 --- /dev/null +++ b/packages/horizon/contracts/libraries/UintRange.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title UintRange library + * @notice A library for handling range checks on uint256 values. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library UintRange { + using UintRange for uint256; + + /** + * @notice Checks if a value is in the range [`min`, `max`]. + * @param value The value to check. + * @param min The minimum value of the range. + * @param max The maximum value of the range. + * @return true if the value is in the range, false otherwise. + */ + function isInRange(uint256 value, uint256 min, uint256 max) internal pure returns (bool) { + return value >= min && value <= max; + } +} diff --git a/packages/horizon/contracts/mocks/ControllerMock.sol b/packages/horizon/contracts/mocks/ControllerMock.sol new file mode 100644 index 000000000..54c3ec8db --- /dev/null +++ b/packages/horizon/contracts/mocks/ControllerMock.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IController } from "@graphprotocol/contracts/contracts/governance/IController.sol"; +import { IManaged } from "@graphprotocol/contracts/contracts/governance/IManaged.sol"; + +/** + * @title Graph Controller contract (mock) + * @dev Controller is a registry of contracts for convenience. Inspired by Livepeer: + * https://github.com/livepeer/protocol/blob/streamflow/contracts/Controller.sol + */ +contract ControllerMock is IController { + /// @dev Track contract ids to contract proxy address + mapping(bytes32 contractName => address contractAddress) private _registry; + address public governor; + bool internal _paused; + bool internal _partialPaused; + address internal _pauseGuardian; + + /// Emitted when the proxy address for a protocol contract has been set + event SetContractProxy(bytes32 indexed id, address contractAddress); + + /** + * Constructor for the Controller mock + * @param governor_ Address of the governor + */ + constructor(address governor_) { + governor = governor_; + } + + // -- Registry -- + + /** + * @notice Register contract id and mapped address + * @param id Contract id (keccak256 hash of contract name) + * @param contractAddress Contract address + */ + function setContractProxy(bytes32 id, address contractAddress) external override { + require(contractAddress != address(0), "Contract address must be set"); + _registry[id] = contractAddress; + emit SetContractProxy(id, contractAddress); + } + + /** + * @notice Unregister a contract address + * @param id Contract id (keccak256 hash of contract name) + */ + function unsetContractProxy(bytes32 id) external override { + _registry[id] = address(0); + emit SetContractProxy(id, address(0)); + } + + /** + * @notice Update a contract's controller + * @param id Contract id (keccak256 hash of contract name) + * @param controller New Controller address + */ + function updateController(bytes32 id, address controller) external override { + require(controller != address(0), "Controller must be set"); + return IManaged(_registry[id]).setController(controller); + } + + // -- Pausing -- + + /** + * @notice Change the partial paused state of the contract + * Partial pause is intended as a partial pause of the protocol + * @param toPause True if the contracts should be (partially) paused, false otherwise + */ + function setPartialPaused(bool toPause) external override { + _partialPaused = toPause; + } + + /** + * @notice Change the paused state of the contract + * Full pause most of protocol functions + * @param toPause True if the contracts should be paused, false otherwise + */ + function setPaused(bool toPause) external override { + _paused = toPause; + } + + /** + * @notice Change the Pause Guardian + * @param newPauseGuardian The address of the new Pause Guardian + */ + function setPauseGuardian(address newPauseGuardian) external override { + require(newPauseGuardian != address(0), "PauseGuardian must be set"); + _pauseGuardian = newPauseGuardian; + } + + /** + * @notice Getter to access governor + * @return Address of the governor + */ + function getGovernor() external view override returns (address) { + return governor; + } + + /** + * @notice Get contract proxy address by its id + * @param id Contract id (keccak256 hash of contract name) + * @return Address of the proxy contract for the provided id + */ + function getContractProxy(bytes32 id) external view virtual override returns (address) { + return _registry[id]; + } + + /** + * @notice Getter to access paused + * @return True if the contracts are paused, false otherwise + */ + function paused() external view override returns (bool) { + return _paused; + } + + /** + * @notice Getter to access partial pause status + * @return True if the contracts are partially paused, false otherwise + */ + function partialPaused() external view override returns (bool) { + return _partialPaused; + } +} diff --git a/packages/horizon/contracts/mocks/CurationMock.sol b/packages/horizon/contracts/mocks/CurationMock.sol new file mode 100644 index 000000000..996f971b1 --- /dev/null +++ b/packages/horizon/contracts/mocks/CurationMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { MockGRTToken } from "./MockGRTToken.sol"; + +contract CurationMock { + mapping(bytes32 => uint256) public curation; + + function signal(bytes32 _subgraphDeploymentID, uint256 _tokens) public { + curation[_subgraphDeploymentID] += _tokens; + } + + function isCurated(bytes32 _subgraphDeploymentID) public view returns (bool) { + return curation[_subgraphDeploymentID] != 0; + } + + function collect(bytes32 _subgraphDeploymentID, uint256 _tokens) external { + curation[_subgraphDeploymentID] += _tokens; + } +} diff --git a/packages/horizon/contracts/mocks/Dummy.sol b/packages/horizon/contracts/mocks/Dummy.sol new file mode 100644 index 000000000..e6a575d0f --- /dev/null +++ b/packages/horizon/contracts/mocks/Dummy.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +contract Dummy {} diff --git a/packages/horizon/contracts/mocks/EpochManagerMock.sol b/packages/horizon/contracts/mocks/EpochManagerMock.sol new file mode 100644 index 000000000..12f694a5e --- /dev/null +++ b/packages/horizon/contracts/mocks/EpochManagerMock.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IEpochManager } from "@graphprotocol/contracts/contracts/epochs/IEpochManager.sol"; + +contract EpochManagerMock is IEpochManager { + // -- Variables -- + + uint256 public epochLength; + uint256 public lastRunEpoch; + uint256 public lastLengthUpdateEpoch; + uint256 public lastLengthUpdateBlock; + + // -- Configuration -- + + function setEpochLength(uint256 _epochLength) public { + lastLengthUpdateEpoch = 1; + lastLengthUpdateBlock = blockNum(); + epochLength = _epochLength; + } + + // -- Epochs + + function runEpoch() public { + lastRunEpoch = currentEpoch(); + } + + // -- Getters -- + + function isCurrentEpochRun() public view returns (bool) { + return lastRunEpoch == currentEpoch(); + } + + function blockNum() public view returns (uint256) { + return block.number; + } + + function blockHash(uint256 _block) public view returns (bytes32) { + return blockhash(_block); + } + + function currentEpoch() public view returns (uint256) { + return lastLengthUpdateEpoch + epochsSinceUpdate(); + } + + function currentEpochBlock() public view returns (uint256) { + return lastLengthUpdateBlock + (epochsSinceUpdate() * epochLength); + } + + function currentEpochBlockSinceStart() public view returns (uint256) { + return blockNum() - currentEpochBlock(); + } + + function epochsSince(uint256 _epoch) public view returns (uint256) { + uint256 epoch = currentEpoch(); + return _epoch < epoch ? (epoch - _epoch) : 0; + } + + function epochsSinceUpdate() public view returns (uint256) { + return (blockNum() - lastLengthUpdateBlock) / epochLength; + } +} diff --git a/packages/horizon/contracts/mocks/MockGRTToken.sol b/packages/horizon/contracts/mocks/MockGRTToken.sol new file mode 100644 index 000000000..235999ae5 --- /dev/null +++ b/packages/horizon/contracts/mocks/MockGRTToken.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; + +contract MockGRTToken is ERC20, IGraphToken { + constructor() ERC20("Graph Token", "GRT") {} + + function burn(uint256 tokens) external { + _burn(msg.sender, tokens); + } + + function burnFrom(address from, uint256 tokens) external { + _burn(from, tokens); + } + + // -- Mint Admin -- + + function addMinter(address account) external {} + + function removeMinter(address account) external {} + + function renounceMinter() external {} + + // -- Permit -- + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external {} + + // -- Allowance -- + + function increaseAllowance(address spender, uint256 addedValue) external returns (bool) {} + + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) {} + + function isMinter(address account) external view returns (bool) {} + + function mint(address to, uint256 tokens) public { + _mint(to, tokens); + } +} diff --git a/packages/horizon/contracts/mocks/RewardsManagerMock.sol b/packages/horizon/contracts/mocks/RewardsManagerMock.sol new file mode 100644 index 000000000..272584ca4 --- /dev/null +++ b/packages/horizon/contracts/mocks/RewardsManagerMock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { MockGRTToken } from "./MockGRTToken.sol"; + +contract RewardsManagerMock { + // -- Variables -- + MockGRTToken public token; + uint256 private rewards; + + // -- Constructor -- + + constructor(MockGRTToken _token, uint256 _rewards) { + token = _token; + rewards = _rewards; + } + + function takeRewards(address) external returns (uint256) { + token.mint(msg.sender, rewards); + return rewards; + } + + function onSubgraphAllocationUpdate(bytes32) public returns (uint256) {} + function onSubgraphSignalUpdate(bytes32 _subgraphDeploymentID) external returns (uint256) {} +} diff --git a/packages/horizon/contracts/mocks/imports.sol b/packages/horizon/contracts/mocks/imports.sol new file mode 100644 index 000000000..115dc91a2 --- /dev/null +++ b/packages/horizon/contracts/mocks/imports.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +// We import these here to force Hardhat to compile them. +// This ensures that their artifacts are available for Hardhat Ignition to use. +pragma solidity 0.8.27; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol new file mode 100644 index 000000000..5bfd993e9 --- /dev/null +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "../interfaces/internal/IHorizonStakingTypes.sol"; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; +import { PPMMath } from "../libraries/PPMMath.sol"; + +import { GraphDirectory } from "../utilities/GraphDirectory.sol"; + +/** + * @title GraphPayments contract + * @notice This contract is part of the Graph Horizon payments protocol. It's designed + * to pull funds (GRT) from the {PaymentsEscrow} and distribute them according to a + * set of pre established rules. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, IGraphPayments { + using TokenUtils for IGraphToken; + using PPMMath for uint256; + + /// @notice Protocol payment cut in PPM + uint256 public immutable PROTOCOL_PAYMENT_CUT; + + /** + * @notice Constructor for the {GraphPayments} contract + * @dev This contract is upgradeable however we still use the constructor to set + * a few immutable variables. + * @param controller The address of the Graph controller + * @param protocolPaymentCut The protocol tax in PPM + */ + constructor(address controller, uint256 protocolPaymentCut) GraphDirectory(controller) { + require(PPMMath.isValidPPM(protocolPaymentCut), GraphPaymentsInvalidCut(protocolPaymentCut)); + PROTOCOL_PAYMENT_CUT = protocolPaymentCut; + _disableInitializers(); + } + + /// @inheritdoc IGraphPayments + function initialize() external initializer { + __Multicall_init(); + } + + /// @inheritdoc IGraphPayments + function collect( + IGraphPayments.PaymentTypes paymentType, + address receiver, + uint256 tokens, + address dataService, + uint256 dataServiceCut + ) external { + require(PPMMath.isValidPPM(dataServiceCut), GraphPaymentsInvalidCut(dataServiceCut)); + + // Pull tokens from the sender + _graphToken().pullTokens(msg.sender, tokens); + + // Calculate token amounts for each party + // Order matters: protocol -> data service -> delegators -> receiver + // Note the substractions should not underflow as we are only deducting a percentage of the remainder + uint256 tokensRemaining = tokens; + + uint256 tokensProtocol = tokensRemaining.mulPPMRoundUp(PROTOCOL_PAYMENT_CUT); + tokensRemaining = tokensRemaining - tokensProtocol; + + uint256 tokensDataService = tokensRemaining.mulPPMRoundUp(dataServiceCut); + tokensRemaining = tokensRemaining - tokensDataService; + + uint256 tokensDelegationPool = 0; + IHorizonStakingTypes.DelegationPool memory pool = _graphStaking().getDelegationPool(receiver, dataService); + if (pool.shares > 0) { + tokensDelegationPool = tokensRemaining.mulPPMRoundUp( + _graphStaking().getDelegationFeeCut(receiver, dataService, paymentType) + ); + tokensRemaining = tokensRemaining - tokensDelegationPool; + } + + // Pay all parties + _graphToken().burnTokens(tokensProtocol); + + _graphToken().pushTokens(dataService, tokensDataService); + + if (tokensDelegationPool > 0) { + _graphToken().approve(address(_graphStaking()), tokensDelegationPool); + _graphStaking().addToDelegationPool(receiver, dataService, tokensDelegationPool); + } + + _graphToken().pushTokens(receiver, tokensRemaining); + + emit GraphPaymentCollected( + paymentType, + msg.sender, + receiver, + dataService, + tokens, + tokensProtocol, + tokensDataService, + tokensDelegationPool, + tokensRemaining + ); + } +} diff --git a/packages/horizon/contracts/payments/PaymentsEscrow.sol b/packages/horizon/contracts/payments/PaymentsEscrow.sol new file mode 100644 index 000000000..c53a7a56e --- /dev/null +++ b/packages/horizon/contracts/payments/PaymentsEscrow.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; +import { IPaymentsEscrow } from "../interfaces/IPaymentsEscrow.sol"; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; + +import { GraphDirectory } from "../utilities/GraphDirectory.sol"; + +/** + * @title PaymentsEscrow contract + * @dev Implements the {IPaymentsEscrow} interface + * @notice This contract is part of the Graph Horizon payments protocol. It holds the funds (GRT) + * for payments made through the payments protocol for services provided + * via a Graph Horizon data service. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, IPaymentsEscrow { + using TokenUtils for IGraphToken; + + /// @notice The maximum thawing period (in seconds) for both escrow withdrawal and collector revocation + /// @dev This is a precautionary measure to avoid inadvertedly locking funds for too long + uint256 public constant MAX_WAIT_PERIOD = 90 days; + + /// @notice Thawing period in seconds for escrow funds withdrawal + uint256 public immutable WITHDRAW_ESCROW_THAWING_PERIOD; + + /// @notice Escrow account details for payer-collector-receiver tuples + mapping(address payer => mapping(address collector => mapping(address receiver => IPaymentsEscrow.EscrowAccount escrowAccount))) + public escrowAccounts; + + /** + * @notice Modifier to prevent function execution when contract is paused + * @dev Reverts if the controller indicates the contract is paused + */ + modifier notPaused() { + require(!_graphController().paused(), PaymentsEscrowIsPaused()); + _; + } + + /** + * @notice Construct the PaymentsEscrow contract + * @param controller The address of the controller + * @param withdrawEscrowThawingPeriod Thawing period in seconds for escrow funds withdrawal + */ + constructor(address controller, uint256 withdrawEscrowThawingPeriod) GraphDirectory(controller) { + require( + withdrawEscrowThawingPeriod <= MAX_WAIT_PERIOD, + PaymentsEscrowThawingPeriodTooLong(withdrawEscrowThawingPeriod, MAX_WAIT_PERIOD) + ); + + WITHDRAW_ESCROW_THAWING_PERIOD = withdrawEscrowThawingPeriod; + _disableInitializers(); + } + + /// @inheritdoc IPaymentsEscrow + function initialize() external initializer { + __Multicall_init(); + } + + /// @inheritdoc IPaymentsEscrow + function deposit(address collector, address receiver, uint256 tokens) external override notPaused { + _deposit(msg.sender, collector, receiver, tokens); + } + + /// @inheritdoc IPaymentsEscrow + function depositTo(address payer, address collector, address receiver, uint256 tokens) external override notPaused { + _deposit(payer, collector, receiver, tokens); + } + + /// @inheritdoc IPaymentsEscrow + function thaw(address collector, address receiver, uint256 tokens) external override notPaused { + require(tokens > 0, PaymentsEscrowInvalidZeroTokens()); + + EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver]; + require(account.balance >= tokens, PaymentsEscrowInsufficientBalance(account.balance, tokens)); + + account.tokensThawing = tokens; + account.thawEndTimestamp = block.timestamp + WITHDRAW_ESCROW_THAWING_PERIOD; + + emit Thaw(msg.sender, collector, receiver, tokens, account.thawEndTimestamp); + } + + /// @inheritdoc IPaymentsEscrow + function cancelThaw(address collector, address receiver) external override notPaused { + EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver]; + require(account.tokensThawing != 0, PaymentsEscrowNotThawing()); + + uint256 tokensThawing = account.tokensThawing; + uint256 thawEndTimestamp = account.thawEndTimestamp; + account.tokensThawing = 0; + account.thawEndTimestamp = 0; + + emit CancelThaw(msg.sender, collector, receiver, tokensThawing, thawEndTimestamp); + } + + /// @inheritdoc IPaymentsEscrow + function withdraw(address collector, address receiver) external override notPaused { + EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver]; + require(account.thawEndTimestamp != 0, PaymentsEscrowNotThawing()); + require( + account.thawEndTimestamp < block.timestamp, + PaymentsEscrowStillThawing(block.timestamp, account.thawEndTimestamp) + ); + + // Amount is the minimum between the amount being thawed and the actual balance + uint256 tokens = account.tokensThawing > account.balance ? account.balance : account.tokensThawing; + + account.balance -= tokens; + account.tokensThawing = 0; + account.thawEndTimestamp = 0; + _graphToken().pushTokens(msg.sender, tokens); + emit Withdraw(msg.sender, collector, receiver, tokens); + } + + /// @inheritdoc IPaymentsEscrow + function collect( + IGraphPayments.PaymentTypes paymentType, + address payer, + address receiver, + uint256 tokens, + address dataService, + uint256 dataServiceCut + ) external override notPaused { + // Check if there are enough funds in the escrow account + EscrowAccount storage account = escrowAccounts[payer][msg.sender][receiver]; + require(account.balance >= tokens, PaymentsEscrowInsufficientBalance(account.balance, tokens)); + + // Reduce amount from account balance + account.balance -= tokens; + + uint256 escrowBalanceBefore = _graphToken().balanceOf(address(this)); + + _graphToken().approve(address(_graphPayments()), tokens); + _graphPayments().collect(paymentType, receiver, tokens, dataService, dataServiceCut); + + // Verify that the escrow balance is consistent with the collected tokens + uint256 escrowBalanceAfter = _graphToken().balanceOf(address(this)); + require( + escrowBalanceBefore == tokens + escrowBalanceAfter, + PaymentsEscrowInconsistentCollection(escrowBalanceBefore, escrowBalanceAfter, tokens) + ); + + emit EscrowCollected(paymentType, payer, msg.sender, receiver, tokens); + } + + /// @inheritdoc IPaymentsEscrow + function getBalance(address payer, address collector, address receiver) external view override returns (uint256) { + EscrowAccount storage account = escrowAccounts[payer][collector][receiver]; + return account.balance > account.tokensThawing ? account.balance - account.tokensThawing : 0; + } + + /** + * @notice Deposits funds into the escrow for a payer-collector-receiver tuple, where + * the payer is the transaction caller. + * @param _payer The address of the payer + * @param _collector The address of the collector + * @param _receiver The address of the receiver + * @param _tokens The amount of tokens to deposit + */ + function _deposit(address _payer, address _collector, address _receiver, uint256 _tokens) private { + escrowAccounts[_payer][_collector][_receiver].balance += _tokens; + _graphToken().pullTokens(msg.sender, _tokens); + emit Deposit(_payer, _collector, _receiver, _tokens); + } +} diff --git a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol new file mode 100644 index 000000000..73f887527 --- /dev/null +++ b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IGraphPayments } from "../../interfaces/IGraphPayments.sol"; +import { IGraphTallyCollector } from "../../interfaces/IGraphTallyCollector.sol"; +import { IPaymentsCollector } from "../../interfaces/IPaymentsCollector.sol"; + +import { Authorizable } from "../../utilities/Authorizable.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { PPMMath } from "../../libraries/PPMMath.sol"; + +import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/** + * @title GraphTallyCollector contract + * @dev Implements the {IGraphTallyCollector}, {IPaymentCollector} and {IAuthorizable} interfaces. + * @notice A payments collector contract that can be used to collect payments using a GraphTally RAV (Receipt Aggregate Voucher). + * @dev Note that the contract expects the RAV aggregate value to be monotonically increasing, each successive RAV for the same + * (data service-payer-receiver) tuple should have a value greater than the previous one. The contract will keep track of the tokens + * already collected and calculate the difference to collect. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTallyCollector { + using PPMMath for uint256; + + /// @notice The EIP712 typehash for the ReceiptAggregateVoucher struct + bytes32 private constant EIP712_RAV_TYPEHASH = + keccak256( + "ReceiptAggregateVoucher(address payer,address serviceProvider,address dataService,uint64 timestampNs,uint128 valueAggregate,bytes metadata)" + ); + + /// @notice Tracks the amount of tokens already collected by a data service from a payer to a receiver. + /// @dev The collectionId provides a secondary key for grouping payment tracking if needed. Data services that do not require + /// grouping can use the same collectionId for all payments (0x00 or some other default value). + mapping(address dataService => mapping(bytes32 collectionId => mapping(address receiver => mapping(address payer => uint256 tokens)))) + public tokensCollected; + + /** + * @notice Constructs a new instance of the GraphTallyCollector contract. + * @param eip712Name The name of the EIP712 domain. + * @param eip712Version The version of the EIP712 domain. + * @param controller The address of the Graph controller. + * @param revokeSignerThawingPeriod The duration (in seconds) in which a signer is thawing before they can be revoked. + */ + constructor( + string memory eip712Name, + string memory eip712Version, + address controller, + uint256 revokeSignerThawingPeriod + ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) Authorizable(revokeSignerThawingPeriod) {} + + /** + * @notice See {IGraphPayments.collect}. + * @dev Requirements: + * - Caller must be the data service the RAV was issued to. + * - Signer of the RAV must be authorized to sign for the payer. + * - Service provider must have an active provision with the data service to collect payments. + * @notice REVERT: This function may revert if ECDSA.recover fails, check ECDSA library for details. + */ + /// @inheritdoc IPaymentsCollector + function collect(IGraphPayments.PaymentTypes paymentType, bytes calldata data) external override returns (uint256) { + return _collect(paymentType, data, 0); + } + + /// @inheritdoc IGraphTallyCollector + function collect( + IGraphPayments.PaymentTypes paymentType, + bytes calldata data, + uint256 tokensToCollect + ) external override returns (uint256) { + return _collect(paymentType, data, tokensToCollect); + } + + /// @inheritdoc IGraphTallyCollector + function recoverRAVSigner(SignedRAV calldata signedRAV) external view override returns (address) { + return _recoverRAVSigner(signedRAV); + } + + /// @inheritdoc IGraphTallyCollector + function encodeRAV(ReceiptAggregateVoucher calldata rav) external view returns (bytes32) { + return _encodeRAV(rav); + } + + /** + * @notice See {IPaymentsCollector.collect} + * This variant adds the ability to partially collect a RAV by specifying the amount of tokens to collect. + * @param _paymentType The payment type to collect + * @param _data Additional data required for the payment collection + * @param _tokensToCollect The amount of tokens to collect + * @return The amount of tokens collected + */ + function _collect( + IGraphPayments.PaymentTypes _paymentType, + bytes calldata _data, + uint256 _tokensToCollect + ) private returns (uint256) { + (SignedRAV memory signedRAV, uint256 dataServiceCut) = abi.decode(_data, (SignedRAV, uint256)); + + // Ensure caller is the RAV data service + require( + signedRAV.rav.dataService == msg.sender, + GraphTallyCollectorCallerNotDataService(msg.sender, signedRAV.rav.dataService) + ); + + // Ensure RAV signer is authorized for the payer + _requireAuthorizedSigner(signedRAV); + + bytes32 collectionId = signedRAV.rav.collectionId; + address dataService = signedRAV.rav.dataService; + address receiver = signedRAV.rav.serviceProvider; + + // Check the service provider has an active provision with the data service + // This prevents an attack where the payer can deny the service provider from collecting payments + // by using a signer as data service to syphon off the tokens in the escrow to an account they control + { + uint256 tokensAvailable = _graphStaking().getProviderTokensAvailable( + signedRAV.rav.serviceProvider, + signedRAV.rav.dataService + ); + require(tokensAvailable > 0, GraphTallyCollectorUnauthorizedDataService(signedRAV.rav.dataService)); + } + + uint256 tokensToCollect = 0; + address payer = signedRAV.rav.payer; + { + uint256 tokensRAV = signedRAV.rav.valueAggregate; + uint256 tokensAlreadyCollected = tokensCollected[dataService][collectionId][receiver][payer]; + require( + tokensRAV > tokensAlreadyCollected, + GraphTallyCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected) + ); + + if (_tokensToCollect == 0) { + tokensToCollect = tokensRAV - tokensAlreadyCollected; + } else { + require( + _tokensToCollect <= tokensRAV - tokensAlreadyCollected, + GraphTallyCollectorInvalidTokensToCollectAmount( + _tokensToCollect, + tokensRAV - tokensAlreadyCollected + ) + ); + tokensToCollect = _tokensToCollect; + } + } + + if (tokensToCollect > 0) { + tokensCollected[dataService][collectionId][receiver][payer] += tokensToCollect; + _graphPaymentsEscrow().collect(_paymentType, payer, receiver, tokensToCollect, dataService, dataServiceCut); + } + + emit PaymentCollected(_paymentType, collectionId, payer, receiver, dataService, tokensToCollect); + + // This event is emitted to allow reconstructing RAV history with onchain data. + emit RAVCollected( + collectionId, + payer, + receiver, + dataService, + signedRAV.rav.timestampNs, + signedRAV.rav.valueAggregate, + signedRAV.rav.metadata, + signedRAV.signature + ); + + return tokensToCollect; + } + + /** + * @dev Recovers the signer address of a signed ReceiptAggregateVoucher (RAV). + * @param _signedRAV The SignedRAV containing the RAV and its signature. + * @return The address of the signer. + */ + function _recoverRAVSigner(SignedRAV memory _signedRAV) private view returns (address) { + bytes32 messageHash = _encodeRAV(_signedRAV.rav); + return ECDSA.recover(messageHash, _signedRAV.signature); + } + + /** + * @dev Computes the hash of a ReceiptAggregateVoucher (RAV). + * @param _rav The RAV for which to compute the hash. + * @return The hash of the RAV. + */ + function _encodeRAV(ReceiptAggregateVoucher memory _rav) private view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + EIP712_RAV_TYPEHASH, + _rav.payer, + _rav.serviceProvider, + _rav.dataService, + _rav.timestampNs, + _rav.valueAggregate, + keccak256(_rav.metadata) + ) + ) + ); + } + + /** + * @notice Reverts if the RAV signer is not authorized by the payer + * @param _signedRAV The signed RAV + */ + function _requireAuthorizedSigner(SignedRAV memory _signedRAV) private view { + require( + _isAuthorized(_signedRAV.rav.payer, _recoverRAVSigner(_signedRAV)), + GraphTallyCollectorInvalidRAVSigner() + ); + } +} diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol new file mode 100644 index 000000000..c495a7914 --- /dev/null +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -0,0 +1,1261 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IHorizonStakingMain } from "../interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingExtension } from "../interfaces/internal/IHorizonStakingExtension.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; + +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; +import { MathUtils } from "../libraries/MathUtils.sol"; +import { PPMMath } from "../libraries/PPMMath.sol"; +import { LinkedList } from "../libraries/LinkedList.sol"; + +import { HorizonStakingBase } from "./HorizonStakingBase.sol"; + +/** + * @title HorizonStaking contract + * @notice The {HorizonStaking} contract allows service providers to stake and provision tokens to verifiers to be used + * as economic security for a service. It also allows delegators to delegate towards a service provider provision. + * @dev Implements the {IHorizonStakingMain} interface. + * @dev This is the main Staking contract in The Graph protocol after the Horizon upgrade. + * It is designed to be deployed as an upgrade to the L2Staking contract from the legacy contracts package. + * @dev It uses a {HorizonStakingExtension} contract to implement the full {IHorizonStaking} interface through delegatecalls. + * This is due to the contract size limit on Arbitrum (24kB). The extension contract implements functionality to support + * the legacy staking functions. It can be eventually removed without affecting the main staking contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { + using TokenUtils for IGraphToken; + using PPMMath for uint256; + using LinkedList for LinkedList.List; + + /// @dev Fixed point precision + uint256 private constant FIXED_POINT_PRECISION = 1e18; + + /// @dev Maximum number of simultaneous stake thaw requests (per provision) or undelegations (per delegation) + uint256 private constant MAX_THAW_REQUESTS = 1_000; + + /// @dev Address of the staking extension contract + address private immutable STAKING_EXTENSION_ADDRESS; + + /// @dev Minimum amount of delegation. + uint256 private constant MIN_DELEGATION = 1e18; + + /** + * @notice Checks that the caller is authorized to operate over a provision. + * @param serviceProvider The address of the service provider. + * @param verifier The address of the verifier. + */ + modifier onlyAuthorized(address serviceProvider, address verifier) { + require( + _isAuthorized(serviceProvider, verifier, msg.sender), + HorizonStakingNotAuthorized(serviceProvider, verifier, msg.sender) + ); + _; + } + + /** + * @dev The staking contract is upgradeable however we still use the constructor to set + * a few immutable variables. + * @param controller The address of the Graph controller contract. + * @param stakingExtensionAddress The address of the staking extension contract. + * @param subgraphDataServiceAddress The address of the subgraph data service. + */ + constructor( + address controller, + address stakingExtensionAddress, + address subgraphDataServiceAddress + ) HorizonStakingBase(controller, subgraphDataServiceAddress) { + STAKING_EXTENSION_ADDRESS = stakingExtensionAddress; + } + + /** + * @notice Delegates the current call to the StakingExtension implementation. + * @dev This function does not return to its internal call site, it will return directly to the + * external caller. + */ + // solhint-disable-next-line payable-fallback, no-complex-fallback + fallback() external { + address extensionImpl = STAKING_EXTENSION_ADDRESS; + // solhint-disable-next-line no-inline-assembly + assembly { + // (a) get free memory pointer + let ptr := mload(0x40) + + // (1) copy incoming call data + calldatacopy(ptr, 0, calldatasize()) + + // (2) forward call to logic contract + let result := delegatecall(gas(), extensionImpl, ptr, calldatasize(), 0, 0) + let size := returndatasize() + + // (3) retrieve return data + returndatacopy(ptr, 0, size) + + // (4) forward return data back to caller + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } + } + + /* + * STAKING + */ + + /// @inheritdoc IHorizonStakingMain + function stake(uint256 tokens) external override notPaused { + _stakeTo(msg.sender, tokens); + } + + /// @inheritdoc IHorizonStakingMain + function stakeTo(address serviceProvider, uint256 tokens) external override notPaused { + _stakeTo(serviceProvider, tokens); + } + + /// @inheritdoc IHorizonStakingMain + function stakeToProvision(address serviceProvider, address verifier, uint256 tokens) external override notPaused { + _stakeTo(serviceProvider, tokens); + _addToProvision(serviceProvider, verifier, tokens); + } + + /// @inheritdoc IHorizonStakingMain + function unstake(uint256 tokens) external override notPaused { + _unstake(tokens); + } + + /// @inheritdoc IHorizonStakingMain + function withdraw() external override notPaused { + _withdraw(msg.sender); + } + + /* + * PROVISIONS + */ + + /// @inheritdoc IHorizonStakingMain + function provision( + address serviceProvider, + address verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) external override notPaused onlyAuthorized(serviceProvider, verifier) { + _createProvision(serviceProvider, tokens, verifier, maxVerifierCut, thawingPeriod); + } + + /// @inheritdoc IHorizonStakingMain + function addToProvision( + address serviceProvider, + address verifier, + uint256 tokens + ) external override notPaused onlyAuthorized(serviceProvider, verifier) { + _addToProvision(serviceProvider, verifier, tokens); + } + + /// @inheritdoc IHorizonStakingMain + function thaw( + address serviceProvider, + address verifier, + uint256 tokens + ) external override notPaused onlyAuthorized(serviceProvider, verifier) returns (bytes32) { + return _thaw(serviceProvider, verifier, tokens); + } + + /// @inheritdoc IHorizonStakingMain + function deprovision( + address serviceProvider, + address verifier, + uint256 nThawRequests + ) external override onlyAuthorized(serviceProvider, verifier) notPaused { + _deprovision(serviceProvider, verifier, nThawRequests); + } + + /// @inheritdoc IHorizonStakingMain + function reprovision( + address serviceProvider, + address oldVerifier, + address newVerifier, + uint256 nThawRequests + ) + external + override + notPaused + onlyAuthorized(serviceProvider, oldVerifier) + onlyAuthorized(serviceProvider, newVerifier) + { + uint256 tokensThawed = _deprovision(serviceProvider, oldVerifier, nThawRequests); + _addToProvision(serviceProvider, newVerifier, tokensThawed); + } + + /// @inheritdoc IHorizonStakingMain + function setProvisionParameters( + address serviceProvider, + address verifier, + uint32 newMaxVerifierCut, + uint64 newThawingPeriod + ) external override notPaused onlyAuthorized(serviceProvider, verifier) { + require(PPMMath.isValidPPM(newMaxVerifierCut), HorizonStakingInvalidMaxVerifierCut(newMaxVerifierCut)); + require( + newThawingPeriod <= _maxThawingPeriod, + HorizonStakingInvalidThawingPeriod(newThawingPeriod, _maxThawingPeriod) + ); + + // Provision must exist + Provision storage prov = _provisions[serviceProvider][verifier]; + require(prov.createdAt != 0, HorizonStakingInvalidProvision(serviceProvider, verifier)); + + if ((prov.maxVerifierCutPending != newMaxVerifierCut) || (prov.thawingPeriodPending != newThawingPeriod)) { + prov.maxVerifierCutPending = newMaxVerifierCut; + prov.thawingPeriodPending = newThawingPeriod; + emit ProvisionParametersStaged(serviceProvider, verifier, newMaxVerifierCut, newThawingPeriod); + } + } + + /// @inheritdoc IHorizonStakingMain + function acceptProvisionParameters(address serviceProvider) external override notPaused { + address verifier = msg.sender; + + // Provision must exist + Provision storage prov = _provisions[serviceProvider][verifier]; + require(prov.createdAt != 0, HorizonStakingInvalidProvision(serviceProvider, verifier)); + + if ((prov.maxVerifierCutPending != prov.maxVerifierCut) || (prov.thawingPeriodPending != prov.thawingPeriod)) { + prov.maxVerifierCut = prov.maxVerifierCutPending; + prov.thawingPeriod = prov.thawingPeriodPending; + emit ProvisionParametersSet(serviceProvider, verifier, prov.maxVerifierCut, prov.thawingPeriod); + } + } + + /* + * DELEGATION + */ + + /// @inheritdoc IHorizonStakingMain + function delegate( + address serviceProvider, + address verifier, + uint256 tokens, + uint256 minSharesOut + ) external override notPaused { + require(tokens != 0, HorizonStakingInvalidZeroTokens()); + _graphToken().pullTokens(msg.sender, tokens); + _delegate(serviceProvider, verifier, tokens, minSharesOut); + } + + /// @inheritdoc IHorizonStakingMain + function addToDelegationPool( + address serviceProvider, + address verifier, + uint256 tokens + ) external override notPaused { + require(tokens != 0, HorizonStakingInvalidZeroTokens()); + + // Provision must exist before adding to delegation pool + Provision memory prov = _provisions[serviceProvider][verifier]; + require(prov.createdAt != 0, HorizonStakingInvalidProvision(serviceProvider, verifier)); + + // Delegation pool must exist before adding tokens + DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier); + require(pool.shares > 0, HorizonStakingInvalidDelegationPool(serviceProvider, verifier)); + + pool.tokens = pool.tokens + tokens; + _graphToken().pullTokens(msg.sender, tokens); + emit TokensToDelegationPoolAdded(serviceProvider, verifier, tokens); + } + + /// @inheritdoc IHorizonStakingMain + function undelegate( + address serviceProvider, + address verifier, + uint256 shares + ) external override notPaused returns (bytes32) { + return _undelegate(ThawRequestType.Delegation, serviceProvider, verifier, shares, msg.sender); + } + + /// @inheritdoc IHorizonStakingMain + function withdrawDelegated( + address serviceProvider, + address verifier, + uint256 nThawRequests + ) external override notPaused { + _withdrawDelegated( + ThawRequestType.Delegation, + serviceProvider, + verifier, + address(0), + address(0), + 0, + nThawRequests + ); + } + + /// @inheritdoc IHorizonStakingMain + function redelegate( + address oldServiceProvider, + address oldVerifier, + address newServiceProvider, + address newVerifier, + uint256 minSharesForNewProvider, + uint256 nThawRequests + ) external override notPaused { + require(newServiceProvider != address(0), HorizonStakingInvalidServiceProviderZeroAddress()); + require(newVerifier != address(0), HorizonStakingInvalidVerifierZeroAddress()); + _withdrawDelegated( + ThawRequestType.Delegation, + oldServiceProvider, + oldVerifier, + newServiceProvider, + newVerifier, + minSharesForNewProvider, + nThawRequests + ); + } + + /// @inheritdoc IHorizonStakingMain + function setDelegationFeeCut( + address serviceProvider, + address verifier, + IGraphPayments.PaymentTypes paymentType, + uint256 feeCut + ) external override notPaused onlyAuthorized(serviceProvider, verifier) { + require(PPMMath.isValidPPM(feeCut), HorizonStakingInvalidDelegationFeeCut(feeCut)); + _delegationFeeCut[serviceProvider][verifier][paymentType] = feeCut; + emit DelegationFeeCutSet(serviceProvider, verifier, paymentType, feeCut); + } + + /// @inheritdoc IHorizonStakingMain + function delegate(address serviceProvider, uint256 tokens) external override notPaused { + require(tokens != 0, HorizonStakingInvalidZeroTokens()); + _graphToken().pullTokens(msg.sender, tokens); + _delegate(serviceProvider, SUBGRAPH_DATA_SERVICE_ADDRESS, tokens, 0); + } + + /// @inheritdoc IHorizonStakingMain + function undelegate(address serviceProvider, uint256 shares) external override notPaused { + _undelegate(ThawRequestType.Delegation, serviceProvider, SUBGRAPH_DATA_SERVICE_ADDRESS, shares, msg.sender); + } + + /// @inheritdoc IHorizonStakingMain + function withdrawDelegated( + address serviceProvider, + address // deprecated - kept for backwards compatibility + ) external override notPaused returns (uint256) { + // Get the delegation pool of the indexer + address delegator = msg.sender; + DelegationPoolInternal storage pool = _legacyDelegationPools[serviceProvider]; + DelegationInternal storage delegation = pool.delegators[delegator]; + + // Validation + uint256 tokensToWithdraw = 0; + uint256 currentEpoch = _graphEpochManager().currentEpoch(); + if ( + delegation.__DEPRECATED_tokensLockedUntil > 0 && currentEpoch >= delegation.__DEPRECATED_tokensLockedUntil + ) { + tokensToWithdraw = delegation.__DEPRECATED_tokensLocked; + } + require(tokensToWithdraw > 0, HorizonStakingNothingToWithdraw()); + + // Reset lock + delegation.__DEPRECATED_tokensLocked = 0; + delegation.__DEPRECATED_tokensLockedUntil = 0; + + emit StakeDelegatedWithdrawn(serviceProvider, delegator, tokensToWithdraw); + + // -- Interactions -- + + // Return tokens to the delegator + _graphToken().pushTokens(delegator, tokensToWithdraw); + + return tokensToWithdraw; + } + + /* + * SLASHING + */ + + /// @inheritdoc IHorizonStakingMain + function slash( + address serviceProvider, + uint256 tokens, + uint256 tokensVerifier, + address verifierDestination + ) external override notPaused { + // TRANSITION PERIOD: remove after the transition period + // Check if sender is authorized to slash on the deprecated list + if (__DEPRECATED_slashers[msg.sender]) { + // Forward call to staking extension + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = STAKING_EXTENSION_ADDRESS.delegatecall( + abi.encodeWithSelector( + IHorizonStakingExtension.legacySlash.selector, + serviceProvider, + tokens, + tokensVerifier, + verifierDestination + ) + ); + require(success, HorizonStakingLegacySlashFailed()); + return; + } + + address verifier = msg.sender; + Provision storage prov = _provisions[serviceProvider][verifier]; + DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier); + uint256 tokensProvisionTotal = prov.tokens + pool.tokens; + require(tokensProvisionTotal != 0, HorizonStakingInsufficientTokens(tokensProvisionTotal, tokens)); + + uint256 tokensToSlash = MathUtils.min(tokens, tokensProvisionTotal); + + // Slash service provider first + // - A portion goes to verifier as reward + // - A portion gets burned + uint256 providerTokensSlashed = MathUtils.min(prov.tokens, tokensToSlash); + if (providerTokensSlashed > 0) { + // Pay verifier reward - must be within the maxVerifierCut percentage + uint256 maxVerifierTokens = providerTokensSlashed.mulPPM(prov.maxVerifierCut); + require( + maxVerifierTokens >= tokensVerifier, + HorizonStakingTooManyTokens(tokensVerifier, maxVerifierTokens) + ); + if (tokensVerifier > 0) { + _graphToken().pushTokens(verifierDestination, tokensVerifier); + emit VerifierTokensSent(serviceProvider, verifier, verifierDestination, tokensVerifier); + } + + // Burn remainder + _graphToken().burnTokens(providerTokensSlashed - tokensVerifier); + + // Provision accounting + uint256 provisionFractionSlashed = (providerTokensSlashed * FIXED_POINT_PRECISION + prov.tokens - 1) / + prov.tokens; + prov.tokensThawing = + (prov.tokensThawing * (FIXED_POINT_PRECISION - provisionFractionSlashed)) / + (FIXED_POINT_PRECISION); + prov.tokens = prov.tokens - providerTokensSlashed; + + // If the slashing leaves the thawing shares with no thawing tokens, cancel pending thawings by: + // - deleting all thawing shares + // - incrementing the nonce to invalidate pending thaw requests + if (prov.sharesThawing != 0 && prov.tokensThawing == 0) { + prov.sharesThawing = 0; + prov.thawingNonce++; + } + + // Service provider accounting + _serviceProviders[serviceProvider].tokensProvisioned = + _serviceProviders[serviceProvider].tokensProvisioned - + providerTokensSlashed; + _serviceProviders[serviceProvider].tokensStaked = + _serviceProviders[serviceProvider].tokensStaked - + providerTokensSlashed; + + emit ProvisionSlashed(serviceProvider, verifier, providerTokensSlashed); + } + + // Slash delegators if needed + // - Slashed delegation is entirely burned + // Since tokensToSlash is already limited above, this subtraction will remain within pool.tokens. + tokensToSlash = tokensToSlash - providerTokensSlashed; + if (tokensToSlash > 0) { + if (_delegationSlashingEnabled) { + // Burn tokens + _graphToken().burnTokens(tokensToSlash); + + // Delegation pool accounting + uint256 delegationFractionSlashed = (tokensToSlash * FIXED_POINT_PRECISION + pool.tokens - 1) / + pool.tokens; + pool.tokens = pool.tokens - tokensToSlash; + pool.tokensThawing = + (pool.tokensThawing * (FIXED_POINT_PRECISION - delegationFractionSlashed)) / + FIXED_POINT_PRECISION; + + // If the slashing leaves the thawing shares with no thawing tokens, cancel pending thawings by: + // - deleting all thawing shares + // - incrementing the nonce to invalidate pending thaw requests + // Note that thawing shares are completely lost, delegators won't get back the corresponding + // delegation pool shares. + if (pool.sharesThawing != 0 && pool.tokensThawing == 0) { + pool.sharesThawing = 0; + pool.thawingNonce++; + } + + emit DelegationSlashed(serviceProvider, verifier, tokensToSlash); + } else { + emit DelegationSlashingSkipped(serviceProvider, verifier, tokensToSlash); + } + } + } + + /* + * LOCKED VERIFIERS + */ + + /// @inheritdoc IHorizonStakingMain + function provisionLocked( + address serviceProvider, + address verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) external override notPaused onlyAuthorized(serviceProvider, verifier) { + require(_allowedLockedVerifiers[verifier], HorizonStakingVerifierNotAllowed(verifier)); + _createProvision(serviceProvider, tokens, verifier, maxVerifierCut, thawingPeriod); + } + + /// @inheritdoc IHorizonStakingMain + function setOperatorLocked(address verifier, address operator, bool allowed) external override notPaused { + require(_allowedLockedVerifiers[verifier], HorizonStakingVerifierNotAllowed(verifier)); + _setOperator(verifier, operator, allowed); + } + + /* + * GOVERNANCE + */ + + /// @inheritdoc IHorizonStakingMain + function setAllowedLockedVerifier(address verifier, bool allowed) external override onlyGovernor { + _allowedLockedVerifiers[verifier] = allowed; + emit AllowedLockedVerifierSet(verifier, allowed); + } + + /// @inheritdoc IHorizonStakingMain + function setDelegationSlashingEnabled() external override onlyGovernor { + _delegationSlashingEnabled = true; + emit DelegationSlashingEnabled(); + } + + /// @inheritdoc IHorizonStakingMain + function clearThawingPeriod() external override onlyGovernor { + __DEPRECATED_thawingPeriod = 0; + emit ThawingPeriodCleared(); + } + + /// @inheritdoc IHorizonStakingMain + function setMaxThawingPeriod(uint64 maxThawingPeriod) external override onlyGovernor { + _maxThawingPeriod = maxThawingPeriod; + emit MaxThawingPeriodSet(_maxThawingPeriod); + } + + /* + * OPERATOR + */ + + /// @inheritdoc IHorizonStakingMain + function setOperator(address verifier, address operator, bool allowed) external override notPaused { + _setOperator(verifier, operator, allowed); + } + + /// @inheritdoc IHorizonStakingMain + function isAuthorized( + address serviceProvider, + address verifier, + address operator + ) external view override returns (bool) { + return _isAuthorized(serviceProvider, verifier, operator); + } + + /* + * GETTERS + */ + + /// @inheritdoc IHorizonStakingMain + function getStakingExtension() external view override returns (address) { + return STAKING_EXTENSION_ADDRESS; + } + + /* + * PRIVATE FUNCTIONS + */ + + /** + * @notice Deposit tokens on the service provider stake, on behalf of the service provider. + * @dev Pulls tokens from the caller. + * @param _serviceProvider Address of the service provider + * @param _tokens Amount of tokens to stake + */ + function _stakeTo(address _serviceProvider, uint256 _tokens) private { + require(_tokens != 0, HorizonStakingInvalidZeroTokens()); + + // Transfer tokens to stake from caller to this contract + _graphToken().pullTokens(msg.sender, _tokens); + + // Stake the transferred tokens + _stake(_serviceProvider, _tokens); + } + + /** + * @notice Move idle stake back to the owner's account. + * Stake is removed from the protocol: + * - During the transition period it's locked for a period of time before it can be withdrawn + * by calling {withdraw}. + * - After the transition period it's immediately withdrawn. + * Note that after the transition period if there are tokens still locked they will have to be + * withdrawn by calling {withdraw}. + * @param _tokens Amount of tokens to unstake + */ + function _unstake(uint256 _tokens) private { + address serviceProvider = msg.sender; + require(_tokens != 0, HorizonStakingInvalidZeroTokens()); + uint256 tokensIdle = _getIdleStake(serviceProvider); + require(_tokens <= tokensIdle, HorizonStakingInsufficientIdleStake(_tokens, tokensIdle)); + + ServiceProviderInternal storage sp = _serviceProviders[serviceProvider]; + uint256 stakedTokens = sp.tokensStaked; + + // This is also only during the transition period: we need + // to ensure tokens stay locked after closing legacy allocations. + // After sufficient time (56 days?) we should remove the closeAllocation function + // and set the thawing period to 0. + uint256 lockingPeriod = __DEPRECATED_thawingPeriod; + if (lockingPeriod == 0) { + sp.tokensStaked = stakedTokens - _tokens; + _graphToken().pushTokens(serviceProvider, _tokens); + emit HorizonStakeWithdrawn(serviceProvider, _tokens); + } else { + // Before locking more tokens, withdraw any unlocked ones if possible + if (sp.__DEPRECATED_tokensLocked != 0 && block.number >= sp.__DEPRECATED_tokensLockedUntil) { + _withdraw(serviceProvider); + } + // TRANSITION PERIOD: remove after the transition period + // Take into account period averaging for multiple unstake requests + if (sp.__DEPRECATED_tokensLocked > 0) { + lockingPeriod = MathUtils.weightedAverageRoundingUp( + MathUtils.diffOrZero(sp.__DEPRECATED_tokensLockedUntil, block.number), // Remaining thawing period + sp.__DEPRECATED_tokensLocked, // Weighted by remaining unstaked tokens + lockingPeriod, // Thawing period + _tokens // Weighted by new tokens to unstake + ); + } + + // Update balances + sp.__DEPRECATED_tokensLocked = sp.__DEPRECATED_tokensLocked + _tokens; + sp.__DEPRECATED_tokensLockedUntil = block.number + lockingPeriod; + emit HorizonStakeLocked(serviceProvider, sp.__DEPRECATED_tokensLocked, sp.__DEPRECATED_tokensLockedUntil); + } + } + + /** + * @notice Withdraw service provider tokens once the thawing period (initiated by {unstake}) has passed. + * All thawed tokens are withdrawn. + * @dev TRANSITION PERIOD: This is only needed during the transition period while we still have + * a global lock. After that, unstake() will automatically withdraw. + * @param _serviceProvider Address of service provider to withdraw funds from + */ + function _withdraw(address _serviceProvider) private { + // Get tokens available for withdraw and update balance + ServiceProviderInternal storage sp = _serviceProviders[_serviceProvider]; + uint256 tokensToWithdraw = sp.__DEPRECATED_tokensLocked; + require(tokensToWithdraw != 0, HorizonStakingInvalidZeroTokens()); + require( + block.number >= sp.__DEPRECATED_tokensLockedUntil, + HorizonStakingStillThawing(sp.__DEPRECATED_tokensLockedUntil) + ); + + // Reset locked tokens + sp.__DEPRECATED_tokensLocked = 0; + sp.__DEPRECATED_tokensLockedUntil = 0; + + sp.tokensStaked = sp.tokensStaked - tokensToWithdraw; + + // Return tokens to the service provider + _graphToken().pushTokens(_serviceProvider, tokensToWithdraw); + + emit HorizonStakeWithdrawn(_serviceProvider, tokensToWithdraw); + } + + /** + * @notice Provision stake to a verifier. The tokens will be locked with a thawing period + * and will be slashable by the verifier. This is the main mechanism to provision stake to a data + * service, where the data service is the verifier. + * This function can be called by the service provider or by an operator authorized by the provider + * for this specific verifier. + * @dev TRANSITION PERIOD: During the transition period, only the subgraph data service can be used as a verifier. This + * prevents an escape hatch for legacy allocation stake. + * @param _serviceProvider The service provider address + * @param _verifier The verifier address for which the tokens are provisioned (who will be able to slash the tokens) + * @param _tokens The amount of tokens that will be locked and slashable + * @param _maxVerifierCut The maximum cut, expressed in PPM, that a verifier can transfer instead of burning when slashing + * @param _thawingPeriod The period in seconds that the tokens will be thawing before they can be removed from the provision + */ + function _createProvision( + address _serviceProvider, + uint256 _tokens, + address _verifier, + uint32 _maxVerifierCut, + uint64 _thawingPeriod + ) private { + require(_tokens > 0, HorizonStakingInvalidZeroTokens()); + // TRANSITION PERIOD: Remove this after the transition period - it prevents an early escape hatch for legacy allocations + require( + _verifier == SUBGRAPH_DATA_SERVICE_ADDRESS || __DEPRECATED_thawingPeriod == 0, + HorizonStakingInvalidVerifier(_verifier) + ); + require(PPMMath.isValidPPM(_maxVerifierCut), HorizonStakingInvalidMaxVerifierCut(_maxVerifierCut)); + require( + _thawingPeriod <= _maxThawingPeriod, + HorizonStakingInvalidThawingPeriod(_thawingPeriod, _maxThawingPeriod) + ); + require(_provisions[_serviceProvider][_verifier].createdAt == 0, HorizonStakingProvisionAlreadyExists()); + uint256 tokensIdle = _getIdleStake(_serviceProvider); + require(_tokens <= tokensIdle, HorizonStakingInsufficientIdleStake(_tokens, tokensIdle)); + + _provisions[_serviceProvider][_verifier] = Provision({ + tokens: _tokens, + tokensThawing: 0, + sharesThawing: 0, + maxVerifierCut: _maxVerifierCut, + thawingPeriod: _thawingPeriod, + createdAt: uint64(block.timestamp), + maxVerifierCutPending: _maxVerifierCut, + thawingPeriodPending: _thawingPeriod, + thawingNonce: 0 + }); + + ServiceProviderInternal storage sp = _serviceProviders[_serviceProvider]; + sp.tokensProvisioned = sp.tokensProvisioned + _tokens; + + emit ProvisionCreated(_serviceProvider, _verifier, _tokens, _maxVerifierCut, _thawingPeriod); + } + + /** + * @notice Adds tokens from the service provider's idle stake to a provision + * @param _serviceProvider The service provider address + * @param _verifier The verifier address + * @param _tokens The amount of tokens to add to the provision + */ + function _addToProvision(address _serviceProvider, address _verifier, uint256 _tokens) private { + Provision storage prov = _provisions[_serviceProvider][_verifier]; + require(_tokens != 0, HorizonStakingInvalidZeroTokens()); + require(prov.createdAt != 0, HorizonStakingInvalidProvision(_serviceProvider, _verifier)); + uint256 tokensIdle = _getIdleStake(_serviceProvider); + require(_tokens <= tokensIdle, HorizonStakingInsufficientIdleStake(_tokens, tokensIdle)); + + prov.tokens = prov.tokens + _tokens; + _serviceProviders[_serviceProvider].tokensProvisioned = + _serviceProviders[_serviceProvider].tokensProvisioned + + _tokens; + emit ProvisionIncreased(_serviceProvider, _verifier, _tokens); + } + + /** + * @notice Start thawing tokens to remove them from a provision. + * This function can be called by the service provider or by an operator authorized by the provider + * for this specific verifier. + * + * Note that removing tokens from a provision is a two step process: + * - First the tokens are thawed using this function. + * - Then after the thawing period, the tokens are removed from the provision using {deprovision} + * or {reprovision}. + * + * @dev We use a thawing pool to keep track of tokens thawing for multiple thaw requests. + * If due to slashing the thawing pool loses all of its tokens, the pool is reset and all pending thaw + * requests are invalidated. + * + * @param _serviceProvider The service provider address + * @param _verifier The verifier address for which the tokens are provisioned + * @param _tokens The amount of tokens to thaw + * @return The ID of the thaw request + */ + function _thaw(address _serviceProvider, address _verifier, uint256 _tokens) private returns (bytes32) { + require(_tokens != 0, HorizonStakingInvalidZeroTokens()); + uint256 tokensAvailable = _getProviderTokensAvailable(_serviceProvider, _verifier); + require(tokensAvailable >= _tokens, HorizonStakingInsufficientTokens(tokensAvailable, _tokens)); + + Provision storage prov = _provisions[_serviceProvider][_verifier]; + + // Calculate shares to issue + // Thawing pool is reset/initialized when the pool is empty: prov.tokensThawing == 0 + // Round thawing shares up to ensure fairness and avoid undervaluing the shares due to rounding down. + uint256 thawingShares = prov.tokensThawing == 0 + ? _tokens + : ((prov.sharesThawing * _tokens + prov.tokensThawing - 1) / prov.tokensThawing); + uint64 thawingUntil = uint64(block.timestamp + uint256(prov.thawingPeriod)); + + prov.sharesThawing = prov.sharesThawing + thawingShares; + prov.tokensThawing = prov.tokensThawing + _tokens; + + bytes32 thawRequestId = _createThawRequest( + ThawRequestType.Provision, + _serviceProvider, + _verifier, + _serviceProvider, + thawingShares, + thawingUntil, + prov.thawingNonce + ); + emit ProvisionThawed(_serviceProvider, _verifier, _tokens); + return thawRequestId; + } + + /** + * @notice Remove tokens from a provision and move them back to the service provider's idle stake. + * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw + * requests in the event that fulfilling all of them results in a gas limit error. + * @param _serviceProvider The service provider address + * @param _verifier The verifier address + * @param _nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests. + * @return The amount of tokens that were removed from the provision + */ + function _deprovision( + address _serviceProvider, + address _verifier, + uint256 _nThawRequests + ) private returns (uint256) { + Provision storage prov = _provisions[_serviceProvider][_verifier]; + + uint256 tokensThawed_ = 0; + uint256 sharesThawing = prov.sharesThawing; + uint256 tokensThawing = prov.tokensThawing; + + FulfillThawRequestsParams memory params = FulfillThawRequestsParams({ + requestType: ThawRequestType.Provision, + serviceProvider: _serviceProvider, + verifier: _verifier, + owner: _serviceProvider, + tokensThawing: tokensThawing, + sharesThawing: sharesThawing, + nThawRequests: _nThawRequests, + thawingNonce: prov.thawingNonce + }); + (tokensThawed_, tokensThawing, sharesThawing) = _fulfillThawRequests(params); + + prov.tokens = prov.tokens - tokensThawed_; + prov.sharesThawing = sharesThawing; + prov.tokensThawing = tokensThawing; + _serviceProviders[_serviceProvider].tokensProvisioned -= tokensThawed_; + + emit TokensDeprovisioned(_serviceProvider, _verifier, tokensThawed_); + return tokensThawed_; + } + + /** + * @notice Delegate tokens to a provision. + * @dev Note that this function does not pull the delegated tokens from the caller. It expects that to + * have been done before calling this function. + * @param _serviceProvider The service provider address + * @param _verifier The verifier address + * @param _tokens The amount of tokens to delegate + * @param _minSharesOut The minimum amount of shares to accept, slippage protection. + */ + function _delegate(address _serviceProvider, address _verifier, uint256 _tokens, uint256 _minSharesOut) private { + // Enforces a minimum delegation amount to prevent share manipulation attacks. + // This stops attackers from inflating share value and blocking other delegators. + require(_tokens >= MIN_DELEGATION, HorizonStakingInsufficientDelegationTokens(_tokens, MIN_DELEGATION)); + require( + _provisions[_serviceProvider][_verifier].createdAt != 0, + HorizonStakingInvalidProvision(_serviceProvider, _verifier) + ); + + DelegationPoolInternal storage pool = _getDelegationPool(_serviceProvider, _verifier); + DelegationInternal storage delegation = pool.delegators[msg.sender]; + + // An invalid delegation pool has shares but no tokens + require( + pool.tokens != 0 || pool.shares == 0, + HorizonStakingInvalidDelegationPoolState(_serviceProvider, _verifier) + ); + + // Calculate shares to issue + // Delegation pool is reset/initialized in any of the following cases: + // - pool.tokens == 0 and pool.shares == 0, pool is completely empty. Note that we don't test shares == 0 because + // the invalid delegation pool check already ensures shares are 0 if tokens are 0 + // - pool.tokens == pool.tokensThawing, the entire pool is thawing + bool initializePool = pool.tokens == 0 || pool.tokens == pool.tokensThawing; + uint256 shares = initializePool ? _tokens : ((_tokens * pool.shares) / (pool.tokens - pool.tokensThawing)); + require(shares != 0 && shares >= _minSharesOut, HorizonStakingSlippageProtection(shares, _minSharesOut)); + + pool.tokens = pool.tokens + _tokens; + pool.shares = pool.shares + shares; + + delegation.shares = delegation.shares + shares; + + emit TokensDelegated(_serviceProvider, _verifier, msg.sender, _tokens, shares); + } + + /** + * @notice Undelegate tokens from a provision and start thawing them. + * Note that undelegating tokens from a provision is a two step process: + * - First the tokens are thawed using this function. + * - Then after the thawing period, the tokens are removed from the provision using {withdrawDelegated}. + * @dev To allow delegation to be slashable even while thawing without breaking accounting + * the delegation pool shares are burned and replaced with thawing pool shares. + * @dev Note that due to slashing the delegation pool can enter an invalid state if all it's tokens are slashed. + * An invalid pool can only be recovered by adding back tokens into the pool with {IHorizonStakingMain-addToDelegationPool}. + * Any time the delegation pool is invalidated, the thawing pool is also reset and any pending undelegate requests get + * invalidated. + * @dev Note that delegation that is caught thawing when the pool is invalidated will be completely lost! However delegation shares + * that were not thawing will be preserved. + * @param _requestType The type of thaw request (Provision or Delegation). + * @param _serviceProvider The service provider address + * @param _verifier The verifier address + * @param _shares The amount of shares to undelegate + * @param _beneficiary The beneficiary address + * @return The ID of the thaw request + */ + function _undelegate( + ThawRequestType _requestType, + address _serviceProvider, + address _verifier, + uint256 _shares, + address _beneficiary + ) private returns (bytes32) { + require(_shares > 0, HorizonStakingInvalidZeroShares()); + DelegationPoolInternal storage pool = _getDelegationPool(_serviceProvider, _verifier); + DelegationInternal storage delegation = pool.delegators[msg.sender]; + require(delegation.shares >= _shares, HorizonStakingInsufficientShares(delegation.shares, _shares)); + + // An invalid delegation pool has shares but no tokens (previous require check ensures shares > 0) + require(pool.tokens != 0, HorizonStakingInvalidDelegationPoolState(_serviceProvider, _verifier)); + + // Calculate thawing shares to issue - convert delegation pool shares to thawing pool shares + // delegation pool shares -> delegation pool tokens -> thawing pool shares + // Thawing pool is reset/initialized when the pool is empty: prov.tokensThawing == 0 + uint256 tokens = (_shares * (pool.tokens - pool.tokensThawing)) / pool.shares; + + // Thawing shares are rounded down to protect the pool and avoid taking extra tokens from other participants. + uint256 thawingShares = pool.tokensThawing == 0 ? tokens : ((tokens * pool.sharesThawing) / pool.tokensThawing); + uint64 thawingUntil = uint64(block.timestamp + uint256(_provisions[_serviceProvider][_verifier].thawingPeriod)); + + pool.tokensThawing = pool.tokensThawing + tokens; + pool.sharesThawing = pool.sharesThawing + thawingShares; + + pool.shares = pool.shares - _shares; + delegation.shares = delegation.shares - _shares; + if (delegation.shares != 0) { + uint256 remainingTokens = (delegation.shares * (pool.tokens - pool.tokensThawing)) / pool.shares; + require( + remainingTokens >= MIN_DELEGATION, + HorizonStakingInsufficientTokens(remainingTokens, MIN_DELEGATION) + ); + } + + bytes32 thawRequestId = _createThawRequest( + _requestType, + _serviceProvider, + _verifier, + _beneficiary, + thawingShares, + thawingUntil, + pool.thawingNonce + ); + + emit TokensUndelegated(_serviceProvider, _verifier, msg.sender, tokens); + return thawRequestId; + } + + /** + * @notice Withdraw undelegated tokens from a provision after thawing. + * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw + * requests in the event that fulfilling all of them results in a gas limit error. + * @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill + * the thaw requests with an amount equal to zero. + * @param _requestType The type of thaw request (Provision or Delegation). + * @param _serviceProvider The service provider address + * @param _verifier The verifier address + * @param _newServiceProvider The new service provider address + * @param _newVerifier The new verifier address + * @param _minSharesForNewProvider The minimum number of shares for the new service provider + * @param _nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests. + */ + function _withdrawDelegated( + ThawRequestType _requestType, + address _serviceProvider, + address _verifier, + address _newServiceProvider, + address _newVerifier, + uint256 _minSharesForNewProvider, + uint256 _nThawRequests + ) private { + DelegationPoolInternal storage pool = _getDelegationPool(_serviceProvider, _verifier); + + // An invalid delegation pool has shares but no tokens + require( + pool.tokens != 0 || pool.shares == 0, + HorizonStakingInvalidDelegationPoolState(_serviceProvider, _verifier) + ); + + uint256 tokensThawed = 0; + uint256 sharesThawing = pool.sharesThawing; + uint256 tokensThawing = pool.tokensThawing; + + FulfillThawRequestsParams memory params = FulfillThawRequestsParams({ + requestType: _requestType, + serviceProvider: _serviceProvider, + verifier: _verifier, + owner: msg.sender, + tokensThawing: tokensThawing, + sharesThawing: sharesThawing, + nThawRequests: _nThawRequests, + thawingNonce: pool.thawingNonce + }); + (tokensThawed, tokensThawing, sharesThawing) = _fulfillThawRequests(params); + + // The next subtraction should never revert becase: pool.tokens >= pool.tokensThawing and pool.tokensThawing >= tokensThawed + // In the event the pool gets completely slashed tokensThawed will fulfil to 0. + pool.tokens = pool.tokens - tokensThawed; + pool.sharesThawing = sharesThawing; + pool.tokensThawing = tokensThawing; + + if (tokensThawed != 0) { + if (_newServiceProvider != address(0) && _newVerifier != address(0)) { + _delegate(_newServiceProvider, _newVerifier, tokensThawed, _minSharesForNewProvider); + } else { + _graphToken().pushTokens(msg.sender, tokensThawed); + emit DelegatedTokensWithdrawn(_serviceProvider, _verifier, msg.sender, tokensThawed); + } + } + } + + /** + * @notice Creates a thaw request. + * Allows creating thaw requests up to a maximum of `MAX_THAW_REQUESTS` per owner. + * Thaw requests are stored in a linked list per owner (and service provider, verifier) to allow for efficient + * processing. + * @param _requestType The type of thaw request. + * @param _serviceProvider The address of the service provider + * @param _verifier The address of the verifier + * @param _owner The address of the owner of the thaw request + * @param _shares The number of shares to thaw + * @param _thawingUntil The timestamp until which the shares are thawing + * @param _thawingNonce Owner's validity nonce for the thaw request + * @return The ID of the thaw request + */ + function _createThawRequest( + ThawRequestType _requestType, + address _serviceProvider, + address _verifier, + address _owner, + uint256 _shares, + uint64 _thawingUntil, + uint256 _thawingNonce + ) private returns (bytes32) { + require(_shares != 0, HorizonStakingInvalidZeroShares()); + LinkedList.List storage thawRequestList = _getThawRequestList( + _requestType, + _serviceProvider, + _verifier, + _owner + ); + require(thawRequestList.count < MAX_THAW_REQUESTS, HorizonStakingTooManyThawRequests()); + + bytes32 thawRequestId = keccak256(abi.encodePacked(_serviceProvider, _verifier, _owner, thawRequestList.nonce)); + ThawRequest storage thawRequest = _getThawRequest(_requestType, thawRequestId); + thawRequest.shares = _shares; + thawRequest.thawingUntil = _thawingUntil; + thawRequest.next = bytes32(0); + thawRequest.thawingNonce = _thawingNonce; + + if (thawRequestList.count != 0) _getThawRequest(_requestType, thawRequestList.tail).next = thawRequestId; + thawRequestList.addTail(thawRequestId); + + emit ThawRequestCreated( + _requestType, + _serviceProvider, + _verifier, + _owner, + _shares, + _thawingUntil, + thawRequestId + ); + return thawRequestId; + } + + /** + * @notice Traverses a thaw request list and fulfills expired thaw requests. + * @param _params The parameters for fulfilling thaw requests + * @return The amount of thawed tokens + * @return The amount of tokens still thawing + * @return The amount of shares still thawing + */ + function _fulfillThawRequests( + FulfillThawRequestsParams memory _params + ) private returns (uint256, uint256, uint256) { + LinkedList.List storage thawRequestList = _getThawRequestList( + _params.requestType, + _params.serviceProvider, + _params.verifier, + _params.owner + ); + require(thawRequestList.count > 0, HorizonStakingNothingThawing()); + + TraverseThawRequestsResults memory results = _traverseThawRequests(_params, thawRequestList); + + emit ThawRequestsFulfilled( + _params.requestType, + _params.serviceProvider, + _params.verifier, + _params.owner, + results.requestsFulfilled, + results.tokensThawed + ); + + return (results.tokensThawed, results.tokensThawing, results.sharesThawing); + } + + /** + * @notice Traverses a thaw request list and fulfills expired thaw requests. + * @param _params The parameters for fulfilling thaw requests + * @param _thawRequestList The list of thaw requests to traverse + * @return The results of the traversal + */ + function _traverseThawRequests( + FulfillThawRequestsParams memory _params, + LinkedList.List storage _thawRequestList + ) private returns (TraverseThawRequestsResults memory) { + function(bytes32) view returns (bytes32) getNextItem = _getNextThawRequest(_params.requestType); + function(bytes32) deleteItem = _getDeleteThawRequest(_params.requestType); + + bytes memory acc = abi.encode( + _params.requestType, + uint256(0), + _params.tokensThawing, + _params.sharesThawing, + _params.thawingNonce + ); + (uint256 thawRequestsFulfilled, bytes memory data) = _thawRequestList.traverse( + getNextItem, + _fulfillThawRequest, + deleteItem, + acc, + _params.nThawRequests + ); + + (, uint256 tokensThawed, uint256 tokensThawing, uint256 sharesThawing) = abi.decode( + data, + (ThawRequestType, uint256, uint256, uint256) + ); + + return + TraverseThawRequestsResults({ + requestsFulfilled: thawRequestsFulfilled, + tokensThawed: tokensThawed, + tokensThawing: tokensThawing, + sharesThawing: sharesThawing + }); + } + + /** + * @notice Fulfills a thaw request. + * @dev This function is used as a callback in the thaw requests linked list traversal. + * @param _thawRequestId The ID of the current thaw request + * @param _acc The accumulator data for the thaw requests being fulfilled + * @return Whether the thaw request is still thawing, indicating that the traversal should continue or stop. + * @return The updated accumulator data + */ + function _fulfillThawRequest(bytes32 _thawRequestId, bytes memory _acc) private returns (bool, bytes memory) { + // decode + ( + ThawRequestType requestType, + uint256 tokensThawed, + uint256 tokensThawing, + uint256 sharesThawing, + uint256 thawingNonce + ) = abi.decode(_acc, (ThawRequestType, uint256, uint256, uint256, uint256)); + + ThawRequest storage thawRequest = _getThawRequest(requestType, _thawRequestId); + + // early exit + if (thawRequest.thawingUntil > block.timestamp) { + return (true, LinkedList.NULL_BYTES); + } + + // process - only fulfill thaw requests for the current valid nonce + uint256 tokens = 0; + bool validThawRequest = thawRequest.thawingNonce == thawingNonce; + if (validThawRequest) { + tokens = (thawRequest.shares * tokensThawing) / sharesThawing; + tokensThawing = tokensThawing - tokens; + sharesThawing = sharesThawing - thawRequest.shares; + tokensThawed = tokensThawed + tokens; + } + emit ThawRequestFulfilled( + requestType, + _thawRequestId, + tokens, + thawRequest.shares, + thawRequest.thawingUntil, + validThawRequest + ); + + // encode + _acc = abi.encode(requestType, tokensThawed, tokensThawing, sharesThawing, thawingNonce); + return (false, _acc); + } + + /** + * @notice Deletes a thaw request for a provision. + * @param _thawRequestId The ID of the thaw request to delete. + */ + function _deleteProvisionThawRequest(bytes32 _thawRequestId) private { + delete _thawRequests[ThawRequestType.Provision][_thawRequestId]; + } + + /** + * @notice Deletes a thaw request for a delegation. + * @param _thawRequestId The ID of the thaw request to delete. + */ + function _deleteDelegationThawRequest(bytes32 _thawRequestId) private { + delete _thawRequests[ThawRequestType.Delegation][_thawRequestId]; + } + + /** + * @notice Authorize or unauthorize an address to be an operator for the caller on a data service. + * @dev Note that this function handles the special case where the verifier is the subgraph data service, + * where the operator settings are stored in the legacy mapping. + * @param _verifier The verifier / data service on which they'll be allowed to operate + * @param _operator Address to authorize or unauthorize + * @param _allowed Whether the operator is authorized or not + */ + function _setOperator(address _verifier, address _operator, bool _allowed) private { + require(_operator != msg.sender, HorizonStakingCallerIsServiceProvider()); + if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { + _legacyOperatorAuth[msg.sender][_operator] = _allowed; + } else { + _operatorAuth[msg.sender][_verifier][_operator] = _allowed; + } + emit OperatorSet(msg.sender, _verifier, _operator, _allowed); + } + + /** + * @notice Check if an operator is authorized for the caller on a specific verifier / data service. + * @dev Note that this function handles the special case where the verifier is the subgraph data service, + * where the operator settings are stored in the legacy mapping. + * @param _serviceProvider The service provider on behalf of whom they're claiming to act + * @param _verifier The verifier / data service on which they're claiming to act + * @param _operator The address to check for auth + * @return Whether the operator is authorized or not + */ + function _isAuthorized(address _serviceProvider, address _verifier, address _operator) private view returns (bool) { + if (_operator == _serviceProvider) { + return true; + } + if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { + return _legacyOperatorAuth[_serviceProvider][_operator]; + } else { + return _operatorAuth[_serviceProvider][_verifier][_operator]; + } + } + + /** + * @notice Determines the correct callback function for `deleteItem` based on the request type. + * @param _requestType The type of thaw request (Provision or Delegation). + * @return A function pointer to the appropriate `deleteItem` callback. + */ + function _getDeleteThawRequest(ThawRequestType _requestType) private pure returns (function(bytes32)) { + if (_requestType == ThawRequestType.Provision) { + return _deleteProvisionThawRequest; + } else if (_requestType == ThawRequestType.Delegation) { + return _deleteDelegationThawRequest; + } else { + revert HorizonStakingInvalidThawRequestType(); + } + } +} diff --git a/packages/horizon/contracts/staking/HorizonStakingBase.sol b/packages/horizon/contracts/staking/HorizonStakingBase.sol new file mode 100644 index 000000000..5ccbddfa2 --- /dev/null +++ b/packages/horizon/contracts/staking/HorizonStakingBase.sol @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IHorizonStakingTypes } from "../interfaces/internal/IHorizonStakingTypes.sol"; +import { IHorizonStakingBase } from "../interfaces/internal/IHorizonStakingBase.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; + +import { MathUtils } from "../libraries/MathUtils.sol"; +import { LinkedList } from "../libraries/LinkedList.sol"; + +import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol"; +import { GraphUpgradeable } from "@graphprotocol/contracts/contracts/upgrades/GraphUpgradeable.sol"; +import { Managed } from "./utilities/Managed.sol"; +import { HorizonStakingV1Storage } from "./HorizonStakingStorage.sol"; + +/** + * @title HorizonStakingBase contract + * @notice This contract is the base staking contract implementing storage getters for both internal + * and external use. + * @dev Implementation of the {IHorizonStakingBase} interface. + * @dev It's meant to be inherited by the {HorizonStaking} and {HorizonStakingExtension} + * contracts so some internal functions are also included here. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract HorizonStakingBase is + Multicall, + Managed, + HorizonStakingV1Storage, + GraphUpgradeable, + IHorizonStakingTypes, + IHorizonStakingBase +{ + using LinkedList for LinkedList.List; + + /** + * @notice The address of the subgraph data service. + * @dev Require to handle the special case when the verifier is the subgraph data service. + */ + address internal immutable SUBGRAPH_DATA_SERVICE_ADDRESS; + + /** + * @dev The staking contract is upgradeable however we still use the constructor to set + * a few immutable variables. + * @param controller The address of the Graph controller contract. + * @param subgraphDataServiceAddress The address of the subgraph data service. + */ + constructor(address controller, address subgraphDataServiceAddress) Managed(controller) { + SUBGRAPH_DATA_SERVICE_ADDRESS = subgraphDataServiceAddress; + } + + /// @inheritdoc IHorizonStakingBase + /// @dev Removes deprecated fields from the return value. + function getServiceProvider(address serviceProvider) external view override returns (ServiceProvider memory) { + ServiceProvider memory sp; + ServiceProviderInternal storage spInternal = _serviceProviders[serviceProvider]; + sp.tokensStaked = spInternal.tokensStaked; + sp.tokensProvisioned = spInternal.tokensProvisioned; + return sp; + } + + /// @inheritdoc IHorizonStakingBase + function getStake(address serviceProvider) external view override returns (uint256) { + return _serviceProviders[serviceProvider].tokensStaked; + } + + /// @inheritdoc IHorizonStakingBase + function getIdleStake(address serviceProvider) external view override returns (uint256) { + return _getIdleStake(serviceProvider); + } + + /// @inheritdoc IHorizonStakingBase + /// @dev Removes deprecated fields from the return value. + function getDelegationPool( + address serviceProvider, + address verifier + ) external view override returns (DelegationPool memory) { + DelegationPool memory pool; + DelegationPoolInternal storage poolInternal = _getDelegationPool(serviceProvider, verifier); + pool.tokens = poolInternal.tokens; + pool.shares = poolInternal.shares; + pool.tokensThawing = poolInternal.tokensThawing; + pool.sharesThawing = poolInternal.sharesThawing; + pool.thawingNonce = poolInternal.thawingNonce; + return pool; + } + + /// @inheritdoc IHorizonStakingBase + /// @dev Removes deprecated fields from the return value. + function getDelegation( + address serviceProvider, + address verifier, + address delegator + ) external view override returns (Delegation memory) { + Delegation memory delegation; + DelegationPoolInternal storage poolInternal = _getDelegationPool(serviceProvider, verifier); + delegation.shares = poolInternal.delegators[delegator].shares; + return delegation; + } + + /// @inheritdoc IHorizonStakingBase + function getDelegationFeeCut( + address serviceProvider, + address verifier, + IGraphPayments.PaymentTypes paymentType + ) external view override returns (uint256) { + return _delegationFeeCut[serviceProvider][verifier][paymentType]; + } + + /// @inheritdoc IHorizonStakingBase + function getProvision(address serviceProvider, address verifier) external view override returns (Provision memory) { + return _provisions[serviceProvider][verifier]; + } + + /// @inheritdoc IHorizonStakingBase + function getTokensAvailable( + address serviceProvider, + address verifier, + uint32 delegationRatio + ) external view override returns (uint256) { + uint256 tokensAvailableProvider = _getProviderTokensAvailable(serviceProvider, verifier); + uint256 tokensAvailableDelegated = _getDelegatedTokensAvailable(serviceProvider, verifier); + + uint256 tokensDelegatedMax = tokensAvailableProvider * (uint256(delegationRatio)); + uint256 tokensDelegatedCapacity = MathUtils.min(tokensAvailableDelegated, tokensDelegatedMax); + + return tokensAvailableProvider + tokensDelegatedCapacity; + } + + /// @inheritdoc IHorizonStakingBase + function getProviderTokensAvailable( + address serviceProvider, + address verifier + ) external view override returns (uint256) { + return _getProviderTokensAvailable(serviceProvider, verifier); + } + + /// @inheritdoc IHorizonStakingBase + function getDelegatedTokensAvailable( + address serviceProvider, + address verifier + ) external view override returns (uint256) { + return _getDelegatedTokensAvailable(serviceProvider, verifier); + } + + /// @inheritdoc IHorizonStakingBase + function getThawRequest( + ThawRequestType requestType, + bytes32 thawRequestId + ) external view override returns (ThawRequest memory) { + return _getThawRequest(requestType, thawRequestId); + } + + /// @inheritdoc IHorizonStakingBase + function getThawRequestList( + ThawRequestType requestType, + address serviceProvider, + address verifier, + address owner + ) external view override returns (LinkedList.List memory) { + return _getThawRequestList(requestType, serviceProvider, verifier, owner); + } + + /// @inheritdoc IHorizonStakingBase + function getThawedTokens( + ThawRequestType requestType, + address serviceProvider, + address verifier, + address owner + ) external view override returns (uint256) { + LinkedList.List storage thawRequestList = _getThawRequestList(requestType, serviceProvider, verifier, owner); + if (thawRequestList.count == 0) { + return 0; + } + + uint256 thawedTokens = 0; + Provision storage prov = _provisions[serviceProvider][verifier]; + uint256 tokensThawing = prov.tokensThawing; + uint256 sharesThawing = prov.sharesThawing; + + bytes32 thawRequestId = thawRequestList.head; + while (thawRequestId != bytes32(0)) { + ThawRequest storage thawRequest = _getThawRequest(requestType, thawRequestId); + if (thawRequest.thawingUntil <= block.timestamp) { + uint256 tokens = (thawRequest.shares * tokensThawing) / sharesThawing; + tokensThawing = tokensThawing - tokens; + sharesThawing = sharesThawing - thawRequest.shares; + thawedTokens = thawedTokens + tokens; + } else { + break; + } + thawRequestId = thawRequest.next; + } + return thawedTokens; + } + + /// @inheritdoc IHorizonStakingBase + function getMaxThawingPeriod() external view override returns (uint64) { + return _maxThawingPeriod; + } + + /// @inheritdoc IHorizonStakingBase + function isAllowedLockedVerifier(address verifier) external view returns (bool) { + return _allowedLockedVerifiers[verifier]; + } + + /// @inheritdoc IHorizonStakingBase + function isDelegationSlashingEnabled() external view returns (bool) { + return _delegationSlashingEnabled; + } + + /** + * @notice Deposit tokens into the service provider stake. + * @dev TRANSITION PERIOD: After transition period move to IHorizonStakingMain. Temporarily it + * needs to be here since it's used by both {HorizonStaking} and {HorizonStakingExtension}. + * + * Emits a {HorizonStakeDeposited} event. + * @param _serviceProvider The address of the service provider. + * @param _tokens The amount of tokens to deposit. + */ + function _stake(address _serviceProvider, uint256 _tokens) internal { + _serviceProviders[_serviceProvider].tokensStaked = _serviceProviders[_serviceProvider].tokensStaked + _tokens; + emit HorizonStakeDeposited(_serviceProvider, _tokens); + } + + /** + * @notice Gets the service provider's idle stake which is the stake that is not being + * used for any provision. Note that this only includes service provider's self stake. + * @dev Note that the calculation considers tokens that were locked in the legacy staking contract. + * @dev TRANSITION PERIOD: update the calculation after the transition period. + * @param _serviceProvider The address of the service provider. + * @return The amount of tokens that are idle. + */ + function _getIdleStake(address _serviceProvider) internal view returns (uint256) { + uint256 tokensUsed = _serviceProviders[_serviceProvider].tokensProvisioned + + _serviceProviders[_serviceProvider].__DEPRECATED_tokensAllocated + + _serviceProviders[_serviceProvider].__DEPRECATED_tokensLocked; + uint256 tokensStaked = _serviceProviders[_serviceProvider].tokensStaked; + return tokensStaked > tokensUsed ? tokensStaked - tokensUsed : 0; + } + + /** + * @notice Gets the details of delegation pool. + * @dev Note that this function handles the special case where the verifier is the subgraph data service, + * where the pools are stored in the legacy mapping. + * @param _serviceProvider The address of the service provider. + * @param _verifier The address of the verifier. + * @return The delegation pool details. + */ + function _getDelegationPool( + address _serviceProvider, + address _verifier + ) internal view returns (DelegationPoolInternal storage) { + if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { + return _legacyDelegationPools[_serviceProvider]; + } else { + return _delegationPools[_serviceProvider][_verifier]; + } + } + + /** + * @notice Gets the service provider's tokens available in a provision. + * @dev Calculated as the tokens available minus the tokens thawing. + * @param _serviceProvider The address of the service provider. + * @param _verifier The address of the verifier. + * @return The amount of tokens available. + */ + function _getProviderTokensAvailable(address _serviceProvider, address _verifier) internal view returns (uint256) { + return _provisions[_serviceProvider][_verifier].tokens - _provisions[_serviceProvider][_verifier].tokensThawing; + } + + /** + * @notice Retrieves the next thaw request for a provision. + * @param _thawRequestId The ID of the current thaw request. + * @return The ID of the next thaw request in the list. + */ + function _getNextProvisionThawRequest(bytes32 _thawRequestId) internal view returns (bytes32) { + return _thawRequests[ThawRequestType.Provision][_thawRequestId].next; + } + + /** + * @notice Retrieves the next thaw request for a delegation. + * @param _thawRequestId The ID of the current thaw request. + * @return The ID of the next thaw request in the list. + */ + function _getNextDelegationThawRequest(bytes32 _thawRequestId) internal view returns (bytes32) { + return _thawRequests[ThawRequestType.Delegation][_thawRequestId].next; + } + + /** + * @notice Retrieves the thaw request list for the given request type. + * @dev Uses the `ThawRequestType` to determine which mapping to access. + * Reverts if the request type is unknown. + * @param _requestType The type of thaw request (Provision or Delegation). + * @param _serviceProvider The address of the service provider. + * @param _verifier The address of the verifier. + * @param _owner The address of the owner of the thaw request. + * @return The linked list of thaw requests for the specified request type. + */ + function _getThawRequestList( + ThawRequestType _requestType, + address _serviceProvider, + address _verifier, + address _owner + ) internal view returns (LinkedList.List storage) { + return _thawRequestLists[_requestType][_serviceProvider][_verifier][_owner]; + } + + /** + * @notice Retrieves a specific thaw request for the given request type. + * @dev Uses the `ThawRequestType` to determine which mapping to access. + * @param _requestType The type of thaw request (Provision or Delegation). + * @param _thawRequestId The unique ID of the thaw request. + * @return The thaw request data for the specified request type and ID. + */ + function _getThawRequest( + ThawRequestType _requestType, + bytes32 _thawRequestId + ) internal view returns (IHorizonStakingTypes.ThawRequest storage) { + return _thawRequests[_requestType][_thawRequestId]; + } + + /** + * @notice Determines the correct callback function for `getNextItem` based on the request type. + * @param _requestType The type of thaw request (Provision or Delegation). + * @return A function pointer to the appropriate `getNextItem` callback. + */ + function _getNextThawRequest( + ThawRequestType _requestType + ) internal pure returns (function(bytes32) view returns (bytes32)) { + if (_requestType == ThawRequestType.Provision) { + return _getNextProvisionThawRequest; + } else if (_requestType == ThawRequestType.Delegation) { + return _getNextDelegationThawRequest; + } else { + revert HorizonStakingInvalidThawRequestType(); + } + } + + /** + * @notice Gets the delegator's tokens available in a provision. + * @dev Calculated as the tokens available minus the tokens thawing. + * @param _serviceProvider The address of the service provider. + * @param _verifier The address of the verifier. + * @return The amount of tokens available. + */ + function _getDelegatedTokensAvailable(address _serviceProvider, address _verifier) private view returns (uint256) { + DelegationPoolInternal storage poolInternal = _getDelegationPool(_serviceProvider, _verifier); + return poolInternal.tokens - poolInternal.tokensThawing; + } +} diff --git a/packages/horizon/contracts/staking/HorizonStakingExtension.sol b/packages/horizon/contracts/staking/HorizonStakingExtension.sol new file mode 100644 index 000000000..00b9c6048 --- /dev/null +++ b/packages/horizon/contracts/staking/HorizonStakingExtension.sol @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { ICuration } from "@graphprotocol/contracts/contracts/curation/ICuration.sol"; +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IHorizonStakingExtension } from "../interfaces/internal/IHorizonStakingExtension.sol"; +import { IRewardsIssuer } from "@graphprotocol/contracts/contracts/rewards/IRewardsIssuer.sol"; + +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; +import { MathUtils } from "../libraries/MathUtils.sol"; +import { ExponentialRebates } from "./libraries/ExponentialRebates.sol"; +import { PPMMath } from "../libraries/PPMMath.sol"; + +import { HorizonStakingBase } from "./HorizonStakingBase.sol"; + +/** + * @title Horizon Staking extension contract + * @notice The {HorizonStakingExtension} contract implements the legacy functionality required to support the transition + * to the Horizon Staking contract. It allows indexers to close allocations and collect pending query fees, but it + * does not allow for the creation of new allocations. This should allow indexers to migrate to a subgraph data service + * without losing rewards or having service interruptions. + * @dev TRANSITION PERIOD: Once the transition period passes this contract can be removed (note that an upgrade to the + * RewardsManager will also be required). It's expected the transition period to last for at least a full allocation cycle + * (28 epochs). + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension { + using TokenUtils for IGraphToken; + using PPMMath for uint256; + + /** + * @dev Check if the caller is the slasher. + */ + modifier onlySlasher() { + require(__DEPRECATED_slashers[msg.sender], "!slasher"); + _; + } + + /** + * @dev The staking contract is upgradeable however we still use the constructor to set + * a few immutable variables. + * @param controller The address of the Graph controller contract. + * @param subgraphDataServiceAddress The address of the subgraph data service. + */ + constructor( + address controller, + address subgraphDataServiceAddress + ) HorizonStakingBase(controller, subgraphDataServiceAddress) {} + + /// @inheritdoc IHorizonStakingExtension + function closeAllocation(address allocationID, bytes32 poi) external override notPaused { + _closeAllocation(allocationID, poi); + } + + /// @inheritdoc IHorizonStakingExtension + function collect(uint256 tokens, address allocationID) external override notPaused { + // Allocation identifier validation + require(allocationID != address(0), "!alloc"); + + // Allocation must exist + AllocationState allocState = _getAllocationState(allocationID); + require(allocState != AllocationState.Null, "!collect"); + + // If the query fees are zero, we don't want to revert + // but we also don't need to do anything, so just return + if (tokens == 0) { + return; + } + + Allocation storage alloc = __DEPRECATED_allocations[allocationID]; + bytes32 subgraphDeploymentID = alloc.subgraphDeploymentID; + + uint256 queryFees = tokens; // Tokens collected from the channel + uint256 protocolTax = 0; // Tokens burnt as protocol tax + uint256 curationFees = 0; // Tokens distributed to curators as curation fees + uint256 queryRebates = 0; // Tokens to distribute to indexer + uint256 delegationRewards = 0; // Tokens to distribute to delegators + + { + // -- Pull tokens from the sender -- + _graphToken().pullTokens(msg.sender, queryFees); + + // -- Collect protocol tax -- + protocolTax = _collectTax(queryFees, __DEPRECATED_protocolPercentage); + queryFees = queryFees - protocolTax; + + // -- Collect curation fees -- + // Only if the subgraph deployment is curated + curationFees = _collectCurationFees(subgraphDeploymentID, queryFees, __DEPRECATED_curationPercentage); + queryFees = queryFees - curationFees; + + // -- Process rebate reward -- + // Using accumulated fees and subtracting previously distributed rebates + // allows for multiple vouchers to be collected while following the rebate formula + alloc.collectedFees = alloc.collectedFees + queryFees; + + // No rebates if indexer has no stake or if lambda is zero + uint256 newRebates = (alloc.tokens == 0 || __DEPRECATED_lambdaNumerator == 0) + ? 0 + : ExponentialRebates.exponentialRebates( + alloc.collectedFees, + alloc.tokens, + __DEPRECATED_alphaNumerator, + __DEPRECATED_alphaDenominator, + __DEPRECATED_lambdaNumerator, + __DEPRECATED_lambdaDenominator + ); + + // -- Ensure rebates to distribute are within bounds -- + // Indexers can become under or over rebated if rebate parameters (alpha, lambda) + // change between successive collect calls for the same allocation + + // Ensure rebates to distribute are not negative (indexer is over-rebated) + queryRebates = MathUtils.diffOrZero(newRebates, alloc.distributedRebates); + + // Ensure rebates to distribute are not greater than available (indexer is under-rebated) + queryRebates = MathUtils.min(queryRebates, queryFees); + + // -- Burn rebates remanent -- + _graphToken().burnTokens(queryFees - queryRebates); + + // -- Distribute rebates -- + if (queryRebates > 0) { + alloc.distributedRebates = alloc.distributedRebates + queryRebates; + + // -- Collect delegation rewards into the delegation pool -- + delegationRewards = _collectDelegationQueryRewards(alloc.indexer, queryRebates); + queryRebates = queryRebates - delegationRewards; + + // -- Transfer or restake rebates -- + _sendRewards(queryRebates, alloc.indexer, __DEPRECATED_rewardsDestination[alloc.indexer] == address(0)); + } + } + + emit RebateCollected( + msg.sender, + alloc.indexer, + subgraphDeploymentID, + allocationID, + _graphEpochManager().currentEpoch(), + tokens, + protocolTax, + curationFees, + queryFees, + queryRebates, + delegationRewards + ); + } + + /// @inheritdoc IHorizonStakingExtension + function legacySlash( + address indexer, + uint256 tokens, + uint256 reward, + address beneficiary + ) external override onlySlasher notPaused { + ServiceProviderInternal storage indexerStake = _serviceProviders[indexer]; + + // Only able to slash a non-zero number of tokens + require(tokens > 0, "!tokens"); + + // Rewards comes from tokens slashed balance + require(tokens >= reward, "rewards>slash"); + + // Cannot slash stake of an indexer without any or enough stake + require(indexerStake.tokensStaked > 0, "!stake"); + require(tokens <= indexerStake.tokensStaked, "slash>stake"); + + // Validate beneficiary of slashed tokens + require(beneficiary != address(0), "!beneficiary"); + + // Slashing more tokens than freely available (over allocation condition) + // Unlock locked tokens to avoid the indexer to withdraw them + uint256 tokensUsed = indexerStake.__DEPRECATED_tokensAllocated + indexerStake.__DEPRECATED_tokensLocked; + uint256 tokensAvailable = tokensUsed > indexerStake.tokensStaked ? 0 : indexerStake.tokensStaked - tokensUsed; + if (tokens > tokensAvailable && indexerStake.__DEPRECATED_tokensLocked > 0) { + uint256 tokensOverAllocated = tokens - tokensAvailable; + uint256 tokensToUnlock = MathUtils.min(tokensOverAllocated, indexerStake.__DEPRECATED_tokensLocked); + indexerStake.__DEPRECATED_tokensLocked = indexerStake.__DEPRECATED_tokensLocked - tokensToUnlock; + if (indexerStake.__DEPRECATED_tokensLocked == 0) { + indexerStake.__DEPRECATED_tokensLockedUntil = 0; + } + } + + // Remove tokens to slash from the stake + indexerStake.tokensStaked = indexerStake.tokensStaked - tokens; + + // -- Interactions -- + + // Set apart the reward for the beneficiary and burn remaining slashed stake + _graphToken().burnTokens(tokens - reward); + + // Give the beneficiary a reward for slashing + _graphToken().pushTokens(beneficiary, reward); + + emit StakeSlashed(indexer, tokens, reward, beneficiary); + } + + /// @inheritdoc IHorizonStakingExtension + function isAllocation(address allocationID) external view override returns (bool) { + return _getAllocationState(allocationID) != AllocationState.Null; + } + + /// @inheritdoc IHorizonStakingExtension + function getAllocation(address allocationID) external view override returns (Allocation memory) { + return __DEPRECATED_allocations[allocationID]; + } + + /// @inheritdoc IRewardsIssuer + function getAllocationData( + address allocationID + ) external view override returns (address, bytes32, uint256, uint256, uint256) { + Allocation memory allo = __DEPRECATED_allocations[allocationID]; + return (allo.indexer, allo.subgraphDeploymentID, allo.tokens, allo.accRewardsPerAllocatedToken, 0); + } + + /// @inheritdoc IHorizonStakingExtension + function getAllocationState(address allocationID) external view override returns (AllocationState) { + return _getAllocationState(allocationID); + } + + /// @inheritdoc IRewardsIssuer + function getSubgraphAllocatedTokens(bytes32 subgraphDeploymentID) external view override returns (uint256) { + return __DEPRECATED_subgraphAllocations[subgraphDeploymentID]; + } + + /// @inheritdoc IHorizonStakingExtension + function getIndexerStakedTokens(address indexer) external view override returns (uint256) { + return _serviceProviders[indexer].tokensStaked; + } + + /// @inheritdoc IHorizonStakingExtension + function getSubgraphService() external view override returns (address) { + return SUBGRAPH_DATA_SERVICE_ADDRESS; + } + + /// @inheritdoc IHorizonStakingExtension + function hasStake(address indexer) external view override returns (bool) { + return _serviceProviders[indexer].tokensStaked > 0; + } + + /// @inheritdoc IHorizonStakingExtension + function __DEPRECATED_getThawingPeriod() external view returns (uint64) { + return __DEPRECATED_thawingPeriod; + } + + /// @inheritdoc IHorizonStakingExtension + function isOperator(address operator, address serviceProvider) public view override returns (bool) { + return _legacyOperatorAuth[serviceProvider][operator]; + } + + /** + * @dev Collect tax to burn for an amount of tokens. + * @param _tokens Total tokens received used to calculate the amount of tax to collect + * @param _percentage Percentage of tokens to burn as tax + * @return Amount of tax charged + */ + function _collectTax(uint256 _tokens, uint256 _percentage) private returns (uint256) { + uint256 tax = _tokens.mulPPMRoundUp(_percentage); + _graphToken().burnTokens(tax); // Burn tax if any + return tax; + } + + /** + * @dev Triggers an update of rewards due to a change in allocations. + * @param _subgraphDeploymentID Subgraph deployment updated + * @return Accumulated rewards per allocated token for the subgraph deployment + */ + function _updateRewards(bytes32 _subgraphDeploymentID) private returns (uint256) { + return _graphRewardsManager().onSubgraphAllocationUpdate(_subgraphDeploymentID); + } + + /** + * @dev Assign rewards for the closed allocation to indexer and delegators. + * @param _allocationID Allocation + * @param _indexer Address of the indexer that did the allocation + */ + function _distributeRewards(address _allocationID, address _indexer) private { + // Automatically triggers update of rewards snapshot as allocation will change + // after this call. Take rewards mint tokens for the Staking contract to distribute + // between indexer and delegators + uint256 totalRewards = _graphRewardsManager().takeRewards(_allocationID); + if (totalRewards == 0) { + return; + } + + // Calculate delegation rewards and add them to the delegation pool + uint256 delegationRewards = _collectDelegationIndexingRewards(_indexer, totalRewards); + uint256 indexerRewards = totalRewards - delegationRewards; + + // Send the indexer rewards + _sendRewards(indexerRewards, _indexer, __DEPRECATED_rewardsDestination[_indexer] == address(0)); + } + + /** + * @dev Send rewards to the appropriate destination. + * @param _tokens Number of rewards tokens + * @param _beneficiary Address of the beneficiary of rewards + * @param _restake Whether to restake or not + */ + function _sendRewards(uint256 _tokens, address _beneficiary, bool _restake) private { + if (_tokens == 0) return; + + if (_restake) { + // Restake to place fees into the indexer stake + _stake(_beneficiary, _tokens); + } else { + // Transfer funds to the beneficiary's designated rewards destination if set + address destination = __DEPRECATED_rewardsDestination[_beneficiary]; + _graphToken().pushTokens(destination == address(0) ? _beneficiary : destination, _tokens); + } + } + + /** + * @dev Close an allocation and free the staked tokens. + * @param _allocationID The allocation identifier + * @param _poi Proof of indexing submitted for the allocated period + */ + function _closeAllocation(address _allocationID, bytes32 _poi) private { + // Allocation must exist and be active + AllocationState allocState = _getAllocationState(_allocationID); + require(allocState == AllocationState.Active, "!active"); + + // Get allocation + Allocation memory alloc = __DEPRECATED_allocations[_allocationID]; + + // Validate that an allocation cannot be closed before one epoch + alloc.closedAtEpoch = _graphEpochManager().currentEpoch(); + uint256 epochs = MathUtils.diffOrZero(alloc.closedAtEpoch, alloc.createdAtEpoch); + + // Indexer or operator can close an allocation + // Anyone is allowed to close ONLY under two concurrent conditions + // - After maxAllocationEpochs passed + // - When the allocation is for non-zero amount of tokens + bool isIndexerOrOperator = msg.sender == alloc.indexer || isOperator(msg.sender, alloc.indexer); + if (epochs <= __DEPRECATED_maxAllocationEpochs || alloc.tokens == 0) { + require(isIndexerOrOperator, "!auth"); + } + + // Close the allocation + __DEPRECATED_allocations[_allocationID].closedAtEpoch = alloc.closedAtEpoch; + + // -- Rewards Distribution -- + + // Process non-zero-allocation rewards tracking + if (alloc.tokens > 0) { + // Distribute rewards if proof of indexing was presented by the indexer or operator + if (isIndexerOrOperator && _poi != 0) { + _distributeRewards(_allocationID, alloc.indexer); + } else { + _updateRewards(alloc.subgraphDeploymentID); + } + + // Free allocated tokens from use + _serviceProviders[alloc.indexer].__DEPRECATED_tokensAllocated = + _serviceProviders[alloc.indexer].__DEPRECATED_tokensAllocated - + alloc.tokens; + + // Track total allocations per subgraph + // Used for rewards calculations + __DEPRECATED_subgraphAllocations[alloc.subgraphDeploymentID] = + __DEPRECATED_subgraphAllocations[alloc.subgraphDeploymentID] - + alloc.tokens; + } + + emit AllocationClosed( + alloc.indexer, + alloc.subgraphDeploymentID, + alloc.closedAtEpoch, + alloc.tokens, + _allocationID, + msg.sender, + _poi, + !isIndexerOrOperator + ); + } + + /** + * @dev Collect the delegation rewards for query fees. + * This function will assign the collected fees to the delegation pool. + * @param _indexer Indexer to which the tokens to distribute are related + * @param _tokens Total tokens received used to calculate the amount of fees to collect + * @return Amount of delegation rewards + */ + function _collectDelegationQueryRewards(address _indexer, uint256 _tokens) private returns (uint256) { + uint256 delegationRewards = 0; + DelegationPoolInternal storage pool = _legacyDelegationPools[_indexer]; + if (pool.tokens > 0 && uint256(pool.__DEPRECATED_queryFeeCut).isValidPPM()) { + uint256 indexerCut = uint256(pool.__DEPRECATED_queryFeeCut).mulPPM(_tokens); + delegationRewards = _tokens - indexerCut; + pool.tokens = pool.tokens + delegationRewards; + } + return delegationRewards; + } + + /** + * @dev Collect the delegation rewards for indexing. + * This function will assign the collected fees to the delegation pool. + * @param _indexer Indexer to which the tokens to distribute are related + * @param _tokens Total tokens received used to calculate the amount of fees to collect + * @return Amount of delegation rewards + */ + function _collectDelegationIndexingRewards(address _indexer, uint256 _tokens) private returns (uint256) { + uint256 delegationRewards = 0; + DelegationPoolInternal storage pool = _legacyDelegationPools[_indexer]; + if (pool.tokens > 0 && uint256(pool.__DEPRECATED_indexingRewardCut).isValidPPM()) { + uint256 indexerCut = uint256(pool.__DEPRECATED_indexingRewardCut).mulPPM(_tokens); + delegationRewards = _tokens - indexerCut; + pool.tokens = pool.tokens + delegationRewards; + } + return delegationRewards; + } + + /** + * @dev Collect the curation fees for a subgraph deployment from an amount of tokens. + * This function transfer curation fees to the Curation contract by calling Curation.collect + * @param _subgraphDeploymentID Subgraph deployment to which the curation fees are related + * @param _tokens Total tokens received used to calculate the amount of fees to collect + * @param _curationCut Percentage of tokens to collect as fees + * @return Amount of curation fees + */ + function _collectCurationFees( + bytes32 _subgraphDeploymentID, + uint256 _tokens, + uint256 _curationCut + ) private returns (uint256) { + if (_tokens == 0) { + return 0; + } + + ICuration curation = _graphCuration(); + bool isCurationEnabled = _curationCut > 0 && address(curation) != address(0); + + if (isCurationEnabled && curation.isCurated(_subgraphDeploymentID)) { + uint256 curationFees = _tokens.mulPPMRoundUp(_curationCut); + if (curationFees > 0) { + // Transfer and call collect() + // This function transfer tokens to a trusted protocol contracts + // Then we call collect() to do the transfer Bookkeeping + _graphRewardsManager().onSubgraphSignalUpdate(_subgraphDeploymentID); + _graphToken().pushTokens(address(curation), curationFees); + curation.collect(_subgraphDeploymentID, curationFees); + } + return curationFees; + } + return 0; + } + + /** + * @dev Return the current state of an allocation + * @param _allocationID Allocation identifier + * @return AllocationState enum with the state of the allocation + */ + function _getAllocationState(address _allocationID) private view returns (AllocationState) { + Allocation storage alloc = __DEPRECATED_allocations[_allocationID]; + + if (alloc.indexer == address(0)) { + return AllocationState.Null; + } + + if (alloc.createdAtEpoch != 0 && alloc.closedAtEpoch == 0) { + return AllocationState.Active; + } + + return AllocationState.Closed; + } +} diff --git a/packages/horizon/contracts/staking/HorizonStakingStorage.sol b/packages/horizon/contracts/staking/HorizonStakingStorage.sol new file mode 100644 index 000000000..15ea0cce0 --- /dev/null +++ b/packages/horizon/contracts/staking/HorizonStakingStorage.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IHorizonStakingExtension } from "../interfaces/internal/IHorizonStakingExtension.sol"; +import { IHorizonStakingTypes } from "../interfaces/internal/IHorizonStakingTypes.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; + +import { LinkedList } from "../libraries/LinkedList.sol"; + +/* solhint-disable max-states-count */ + +/** + * @title HorizonStakingV1Storage + * @notice This contract holds all the storage variables for the Staking contract. + * @dev Deprecated variables are kept to support the transition to Horizon Staking. + * They can eventually be collapsed into a single storage slot. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract HorizonStakingV1Storage { + // -- Staking -- + + /// @dev Minimum amount of tokens an indexer needs to stake. + /// Deprecated, now enforced by each data service (verifier) + uint256 internal __DEPRECATED_minimumIndexerStake; + + /// @dev Time in blocks to unstake + /// Deprecated, now enforced by each data service (verifier) + uint32 internal __DEPRECATED_thawingPeriod; // in blocks + + /// @dev Percentage of fees going to curators + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + /// Deprecated, now enforced by each data service (verifier) + uint32 internal __DEPRECATED_curationPercentage; + + /// @dev Percentage of fees burned as protocol fee + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + /// Deprecated, now enforced by each data service (verifier) + uint32 internal __DEPRECATED_protocolPercentage; + + /// @dev Period for allocation to be finalized + /// Deprecated with exponential rebates. + uint32 private __DEPRECATED_channelDisputeEpochs; + + /// @dev Maximum allocation time. + /// Deprecated, allocations now live on the subgraph service contract. + uint32 internal __DEPRECATED_maxAllocationEpochs; + + /// @dev Rebate alpha numerator + /// Originally used for Cobb-Douglas rebates, now used for exponential rebates + /// Deprecated, any rebate mechanism is now applied on the subgraph data service. + uint32 internal __DEPRECATED_alphaNumerator; + + /// @dev Rebate alpha denominator + /// Originally used for Cobb-Douglas rebates, now used for exponential rebates + /// Deprecated, any rebate mechanism is now applied on the subgraph data service. + uint32 internal __DEPRECATED_alphaDenominator; + + /// @dev Service providers details, tracks stake utilization. + mapping(address serviceProvider => IHorizonStakingTypes.ServiceProviderInternal details) internal _serviceProviders; + + /// @dev Allocation details. + /// Deprecated, now applied on the subgraph data service + mapping(address allocationId => IHorizonStakingExtension.Allocation allocation) internal __DEPRECATED_allocations; + + /// @dev Subgraph allocations, tracks the tokens allocated to a subgraph deployment + /// Deprecated, now applied on the SubgraphService + mapping(bytes32 subgraphDeploymentId => uint256 tokens) internal __DEPRECATED_subgraphAllocations; + + /// @dev Rebate pool details per epoch + /// Deprecated with exponential rebates. + mapping(uint256 epoch => uint256 rebates) private __DEPRECATED_rebates; + + // -- Slashing -- + + /// @dev List of addresses allowed to slash stakes + /// Deprecated, now each verifier can slash the corresponding provision. + mapping(address slasher => bool allowed) internal __DEPRECATED_slashers; + + // -- Delegation -- + + /// @dev Delegation capacity multiplier defined by the delegation ratio + /// Deprecated, enforced by each data service as needed. + uint32 internal __DEPRECATED_delegationRatio; + + /// @dev Time in blocks an indexer needs to wait to change delegation parameters + /// Deprecated, enforced by each data service as needed. + uint32 internal __DEPRECATED_delegationParametersCooldown; + + /// @dev Time in epochs a delegator needs to wait to withdraw delegated stake + /// Deprecated, now only enforced during a transition period + uint32 internal __DEPRECATED_delegationUnbondingPeriod; + + /// @dev Percentage of tokens to tax a delegation deposit + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + /// Deprecated, no tax is applied now. + uint32 internal __DEPRECATED_delegationTaxPercentage; + + /// @dev Delegation pools (legacy). + /// Only used when the verifier is the subgraph data service. + mapping(address serviceProvider => IHorizonStakingTypes.DelegationPoolInternal delegationPool) + internal _legacyDelegationPools; + + // -- Operators -- + + /// @dev Operator allow list (legacy) + /// Only used when the verifier is the subgraph data service. + mapping(address serviceProvider => mapping(address legacyOperator => bool authorized)) internal _legacyOperatorAuth; + + // -- Asset Holders -- + + /// @dev Asset holder allow list + /// Deprecated with permissionless payers + mapping(address assetHolder => bool allowed) private __DEPRECATED_assetHolders; + + /// @dev Destination of accrued indexing rewards + /// Deprecated, defined by each data service as needed + mapping(address serviceProvider => address rewardsDestination) internal __DEPRECATED_rewardsDestination; + + /// @dev Address of the counterpart Staking contract on L1/L2 + /// Deprecated, transfer tools no longer enabled. + address internal __DEPRECATED_counterpartStakingAddress; + + /// @dev Address of the StakingExtension implementation + /// This is now an immutable variable to save some gas. + address internal __DEPRECATED_extensionImpl; + + /// @dev Rebate lambda numerator for exponential rebates + /// Deprecated, any rebate mechanism is now applied on the subgraph data service. + uint32 internal __DEPRECATED_lambdaNumerator; + + /// @dev Rebate lambda denominator for exponential rebates + /// Deprecated, any rebate mechanism is now applied on the subgraph data service. + uint32 internal __DEPRECATED_lambdaDenominator; + + // -- Horizon Staking -- + + /// @dev Maximum thawing period, in seconds, for a provision + uint64 internal _maxThawingPeriod; + + /// @dev Provisions from each service provider for each data service + mapping(address serviceProvider => mapping(address verifier => IHorizonStakingTypes.Provision provision)) + internal _provisions; + + /// @dev Delegation fee cuts for each service provider on each provision, by fee type: + /// This is the effective delegator fee cuts for each (data-service-defined) fee type (e.g. indexing fees, query fees). + /// This is in PPM and is the cut taken by the service provider from the fees that correspond to delegators. + /// (based on stake vs delegated stake proportion). + /// The cuts are applied in GraphPayments so apply to all data services that use it. + mapping(address serviceProvider => mapping(address verifier => mapping(IGraphPayments.PaymentTypes paymentType => uint256 feeCut))) + internal _delegationFeeCut; + + /// @dev Thaw requests + /// Details for each thawing operation in the staking contract (for both service providers and delegators). + mapping(IHorizonStakingTypes.ThawRequestType thawRequestType => mapping(bytes32 thawRequestId => IHorizonStakingTypes.ThawRequest thawRequest)) + internal _thawRequests; + + /// @dev Thaw request lists + /// Metadata defining linked lists of thaw requests for each service provider or delegator (owner) + mapping(IHorizonStakingTypes.ThawRequestType thawRequestType => mapping(address serviceProvider => mapping(address verifier => mapping(address owner => LinkedList.List list)))) + internal _thawRequestLists; + + /// @dev Operator allow list + /// Used for all verifiers except the subgraph data service. + mapping(address serviceProvider => mapping(address verifier => mapping(address operator => bool authorized))) + internal _operatorAuth; + + /// @dev Flag to enable or disable delegation slashing + bool internal _delegationSlashingEnabled; + + /// @dev Delegation pools for each service provider and verifier + mapping(address serviceProvider => mapping(address verifier => IHorizonStakingTypes.DelegationPoolInternal delegationPool)) + internal _delegationPools; + + /// @dev Allowed verifiers for locked provisions (i.e. from GraphTokenLockWallets) + // Verifiers are whitelisted to ensure locked tokens cannot escape using an arbitrary verifier. + mapping(address verifier => bool allowed) internal _allowedLockedVerifiers; +} diff --git a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol new file mode 100644 index 000000000..b137079b3 --- /dev/null +++ b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { LibFixedMath } from "../../libraries/LibFixedMath.sol"; + +/** + * @title ExponentialRebates library + * @notice A library to compute query fee rebates using an exponential formula + * @dev This is only used for backwards compatibility in HorizonStaking, and should + * be removed after the transition period. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library ExponentialRebates { + /// @dev Maximum value of the exponent for which to compute the exponential before clamping to zero. + uint32 private constant MAX_EXPONENT = 15; + + /// @dev The exponential formula used to compute fee-based rewards for + /// staking pools in a given epoch. This function does not perform + /// bounds checking on the inputs, but the following conditions + /// need to be true: + /// 0 <= alphaNumerator / alphaDenominator <= 1 + /// 0 < lambdaNumerator / lambdaDenominator + /// The exponential rebates function has the form: + /// `(1 - alpha * exp ^ (-lambda * stake / fees)) * fees` + /// @param fees Fees generated by indexer in the staking pool. + /// @param stake Stake attributed to the indexer in the staking pool. + /// @param alphaNumerator Numerator of `alpha` in the rebates function. + /// @param alphaDenominator Denominator of `alpha` in the rebates function. + /// @param lambdaNumerator Numerator of `lambda` in the rebates function. + /// @param lambdaDenominator Denominator of `lambda` in the rebates function. + /// @return rewards Rewards owed to the staking pool. + function exponentialRebates( + uint256 fees, + uint256 stake, + uint32 alphaNumerator, + uint32 alphaDenominator, + uint32 lambdaNumerator, + uint32 lambdaDenominator + ) external pure returns (uint256) { + // If alpha is zero indexer gets 100% fees rebate + int256 alpha = LibFixedMath.toFixed(int32(alphaNumerator), int32(alphaDenominator)); + if (alpha == 0) { + return fees; + } + + // No rebates if no fees... + if (fees == 0) { + return 0; + } + + // Award all fees as rebate if the exponent is too large + int256 lambda = LibFixedMath.toFixed(int32(lambdaNumerator), int32(lambdaDenominator)); + int256 exponent = LibFixedMath.mulDiv(lambda, int256(stake), int256(fees)); + if (LibFixedMath.toInteger(exponent) > int256(uint256(MAX_EXPONENT))) { + return fees; + } + + // Compute `1 - alpha * exp ^(-exponent)` + int256 factor = LibFixedMath.sub(LibFixedMath.one(), LibFixedMath.mul(alpha, LibFixedMath.exp(-exponent))); + + // Weight the fees by the factor + return LibFixedMath.uintMul(factor, fees); + } +} diff --git a/packages/horizon/contracts/staking/utilities/Managed.sol b/packages/horizon/contracts/staking/utilities/Managed.sol new file mode 100644 index 000000000..c1b466870 --- /dev/null +++ b/packages/horizon/contracts/staking/utilities/Managed.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; + +/* solhint-disable var-name-mixedcase */ + +/** + * @title Graph Managed contract + * @dev The Managed contract provides an interface to interact with the Controller. + * For Graph Horizon this contract is mostly a shell that uses {GraphDirectory}, however since the {HorizonStaking} + * contract uses it we need to preserve the storage layout. + * Inspired by Livepeer: https://github.com/livepeer/protocol/blob/streamflow/contracts/Controller.sol + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract Managed is GraphDirectory { + // -- State -- + + /// @notice Controller that manages this contract + address private __DEPRECATED_controller; + + /// @dev Cache for the addresses of the contracts retrieved from the controller + mapping(bytes32 contractName => address contractAddress) private __DEPRECATED_addressCache; + + /// @dev Gap for future storage variables + uint256[10] private __gap; + + /** + * @notice Thrown when a protected function is called and the contract is paused. + */ + error ManagedIsPaused(); + + /** + * @notice Thrown when a the caller is not the expected controller address. + */ + error ManagedOnlyController(); + + /** + * @notice Thrown when a the caller is not the governor. + */ + error ManagedOnlyGovernor(); + + /** + * @dev Revert if the controller is paused + */ + modifier notPaused() { + require(!_graphController().paused(), ManagedIsPaused()); + _; + } + + /** + * @dev Revert if the caller is not the Controller + */ + modifier onlyController() { + require(msg.sender == address(_graphController()), ManagedOnlyController()); + _; + } + + /** + * @dev Revert if the caller is not the governor + */ + modifier onlyGovernor() { + require(msg.sender == _graphController().getGovernor(), ManagedOnlyGovernor()); + _; + } + + /** + * @dev Initialize the contract + * @param controller_ The address of the Graph controller contract. + */ + constructor(address controller_) GraphDirectory(controller_) {} +} diff --git a/packages/horizon/contracts/utilities/Authorizable.sol b/packages/horizon/contracts/utilities/Authorizable.sol new file mode 100644 index 000000000..5d164c2c3 --- /dev/null +++ b/packages/horizon/contracts/utilities/Authorizable.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IAuthorizable } from "../interfaces/IAuthorizable.sol"; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +/** + * @title Authorizable contract + * @dev Implements the {IAuthorizable} interface. + * @notice A mechanism to authorize signers to sign messages on behalf of an authorizer. + * Signers cannot be reused for different authorizers. + * @dev Contract uses "authorizeSignerProof" as the domain for signer proofs. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract Authorizable is IAuthorizable { + /// @notice The duration (in seconds) for which an authorization is thawing before it can be revoked + uint256 public immutable REVOKE_AUTHORIZATION_THAWING_PERIOD; + + /// @notice Authorization details for authorizer-signer pairs + mapping(address signer => Authorization authorization) public authorizations; + + /** + * @dev Revert if the caller has not authorized the signer + * @param signer The address of the signer + */ + modifier onlyAuthorized(address signer) { + _requireAuthorized(msg.sender, signer); + _; + } + + /** + * @notice Constructs a new instance of the Authorizable contract. + * @param revokeAuthorizationThawingPeriod The duration (in seconds) for which an authorization is thawing before it can be revoked. + */ + constructor(uint256 revokeAuthorizationThawingPeriod) { + REVOKE_AUTHORIZATION_THAWING_PERIOD = revokeAuthorizationThawingPeriod; + } + + /// @inheritdoc IAuthorizable + function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external { + require( + authorizations[signer].authorizer == address(0), + AuthorizableSignerAlreadyAuthorized( + authorizations[signer].authorizer, + signer, + authorizations[signer].revoked + ) + ); + _verifyAuthorizationProof(proof, proofDeadline, signer); + authorizations[signer].authorizer = msg.sender; + emit SignerAuthorized(msg.sender, signer); + } + + /// @inheritdoc IAuthorizable + function thawSigner(address signer) external onlyAuthorized(signer) { + authorizations[signer].thawEndTimestamp = block.timestamp + REVOKE_AUTHORIZATION_THAWING_PERIOD; + emit SignerThawing(msg.sender, signer, authorizations[signer].thawEndTimestamp); + } + + /// @inheritdoc IAuthorizable + function cancelThawSigner(address signer) external onlyAuthorized(signer) { + require(authorizations[signer].thawEndTimestamp > 0, AuthorizableSignerNotThawing(signer)); + uint256 thawEnd = authorizations[signer].thawEndTimestamp; + authorizations[signer].thawEndTimestamp = 0; + emit SignerThawCanceled(msg.sender, signer, thawEnd); + } + + /// @inheritdoc IAuthorizable + function revokeAuthorizedSigner(address signer) external onlyAuthorized(signer) { + uint256 thawEndTimestamp = authorizations[signer].thawEndTimestamp; + require(thawEndTimestamp > 0, AuthorizableSignerNotThawing(signer)); + require(thawEndTimestamp <= block.timestamp, AuthorizableSignerStillThawing(block.timestamp, thawEndTimestamp)); + authorizations[signer].revoked = true; + emit SignerRevoked(msg.sender, signer); + } + + /// @inheritdoc IAuthorizable + function getThawEnd(address signer) external view returns (uint256) { + return authorizations[signer].thawEndTimestamp; + } + + /// @inheritdoc IAuthorizable + function isAuthorized(address authorizer, address signer) external view returns (bool) { + return _isAuthorized(authorizer, signer); + } + + /** + * @notice Returns true if the signer is authorized by the authorizer + * @param _authorizer The address of the authorizer + * @param _signer The address of the signer + * @return true if the signer is authorized by the authorizer, false otherwise + */ + function _isAuthorized(address _authorizer, address _signer) internal view returns (bool) { + return (_authorizer != address(0) && + authorizations[_signer].authorizer == _authorizer && + !authorizations[_signer].revoked); + } + + /** + * @notice Reverts if the authorizer has not authorized the signer + * @param _authorizer The address of the authorizer + * @param _signer The address of the signer + */ + function _requireAuthorized(address _authorizer, address _signer) internal view { + require(_isAuthorized(_authorizer, _signer), AuthorizableSignerNotAuthorized(_authorizer, _signer)); + } + + /** + * @notice Verify the authorization proof provided by the authorizer + * @param _proof The proof provided by the authorizer + * @param _proofDeadline The deadline by which the proof must be verified + * @param _signer The authorization recipient + */ + function _verifyAuthorizationProof(bytes calldata _proof, uint256 _proofDeadline, address _signer) private view { + // Check that the proofDeadline has not passed + require( + _proofDeadline > block.timestamp, + AuthorizableInvalidSignerProofDeadline(_proofDeadline, block.timestamp) + ); + + // Generate the message hash + bytes32 messageHash = keccak256( + abi.encodePacked(block.chainid, address(this), "authorizeSignerProof", _proofDeadline, msg.sender) + ); + + // Generate the allegedly signed digest + bytes32 digest = MessageHashUtils.toEthSignedMessageHash(messageHash); + + // Verify that the recovered signer matches the to be authorized signer + require(ECDSA.recover(digest, _proof) == _signer, AuthorizableInvalidSignerProof()); + } +} diff --git a/packages/horizon/contracts/utilities/GraphDirectory.sol b/packages/horizon/contracts/utilities/GraphDirectory.sol new file mode 100644 index 000000000..418b58619 --- /dev/null +++ b/packages/horizon/contracts/utilities/GraphDirectory.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IHorizonStaking } from "../interfaces/IHorizonStaking.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; +import { IPaymentsEscrow } from "../interfaces/IPaymentsEscrow.sol"; + +import { IController } from "@graphprotocol/contracts/contracts/governance/IController.sol"; +import { IEpochManager } from "@graphprotocol/contracts/contracts/epochs/IEpochManager.sol"; +import { IRewardsManager } from "@graphprotocol/contracts/contracts/rewards/IRewardsManager.sol"; +import { ITokenGateway } from "@graphprotocol/contracts/contracts/arbitrum/ITokenGateway.sol"; +import { IGraphProxyAdmin } from "../interfaces/IGraphProxyAdmin.sol"; + +import { ICuration } from "@graphprotocol/contracts/contracts/curation/ICuration.sol"; + +/** + * @title GraphDirectory contract + * @notice This contract is meant to be inherited by other contracts that + * need to keep track of the addresses in Graph Horizon contracts. + * It fetches the addresses from the Controller supplied during construction, + * and uses immutable variables to minimize gas costs. + */ +abstract contract GraphDirectory { + // -- Graph Horizon contracts -- + + /// @notice The Graph Token contract address + IGraphToken private immutable GRAPH_TOKEN; + + /// @notice The Horizon Staking contract address + IHorizonStaking private immutable GRAPH_STAKING; + + /// @notice The Graph Payments contract address + IGraphPayments private immutable GRAPH_PAYMENTS; + + /// @notice The Payments Escrow contract address + IPaymentsEscrow private immutable GRAPH_PAYMENTS_ESCROW; + + // -- Graph periphery contracts -- + + /// @notice The Graph Controller contract address + IController private immutable GRAPH_CONTROLLER; + + /// @notice The Epoch Manager contract address + IEpochManager private immutable GRAPH_EPOCH_MANAGER; + + /// @notice The Rewards Manager contract address + IRewardsManager private immutable GRAPH_REWARDS_MANAGER; + + /// @notice The Token Gateway contract address + ITokenGateway private immutable GRAPH_TOKEN_GATEWAY; + + /// @notice The Graph Proxy Admin contract address + IGraphProxyAdmin private immutable GRAPH_PROXY_ADMIN; + + // -- Legacy Graph contracts -- + // These are required for backwards compatibility on HorizonStakingExtension + // TRANSITION PERIOD: remove these once HorizonStakingExtension is removed + + /// @notice The Curation contract address + ICuration private immutable GRAPH_CURATION; + + /** + * @notice Emitted when the GraphDirectory is initialized + * @param graphToken The Graph Token contract address + * @param graphStaking The Horizon Staking contract address + * @param graphPayments The Graph Payments contract address + * @param graphEscrow The Payments Escrow contract address + * @param graphController The Graph Controller contract address + * @param graphEpochManager The Epoch Manager contract address + * @param graphRewardsManager The Rewards Manager contract address + * @param graphTokenGateway The Token Gateway contract address + * @param graphProxyAdmin The Graph Proxy Admin contract address + * @param graphCuration The Curation contract address + */ + event GraphDirectoryInitialized( + address indexed graphToken, + address indexed graphStaking, + address graphPayments, + address graphEscrow, + address indexed graphController, + address graphEpochManager, + address graphRewardsManager, + address graphTokenGateway, + address graphProxyAdmin, + address graphCuration + ); + + /** + * @notice Thrown when either the controller is the zero address or a contract address is not found + * on the controller + * @param contractName The name of the contract that was not found, or the controller + */ + error GraphDirectoryInvalidZeroAddress(bytes contractName); + + /** + * @notice Constructor for the GraphDirectory contract + * @dev Requirements: + * - `controller` cannot be zero address + * + * Emits a {GraphDirectoryInitialized} event + * + * @param controller The address of the Graph Controller contract. + */ + constructor(address controller) { + require(controller != address(0), GraphDirectoryInvalidZeroAddress("Controller")); + + GRAPH_CONTROLLER = IController(controller); + GRAPH_TOKEN = IGraphToken(_getContractFromController("GraphToken")); + GRAPH_STAKING = IHorizonStaking(_getContractFromController("Staking")); + GRAPH_PAYMENTS = IGraphPayments(_getContractFromController("GraphPayments")); + GRAPH_PAYMENTS_ESCROW = IPaymentsEscrow(_getContractFromController("PaymentsEscrow")); + GRAPH_EPOCH_MANAGER = IEpochManager(_getContractFromController("EpochManager")); + GRAPH_REWARDS_MANAGER = IRewardsManager(_getContractFromController("RewardsManager")); + GRAPH_TOKEN_GATEWAY = ITokenGateway(_getContractFromController("GraphTokenGateway")); + GRAPH_PROXY_ADMIN = IGraphProxyAdmin(_getContractFromController("GraphProxyAdmin")); + GRAPH_CURATION = ICuration(_getContractFromController("Curation")); + + emit GraphDirectoryInitialized( + address(GRAPH_TOKEN), + address(GRAPH_STAKING), + address(GRAPH_PAYMENTS), + address(GRAPH_PAYMENTS_ESCROW), + address(GRAPH_CONTROLLER), + address(GRAPH_EPOCH_MANAGER), + address(GRAPH_REWARDS_MANAGER), + address(GRAPH_TOKEN_GATEWAY), + address(GRAPH_PROXY_ADMIN), + address(GRAPH_CURATION) + ); + } + + /** + * @notice Get the Graph Token contract + * @return The Graph Token contract + */ + function _graphToken() internal view returns (IGraphToken) { + return GRAPH_TOKEN; + } + + /** + * @notice Get the Horizon Staking contract + * @return The Horizon Staking contract + */ + function _graphStaking() internal view returns (IHorizonStaking) { + return GRAPH_STAKING; + } + + /** + * @notice Get the Graph Payments contract + * @return The Graph Payments contract + */ + function _graphPayments() internal view returns (IGraphPayments) { + return GRAPH_PAYMENTS; + } + + /** + * @notice Get the Payments Escrow contract + * @return The Payments Escrow contract + */ + function _graphPaymentsEscrow() internal view returns (IPaymentsEscrow) { + return GRAPH_PAYMENTS_ESCROW; + } + + /** + * @notice Get the Graph Controller contract + * @return The Graph Controller contract + */ + function _graphController() internal view returns (IController) { + return GRAPH_CONTROLLER; + } + + /** + * @notice Get the Epoch Manager contract + * @return The Epoch Manager contract + */ + function _graphEpochManager() internal view returns (IEpochManager) { + return GRAPH_EPOCH_MANAGER; + } + + /** + * @notice Get the Rewards Manager contract + * @return The Rewards Manager contract address + */ + function _graphRewardsManager() internal view returns (IRewardsManager) { + return GRAPH_REWARDS_MANAGER; + } + + /** + * @notice Get the Graph Token Gateway contract + * @return The Graph Token Gateway contract + */ + function _graphTokenGateway() internal view returns (ITokenGateway) { + return GRAPH_TOKEN_GATEWAY; + } + + /** + * @notice Get the Graph Proxy Admin contract + * @return The Graph Proxy Admin contract + */ + function _graphProxyAdmin() internal view returns (IGraphProxyAdmin) { + return GRAPH_PROXY_ADMIN; + } + + /** + * @notice Get the Curation contract + * @return The Curation contract + */ + function _graphCuration() internal view returns (ICuration) { + return GRAPH_CURATION; + } + + /** + * @notice Get a contract address from the controller + * @dev Requirements: + * - The `_contractName` must be registered in the controller + * @param _contractName The name of the contract to fetch from the controller + * @return The address of the contract + */ + function _getContractFromController(bytes memory _contractName) private view returns (address) { + address contractAddress = GRAPH_CONTROLLER.getContractProxy(keccak256(_contractName)); + require(contractAddress != address(0), GraphDirectoryInvalidZeroAddress(_contractName)); + return contractAddress; + } +} diff --git a/packages/horizon/eslint.config.js b/packages/horizon/eslint.config.js new file mode 100644 index 000000000..c9e06b116 --- /dev/null +++ b/packages/horizon/eslint.config.js @@ -0,0 +1,21 @@ +// @ts-check +/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +const eslintGraphConfig = require('eslint-graph-config') +module.exports = [ + ...eslintGraphConfig.default, + { + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + }, + }, + { + ignores: ['typechain-types/*', 'lib/*'], + }, +] diff --git a/packages/horizon/foundry.toml b/packages/horizon/foundry.toml new file mode 100644 index 000000000..552938288 --- /dev/null +++ b/packages/horizon/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +src = 'contracts' +out = 'build' +libs = ['node_modules', 'lib'] +test = 'test' +cache_path = 'cache_forge' +fs_permissions = [{ access = "read", path = "./"}] +optimizer = true +optimizer_runs = 100 \ No newline at end of file diff --git a/packages/horizon/hardhat.config.ts b/packages/horizon/hardhat.config.ts new file mode 100644 index 000000000..494de23d5 --- /dev/null +++ b/packages/horizon/hardhat.config.ts @@ -0,0 +1,31 @@ +import { hardhatBaseConfig } from 'hardhat-graph-protocol/sdk' + +// Hardhat plugins +import '@nomicfoundation/hardhat-foundry' +import '@nomicfoundation/hardhat-toolbox' +import '@nomicfoundation/hardhat-ignition-ethers' +import 'hardhat-storage-layout' +import 'hardhat-contract-sizer' +import 'hardhat-secure-accounts' +import { HardhatUserConfig } from 'hardhat/types' + +// Skip importing hardhat-graph-protocol when building the project, it has circular dependency +if (process.env.BUILD_RUN !== 'true') { + require('hardhat-graph-protocol') + require('./tasks/deploy') +} + +const config: HardhatUserConfig = { + ...hardhatBaseConfig, + solidity: { + version: '0.8.27', + settings: { + optimizer: { + enabled: true, + runs: 20, + }, + }, + }, +} + +export default config diff --git a/packages/horizon/ignition/configs/migrate.default.json5 b/packages/horizon/ignition/configs/migrate.default.json5 new file mode 100644 index 000000000..6061e1216 --- /dev/null +++ b/packages/horizon/ignition/configs/migrate.default.json5 @@ -0,0 +1,50 @@ +{ + "$global": { + // Accounts + "governor": "0x72ee30d43Fb5A90B3FE983156C5d2fBE6F6d07B3", + + // Addresses for contracts deployed in the original Graph Protocol + "graphProxyAdminAddress": "0x7474a6cc5fAeDEc620Db0fa8E4da6eD58477042C", + "controllerAddress": "0x9DB3ee191681f092607035d9BDA6e59FbEaCa695", + "horizonStakingAddress": "0x865365C425f3A593Ffe698D9c4E6707D14d51e08", + "epochManagerAddress": "0x88b3C7f37253bAA1A9b95feAd69bD5320585826D", + "graphTokenAddress": "0xf8c05dCF59E8B28BFD5eed176C562bEbcfc7Ac04", + "graphTokenGatewayAddress": "0xB24Ce0f8c18c4DdDa584A7EeC132F49C966813bb", + "rewardsManagerAddress": "0x1F49caE7669086c8ba53CC35d1E9f80176d67E79", + "curationAddress": "0xDe761f075200E75485F4358978FB4d1dC8644FD5", + + // Must be set for step 3 and 4 of the migration + "subgraphServiceAddress": "", + + // Parameters + "maxThawingPeriod": 2419200 + }, + "GraphPayments": { + "protocolPaymentCut": 10000 + }, + "PaymentsEscrow": { + "withdrawEscrowThawingPeriod": 10000 + }, + "GraphTallyCollector": { + "eip712Name": "GraphTallyCollector", + "eip712Version": "1", + "revokeSignerThawingPeriod": 10000 + }, + "HorizonProxiesGovernor": { + // Must be set for step 2 of the migration + "graphPaymentsAddress": "", + "paymentsEscrowAddress": "" + }, + "HorizonStakingGovernor": { + // Must be set for step 4 of the migration + "horizonStakingImplementationAddress": "" + }, + "L2CurationGovernor": { + // Must be set for step 4 of the migration + "curationImplementationAddress": "" + }, + "RewardsManagerGovernor": { + // Must be set for step 4 of the migration + "rewardsManagerImplementationAddress": "" + } +} diff --git a/packages/horizon/ignition/configs/protocol.default.json5 b/packages/horizon/ignition/configs/protocol.default.json5 new file mode 100644 index 000000000..3849fb93a --- /dev/null +++ b/packages/horizon/ignition/configs/protocol.default.json5 @@ -0,0 +1,36 @@ +{ + "$global": { + "pauseGuardian": "0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC", + + // Placeholder address for a standalone Horizon deployment, see README.md for more details + "subgraphServiceAddress": "0x0000000000000000000000000000000000000000" + }, + "RewardsManager": { + "subgraphAvailabilityOracle": "0xd03ea8624C8C5987235048901fB614fDcA89b117", + "issuancePerBlock": "114155251141552511415n" + }, + "EpochManager": { + "epochLength": 60 + }, + "L2Curation": { + "curationTaxPercentage": 10000, + "minimumCurationDeposit": 1 + }, + "L2GraphToken": { + "initialSupply": "10000000000000000000000000000n" + }, + "HorizonStaking": { + "maxThawingPeriod": 2419200 + }, + "GraphPayments": { + "protocolPaymentCut": 10000 + }, + "PaymentsEscrow": { + "withdrawEscrowThawingPeriod": 10000 + }, + "GraphTallyCollector": { + "eip712Name": "GraphTallyCollector", + "eip712Version": "1", + "revokeSignerThawingPeriod": 10000 + } +} diff --git a/packages/horizon/ignition/modules/core/GraphPayments.ts b/packages/horizon/ignition/modules/core/GraphPayments.ts new file mode 100644 index 000000000..56aa73f60 --- /dev/null +++ b/packages/horizon/ignition/modules/core/GraphPayments.ts @@ -0,0 +1,70 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployImplementation } from '../proxy/implementation' +import { upgradeTransparentUpgradeableProxy } from '../proxy/TransparentUpgradeableProxy' + +import GraphPeripheryModule, { MigratePeripheryModule } from '../periphery/periphery' +import HorizonProxiesModule, { MigrateHorizonProxiesDeployerModule } from './HorizonProxies' + +import GraphPaymentsArtifact from '../../../build/contracts/contracts/payments/GraphPayments.sol/GraphPayments.json' + +export default buildModule('GraphPayments', (m) => { + const { Controller } = m.useModule(GraphPeripheryModule) + const { GraphPaymentsProxyAdmin, GraphPaymentsProxy } = m.useModule(HorizonProxiesModule) + + const governor = m.getAccount(1) + const protocolPaymentCut = m.getParameter('protocolPaymentCut') + + // Deploy GraphPayments implementation - requires periphery and proxies to be registered in the controller + const GraphPaymentsImplementation = deployImplementation(m, { + name: 'GraphPayments', + artifact: GraphPaymentsArtifact, + constructorArgs: [Controller, protocolPaymentCut], + }, { after: [GraphPeripheryModule, HorizonProxiesModule] }) + + // Upgrade proxy to implementation contract + const GraphPayments = upgradeTransparentUpgradeableProxy(m, + GraphPaymentsProxyAdmin, + GraphPaymentsProxy, + GraphPaymentsImplementation, { + name: 'GraphPayments', + artifact: GraphPaymentsArtifact, + initArgs: [], + }) + + m.call(GraphPaymentsProxyAdmin, 'transferOwnership', [governor], { after: [GraphPayments] }) + + return { GraphPayments, GraphPaymentsProxyAdmin, GraphPaymentsImplementation } +}) + +// Note that this module requires MigrateHorizonProxiesGovernorModule to be executed first +// The dependency is not made explicit to support the production workflow where the governor is a +// multisig owned by the Graph Council. +// For testnet, the dependency can be made explicit by having a parent module establish it. +export const MigrateGraphPaymentsModule = buildModule('GraphPayments', (m) => { + const { GraphPaymentsProxyAdmin, GraphPaymentsProxy } = m.useModule(MigrateHorizonProxiesDeployerModule) + const { Controller } = m.useModule(MigratePeripheryModule) + + const governor = m.getParameter('governor') + const protocolPaymentCut = m.getParameter('protocolPaymentCut') + + // Deploy GraphPayments implementation + const GraphPaymentsImplementation = deployImplementation(m, { + name: 'GraphPayments', + artifact: GraphPaymentsArtifact, + constructorArgs: [Controller, protocolPaymentCut], + }) + + // Upgrade proxy to implementation contract + const GraphPayments = upgradeTransparentUpgradeableProxy(m, + GraphPaymentsProxyAdmin, + GraphPaymentsProxy, + GraphPaymentsImplementation, { + name: 'GraphPayments', + artifact: GraphPaymentsArtifact, + initArgs: [], + }) + + m.call(GraphPaymentsProxyAdmin, 'transferOwnership', [governor], { after: [GraphPayments] }) + + return { GraphPayments, GraphPaymentsProxyAdmin, GraphPaymentsImplementation } +}) diff --git a/packages/horizon/ignition/modules/core/GraphTallyCollector.ts b/packages/horizon/ignition/modules/core/GraphTallyCollector.ts new file mode 100644 index 000000000..1f1a7f767 --- /dev/null +++ b/packages/horizon/ignition/modules/core/GraphTallyCollector.ts @@ -0,0 +1,43 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import GraphPeripheryModule, { MigratePeripheryModule } from '../periphery/periphery' +import HorizonProxiesModule from './HorizonProxies' + +import GraphTallyCollectorArtifact from '../../../build/contracts/contracts/payments/collectors/GraphTallyCollector.sol/GraphTallyCollector.json' + +export default buildModule('GraphTallyCollector', (m) => { + const { Controller } = m.useModule(GraphPeripheryModule) + + const name = m.getParameter('eip712Name') + const version = m.getParameter('eip712Version') + const revokeSignerThawingPeriod = m.getParameter('revokeSignerThawingPeriod') + + const GraphTallyCollector = m.contract( + 'GraphTallyCollector', + GraphTallyCollectorArtifact, + [name, version, Controller, revokeSignerThawingPeriod], + { after: [GraphPeripheryModule, HorizonProxiesModule] }, + ) + + return { GraphTallyCollector } +}) + +// Note that this module requires MigrateHorizonProxiesGovernorModule to be executed first +// The dependency is not made explicit to support the production workflow where the governor is a +// multisig owned by the Graph Council. +// For testnet, the dependency can be made explicit by having a parent module establish it. +export const MigrateGraphTallyCollectorModule = buildModule('GraphTallyCollector', (m) => { + const { Controller } = m.useModule(MigratePeripheryModule) + + const name = m.getParameter('eip712Name') + const version = m.getParameter('eip712Version') + const revokeSignerThawingPeriod = m.getParameter('revokeSignerThawingPeriod') + + const GraphTallyCollector = m.contract( + 'GraphTallyCollector', + GraphTallyCollectorArtifact, + [name, version, Controller, revokeSignerThawingPeriod], + ) + + return { GraphTallyCollector } +}) diff --git a/packages/horizon/ignition/modules/core/HorizonProxies.ts b/packages/horizon/ignition/modules/core/HorizonProxies.ts new file mode 100644 index 000000000..9345beeae --- /dev/null +++ b/packages/horizon/ignition/modules/core/HorizonProxies.ts @@ -0,0 +1,81 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployGraphProxy } from '../proxy/GraphProxy' +import { deployTransparentUpgradeableProxy } from '../proxy/TransparentUpgradeableProxy' +import { ethers } from 'ethers' + +import GraphPeripheryModule from '../periphery/periphery' +import { MigrateControllerGovernorModule } from '../periphery/Controller' + +import GraphPaymentsArtifact from '../../../build/contracts/contracts/payments/GraphPayments.sol/GraphPayments.json' +import PaymentsEscrowArtifact from '../../../build/contracts/contracts/payments/PaymentsEscrow.sol/PaymentsEscrow.json' + +// HorizonStaking, GraphPayments and PaymentsEscrow use GraphDirectory but they are also in the directory. +// So we need to deploy their proxies, register them in the controller before being able to deploy the implementations +export default buildModule('HorizonProxies', (m) => { + const { Controller, GraphProxyAdmin } = m.useModule(GraphPeripheryModule) + + // Deploy HorizonStaking proxy with no implementation + const HorizonStakingProxy = deployGraphProxy(m, GraphProxyAdmin) + m.call(Controller, 'setContractProxy', + [ethers.keccak256(ethers.toUtf8Bytes('Staking')), HorizonStakingProxy], + { id: 'setContractProxy_HorizonStaking' }, + ) + + // Deploy and register GraphPayments proxy + const { Proxy: GraphPaymentsProxy, ProxyAdmin: GraphPaymentsProxyAdmin } = deployTransparentUpgradeableProxy(m, { + name: 'GraphPayments', + artifact: GraphPaymentsArtifact, + }) + m.call(Controller, 'setContractProxy', + [ethers.keccak256(ethers.toUtf8Bytes('GraphPayments')), GraphPaymentsProxy], + { id: 'setContractProxy_GraphPayments' }, + ) + + // Deploy and register PaymentsEscrow proxy + const { Proxy: PaymentsEscrowProxy, ProxyAdmin: PaymentsEscrowProxyAdmin } = deployTransparentUpgradeableProxy(m, { + name: 'PaymentsEscrow', + artifact: PaymentsEscrowArtifact, + }) + m.call(Controller, 'setContractProxy', + [ethers.keccak256(ethers.toUtf8Bytes('PaymentsEscrow')), PaymentsEscrowProxy], + { id: 'setContractProxy_PaymentsEscrow' }, + ) + + return { HorizonStakingProxy, GraphPaymentsProxy, PaymentsEscrowProxy, GraphPaymentsProxyAdmin, PaymentsEscrowProxyAdmin } +}) + +export const MigrateHorizonProxiesDeployerModule = buildModule('HorizonProxiesDeployer', (m) => { + // Deploy GraphPayments proxy + const { Proxy: GraphPaymentsProxy, ProxyAdmin: GraphPaymentsProxyAdmin } = deployTransparentUpgradeableProxy(m, { + name: 'GraphPayments', + artifact: GraphPaymentsArtifact, + }) + + // Deploy PaymentsEscrow proxy + const { Proxy: PaymentsEscrowProxy, ProxyAdmin: PaymentsEscrowProxyAdmin } = deployTransparentUpgradeableProxy(m, { + name: 'PaymentsEscrow', + artifact: PaymentsEscrowArtifact, + }) + + return { GraphPaymentsProxy, PaymentsEscrowProxy, GraphPaymentsProxyAdmin, PaymentsEscrowProxyAdmin } +}) + +export const MigrateHorizonProxiesGovernorModule = buildModule('HorizonProxiesGovernor', (m) => { + const { Controller } = m.useModule(MigrateControllerGovernorModule) + + const graphPaymentsAddress = m.getParameter('graphPaymentsAddress') + const paymentsEscrowAddress = m.getParameter('paymentsEscrowAddress') + + // Register proxies in controller + m.call(Controller, 'setContractProxy', + [ethers.keccak256(ethers.toUtf8Bytes('GraphPayments')), graphPaymentsAddress], + { id: 'setContractProxy_GraphPayments' }, + ) + + m.call(Controller, 'setContractProxy', + [ethers.keccak256(ethers.toUtf8Bytes('PaymentsEscrow')), paymentsEscrowAddress], + { id: 'setContractProxy_PaymentsEscrow' }, + ) + + return { } +}) diff --git a/packages/horizon/ignition/modules/core/HorizonStaking.ts b/packages/horizon/ignition/modules/core/HorizonStaking.ts new file mode 100644 index 000000000..c87208be2 --- /dev/null +++ b/packages/horizon/ignition/modules/core/HorizonStaking.ts @@ -0,0 +1,99 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployImplementation } from '../proxy/implementation' +import { upgradeGraphProxy } from '../proxy/GraphProxy' + +import GraphPeripheryModule, { MigratePeripheryModule } from '../periphery/periphery' +import HorizonProxiesModule from './HorizonProxies' + +import ExponentialRebatesArtifact from '../../../build/contracts/contracts/staking/libraries/ExponentialRebates.sol/ExponentialRebates.json' +import GraphProxyAdminArtifact from '@graphprotocol/contracts/build/contracts/contracts/upgrades/GraphProxyAdmin.sol/GraphProxyAdmin.json' +import GraphProxyArtifact from '@graphprotocol/contracts/build/contracts/contracts/upgrades/GraphProxy.sol/GraphProxy.json' +import HorizonStakingArtifact from '../../../build/contracts/contracts/staking/HorizonStaking.sol/HorizonStaking.json' +import HorizonStakingExtensionArtifact from '../../../build/contracts/contracts/staking/HorizonStakingExtension.sol/HorizonStakingExtension.json' + +export default buildModule('HorizonStaking', (m) => { + const { Controller, GraphProxyAdmin } = m.useModule(GraphPeripheryModule) + const { HorizonStakingProxy } = m.useModule(HorizonProxiesModule) + + const subgraphServiceAddress = m.getParameter('subgraphServiceAddress') + const maxThawingPeriod = m.getParameter('maxThawingPeriod') + + // Deploy HorizonStakingExtension - requires periphery and proxies to be registered in the controller + const ExponentialRebates = m.library('ExponentialRebates', ExponentialRebatesArtifact) + const HorizonStakingExtension = m.contract('HorizonStakingExtension', + HorizonStakingExtensionArtifact, + [Controller, subgraphServiceAddress], { + libraries: { + ExponentialRebates: ExponentialRebates, + }, + after: [GraphPeripheryModule, HorizonProxiesModule], + }) + + // Deploy HorizonStaking implementation + const HorizonStakingImplementation = deployImplementation(m, { + name: 'HorizonStaking', + artifact: HorizonStakingArtifact, + constructorArgs: [Controller, HorizonStakingExtension, subgraphServiceAddress], + }) + + // Upgrade proxy to implementation contract + const HorizonStaking = upgradeGraphProxy(m, GraphProxyAdmin, HorizonStakingProxy, HorizonStakingImplementation, { + name: 'HorizonStaking', + artifact: HorizonStakingArtifact, + }) + m.call(HorizonStaking, 'setMaxThawingPeriod', [maxThawingPeriod]) + + return { HorizonStaking, HorizonStakingImplementation } +}) + +// Note that this module requires MigrateHorizonProxiesGovernorModule to be executed first +// The dependency is not made explicit to support the production workflow where the governor is a +// multisig owned by the Graph Council. +// For testnet, the dependency can be made explicit by having a parent module establish it. +export const MigrateHorizonStakingDeployerModule = buildModule('HorizonStakingDeployer', (m) => { + const { Controller } = m.useModule(MigratePeripheryModule) + + const subgraphServiceAddress = m.getParameter('subgraphServiceAddress') + const horizonStakingAddress = m.getParameter('horizonStakingAddress') + + const HorizonStakingProxy = m.contractAt('HorizonStakingProxy', GraphProxyArtifact, horizonStakingAddress) + + // Deploy HorizonStakingExtension - requires periphery and proxies to be registered in the controller + const ExponentialRebates = m.library('ExponentialRebates', ExponentialRebatesArtifact) + const HorizonStakingExtension = m.contract('HorizonStakingExtension', + HorizonStakingExtensionArtifact, + [Controller, subgraphServiceAddress], { + libraries: { + ExponentialRebates: ExponentialRebates, + }, + }) + + // Deploy HorizonStaking implementation + const HorizonStakingImplementation = deployImplementation(m, { + name: 'HorizonStaking', + artifact: HorizonStakingArtifact, + constructorArgs: [Controller, HorizonStakingExtension, subgraphServiceAddress], + }) + + return { HorizonStakingProxy, HorizonStakingImplementation } +}) + +export const MigrateHorizonStakingGovernorModule = buildModule('HorizonStakingGovernor', (m) => { + const maxThawingPeriod = m.getParameter('maxThawingPeriod') + const graphProxyAdminAddress = m.getParameter('graphProxyAdminAddress') + const horizonStakingAddress = m.getParameter('horizonStakingAddress') + const horizonStakingImplementationAddress = m.getParameter('horizonStakingImplementationAddress') + + const HorizonStakingImplementation = m.contractAt('HorizonStakingImplementation', HorizonStakingArtifact, horizonStakingImplementationAddress) + const HorizonStakingProxy = m.contractAt('HorizonStakingProxy', GraphProxyArtifact, horizonStakingAddress) + const GraphProxyAdmin = m.contractAt('GraphProxyAdmin', GraphProxyAdminArtifact, graphProxyAdminAddress) + + // Upgrade proxy to implementation contract + const HorizonStaking = upgradeGraphProxy(m, GraphProxyAdmin, HorizonStakingProxy, HorizonStakingImplementation, { + name: 'HorizonStaking', + artifact: HorizonStakingArtifact, + }) + m.call(HorizonStaking, 'setMaxThawingPeriod', [maxThawingPeriod]) + + return { HorizonStaking, HorizonStakingImplementation } +}) diff --git a/packages/horizon/ignition/modules/core/PaymentsEscrow.ts b/packages/horizon/ignition/modules/core/PaymentsEscrow.ts new file mode 100644 index 000000000..139d06b06 --- /dev/null +++ b/packages/horizon/ignition/modules/core/PaymentsEscrow.ts @@ -0,0 +1,70 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployImplementation } from '../proxy/implementation' +import { upgradeTransparentUpgradeableProxy } from '../proxy/TransparentUpgradeableProxy' + +import GraphPeripheryModule, { MigratePeripheryModule } from '../periphery/periphery' +import HorizonProxiesModule, { MigrateHorizonProxiesDeployerModule } from './HorizonProxies' + +import PaymentsEscrowArtifact from '../../../build/contracts/contracts/payments/PaymentsEscrow.sol/PaymentsEscrow.json' + +export default buildModule('PaymentsEscrow', (m) => { + const { Controller } = m.useModule(GraphPeripheryModule) + const { PaymentsEscrowProxyAdmin, PaymentsEscrowProxy } = m.useModule(HorizonProxiesModule) + + const governor = m.getAccount(1) + const withdrawEscrowThawingPeriod = m.getParameter('withdrawEscrowThawingPeriod') + + // Deploy PaymentsEscrow implementation - requires periphery and proxies to be registered in the controller + const PaymentsEscrowImplementation = deployImplementation(m, { + name: 'PaymentsEscrow', + artifact: PaymentsEscrowArtifact, + constructorArgs: [Controller, withdrawEscrowThawingPeriod], + }, { after: [GraphPeripheryModule, HorizonProxiesModule] }) + + // Upgrade proxy to implementation contract + const PaymentsEscrow = upgradeTransparentUpgradeableProxy(m, + PaymentsEscrowProxyAdmin, + PaymentsEscrowProxy, + PaymentsEscrowImplementation, { + name: 'PaymentsEscrow', + artifact: PaymentsEscrowArtifact, + initArgs: [], + }) + + m.call(PaymentsEscrowProxyAdmin, 'transferOwnership', [governor], { after: [PaymentsEscrow] }) + + return { PaymentsEscrow, PaymentsEscrowProxyAdmin, PaymentsEscrowImplementation } +}) + +// Note that this module requires MigrateHorizonProxiesGovernorModule to be executed first +// The dependency is not made explicit to support the production workflow where the governor is a +// multisig owned by the Graph Council. +// For testnet, the dependency can be made explicit by having a parent module establish it. +export const MigratePaymentsEscrowModule = buildModule('PaymentsEscrow', (m) => { + const { PaymentsEscrowProxyAdmin, PaymentsEscrowProxy } = m.useModule(MigrateHorizonProxiesDeployerModule) + const { Controller } = m.useModule(MigratePeripheryModule) + + const governor = m.getParameter('governor') + const withdrawEscrowThawingPeriod = m.getParameter('withdrawEscrowThawingPeriod') + + // Deploy PaymentsEscrow implementation + const PaymentsEscrowImplementation = deployImplementation(m, { + name: 'PaymentsEscrow', + artifact: PaymentsEscrowArtifact, + constructorArgs: [Controller, withdrawEscrowThawingPeriod], + }) + + // Upgrade proxy to implementation contract + const PaymentsEscrow = upgradeTransparentUpgradeableProxy(m, + PaymentsEscrowProxyAdmin, + PaymentsEscrowProxy, + PaymentsEscrowImplementation, { + name: 'PaymentsEscrow', + artifact: PaymentsEscrowArtifact, + initArgs: [], + }) + + m.call(PaymentsEscrowProxyAdmin, 'transferOwnership', [governor], { after: [PaymentsEscrow] }) + + return { PaymentsEscrow, PaymentsEscrowProxyAdmin, PaymentsEscrowImplementation } +}) diff --git a/packages/horizon/ignition/modules/core/core.ts b/packages/horizon/ignition/modules/core/core.ts new file mode 100644 index 000000000..f719e6605 --- /dev/null +++ b/packages/horizon/ignition/modules/core/core.ts @@ -0,0 +1,40 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import GraphPaymentsModule, { MigrateGraphPaymentsModule } from './GraphPayments' +import GraphTallyCollectorModule, { MigrateGraphTallyCollectorModule } from './GraphTallyCollector' +import HorizonStakingModule, { MigrateHorizonStakingDeployerModule } from './HorizonStaking' +import PaymentsEscrowModule, { MigratePaymentsEscrowModule } from './PaymentsEscrow' + +export default buildModule('GraphHorizon_Core', (m) => { + const { HorizonStaking, HorizonStakingImplementation } = m.useModule(HorizonStakingModule) + const { GraphPayments, GraphPaymentsImplementation } = m.useModule(GraphPaymentsModule) + const { PaymentsEscrow, PaymentsEscrowImplementation } = m.useModule(PaymentsEscrowModule) + const { GraphTallyCollector } = m.useModule(GraphTallyCollectorModule) + + return { + HorizonStaking, + HorizonStakingImplementation, + GraphPayments, + GraphPaymentsImplementation, + PaymentsEscrow, + PaymentsEscrowImplementation, + GraphTallyCollector, + } +}) + +export const MigrateHorizonCoreModule = buildModule('GraphHorizon_Core', (m) => { + const { HorizonStakingProxy: HorizonStaking, HorizonStakingImplementation } = m.useModule(MigrateHorizonStakingDeployerModule) + const { GraphPayments, GraphPaymentsImplementation } = m.useModule(MigrateGraphPaymentsModule) + const { PaymentsEscrow, PaymentsEscrowImplementation } = m.useModule(MigratePaymentsEscrowModule) + const { GraphTallyCollector } = m.useModule(MigrateGraphTallyCollectorModule) + + return { + HorizonStaking, + HorizonStakingImplementation, + GraphPayments, + GraphPaymentsImplementation, + PaymentsEscrow, + PaymentsEscrowImplementation, + GraphTallyCollector, + } +}) diff --git a/packages/horizon/ignition/modules/deploy.ts b/packages/horizon/ignition/modules/deploy.ts new file mode 100644 index 000000000..aed98f69f --- /dev/null +++ b/packages/horizon/ignition/modules/deploy.ts @@ -0,0 +1,62 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import GraphHorizonCoreModule from './core/core' +import GraphPeripheryModule from './periphery/periphery' + +export default buildModule('GraphHorizon_Deploy', (m) => { + const { + Controller, + EpochManager, + EpochManagerImplementation, + GraphProxyAdmin, + L2GraphTokenGateway, + L2GraphTokenGatewayImplementation, + L2GraphToken, + L2GraphTokenImplementation, + RewardsManager, + RewardsManagerImplementation, + L2Curation, + L2CurationImplementation, + } = m.useModule(GraphPeripheryModule) + const { + HorizonStaking, + HorizonStakingImplementation, + GraphPayments, + GraphPaymentsImplementation, + PaymentsEscrow, + PaymentsEscrowImplementation, + GraphTallyCollector, + } = m.useModule(GraphHorizonCoreModule) + + const governor = m.getAccount(1) + + // BUG?: acceptOwnership should be called after everything in GraphHorizonCoreModule and GraphPeripheryModule is resolved + // but it seems that it's not waiting for interal calls. Waiting on HorizonStaking seems to fix the issue for some reason + // Removing HorizonStaking from the after list will trigger the bug + + // Accept ownership of Graph Governed based contracts + m.call(Controller, 'acceptOwnership', [], { from: governor, after: [GraphPeripheryModule, GraphHorizonCoreModule, HorizonStaking] }) + m.call(GraphProxyAdmin, 'acceptOwnership', [], { from: governor, after: [GraphPeripheryModule, GraphHorizonCoreModule, HorizonStaking] }) + + return { + Controller, + Graph_Proxy_EpochManager: EpochManager, + Implementation_EpochManager: EpochManagerImplementation, + Graph_Proxy_L2Curation: L2Curation, + Implementation_L2Curation: L2CurationImplementation, + Graph_Proxy_RewardsManager: RewardsManager, + Implementation_RewardsManager: RewardsManagerImplementation, + Graph_Proxy_L2GraphTokenGateway: L2GraphTokenGateway, + Implementation_L2GraphTokenGateway: L2GraphTokenGatewayImplementation, + Graph_Proxy_L2GraphToken: L2GraphToken, + Implementation_L2GraphToken: L2GraphTokenImplementation, + GraphProxyAdmin, + Graph_Proxy_HorizonStaking: HorizonStaking, + Implementation_HorizonStaking: HorizonStakingImplementation, + Transparent_Proxy_GraphPayments: GraphPayments, + Implementation_GraphPayments: GraphPaymentsImplementation, + Transparent_Proxy_PaymentsEscrow: PaymentsEscrow, + Implementation_PaymentsEscrow: PaymentsEscrowImplementation, + GraphTallyCollector, + } +}) diff --git a/packages/horizon/ignition/modules/migrate/migrate-1.ts b/packages/horizon/ignition/modules/migrate/migrate-1.ts new file mode 100644 index 000000000..9b7cab512 --- /dev/null +++ b/packages/horizon/ignition/modules/migrate/migrate-1.ts @@ -0,0 +1,19 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import { MigrateHorizonProxiesDeployerModule } from '../core/HorizonProxies' + +export default buildModule('GraphHorizon_Migrate_1', (m) => { + const { + GraphPaymentsProxy, + PaymentsEscrowProxy, + GraphPaymentsProxyAdmin, + PaymentsEscrowProxyAdmin, + } = m.useModule(MigrateHorizonProxiesDeployerModule) + + return { + Transparent_Proxy_GraphPayments: GraphPaymentsProxy, + Transparent_Proxy_PaymentsEscrow: PaymentsEscrowProxy, + Transparent_ProxyAdmin_GraphPayments: GraphPaymentsProxyAdmin, + Transparent_ProxyAdmin_PaymentsEscrow: PaymentsEscrowProxyAdmin, + } +}) diff --git a/packages/horizon/ignition/modules/migrate/migrate-2.ts b/packages/horizon/ignition/modules/migrate/migrate-2.ts new file mode 100644 index 000000000..7b59217e4 --- /dev/null +++ b/packages/horizon/ignition/modules/migrate/migrate-2.ts @@ -0,0 +1,9 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import { MigrateHorizonProxiesGovernorModule } from '../core/HorizonProxies' + +export default buildModule('GraphHorizon_Migrate_2', (m) => { + m.useModule(MigrateHorizonProxiesGovernorModule) + + return {} +}) diff --git a/packages/horizon/ignition/modules/migrate/migrate-3.ts b/packages/horizon/ignition/modules/migrate/migrate-3.ts new file mode 100644 index 000000000..999cc25cc --- /dev/null +++ b/packages/horizon/ignition/modules/migrate/migrate-3.ts @@ -0,0 +1,47 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import { MigrateHorizonCoreModule } from '../core/core' +import { MigratePeripheryModule } from '../periphery/periphery' + +export default buildModule('GraphHorizon_Migrate_3', (m) => { + const { + L2Curation, + L2CurationImplementation, + RewardsManager, + RewardsManagerImplementation, + Controller, + GraphProxyAdmin, + EpochManager, + L2GraphToken, + L2GraphTokenGateway, + } = m.useModule(MigratePeripheryModule) + + const { + HorizonStaking, + HorizonStakingImplementation, + GraphPayments, + GraphPaymentsImplementation, + PaymentsEscrow, + PaymentsEscrowImplementation, + GraphTallyCollector, + } = m.useModule(MigrateHorizonCoreModule) + + return { + Graph_Proxy_L2Curation: L2Curation, + Implementation_L2Curation: L2CurationImplementation, + Graph_Proxy_RewardsManager: RewardsManager, + Implementation_RewardsManager: RewardsManagerImplementation, + Graph_Proxy_HorizonStaking: HorizonStaking, + Implementation_HorizonStaking: HorizonStakingImplementation, + Transparent_Proxy_GraphPayments: GraphPayments, + Implementation_GraphPayments: GraphPaymentsImplementation, + Transparent_Proxy_PaymentsEscrow: PaymentsEscrow, + Implementation_PaymentsEscrow: PaymentsEscrowImplementation, + GraphTallyCollector, + Controller: Controller, + GraphProxyAdmin, + Graph_Proxy_EpochManager: EpochManager, + Graph_Proxy_L2GraphToken: L2GraphToken, + Graph_Proxy_L2GraphTokenGateway: L2GraphTokenGateway, + } +}) diff --git a/packages/horizon/ignition/modules/migrate/migrate-4.ts b/packages/horizon/ignition/modules/migrate/migrate-4.ts new file mode 100644 index 000000000..9290cc753 --- /dev/null +++ b/packages/horizon/ignition/modules/migrate/migrate-4.ts @@ -0,0 +1,31 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import { MigrateCurationGovernorModule } from '../periphery/Curation' +import { MigrateHorizonStakingGovernorModule } from '../core/HorizonStaking' +import { MigrateRewardsManagerGovernorModule } from '../periphery/RewardsManager' + +export default buildModule('GraphHorizon_Migrate_4', (m) => { + const { + L2Curation, + L2CurationImplementation, + } = m.useModule(MigrateCurationGovernorModule) + + const { + RewardsManager, + RewardsManagerImplementation, + } = m.useModule(MigrateRewardsManagerGovernorModule) + + const { + HorizonStaking, + HorizonStakingImplementation, + } = m.useModule(MigrateHorizonStakingGovernorModule) + + return { + Graph_Proxy_L2Curation: L2Curation, + Implementation_L2Curation: L2CurationImplementation, + Graph_Proxy_RewardsManager: RewardsManager, + Implementation_RewardsManager: RewardsManagerImplementation, + Graph_Proxy_HorizonStaking: HorizonStaking, + Implementation_HorizonStaking: HorizonStakingImplementation, + } +}) diff --git a/packages/horizon/ignition/modules/periphery/Controller.ts b/packages/horizon/ignition/modules/periphery/Controller.ts new file mode 100644 index 000000000..8e8e130d7 --- /dev/null +++ b/packages/horizon/ignition/modules/periphery/Controller.ts @@ -0,0 +1,39 @@ +/* eslint-disable no-secrets/no-secrets */ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { ethers } from 'ethers' + +import ControllerArtifact from '@graphprotocol/contracts/build/contracts/contracts/governance/Controller.sol/Controller.json' + +export default buildModule('Controller', (m) => { + const governor = m.getAccount(1) + const pauseGuardian = m.getParameter('pauseGuardian') + + const Controller = m.contract('Controller', ControllerArtifact) + m.call(Controller, 'setPauseGuardian', [pauseGuardian]) + m.call(Controller, 'setPaused', [false]) + m.call(Controller, 'transferOwnership', [governor]) + + return { Controller } +}) + +export const MigrateControllerDeployerModule = buildModule('ControllerDeployer', (m) => { + const controllerAddress = m.getParameter('controllerAddress') + + const Controller = m.contractAt('Controller', ControllerArtifact, controllerAddress) + + return { Controller } +}) + +export const MigrateControllerGovernorModule = buildModule('ControllerGovernor', (m) => { + const { Controller } = m.useModule(MigrateControllerDeployerModule) + + const graphProxyAdminAddress = m.getParameter('graphProxyAdminAddress') + + // GraphProxyAdmin was not registered in the controller in the original protocol + m.call(Controller, 'setContractProxy', + [ethers.keccak256(ethers.toUtf8Bytes('GraphProxyAdmin')), graphProxyAdminAddress], + { id: 'setContractProxy_GraphProxyAdmin' }, + ) + + return { Controller } +}) diff --git a/packages/horizon/ignition/modules/periphery/Curation.ts b/packages/horizon/ignition/modules/periphery/Curation.ts new file mode 100644 index 000000000..d93ffc678 --- /dev/null +++ b/packages/horizon/ignition/modules/periphery/Curation.ts @@ -0,0 +1,66 @@ +import { buildModule, IgnitionModuleBuilder } from '@nomicfoundation/ignition-core' +import { deployWithGraphProxy, upgradeGraphProxy } from '../proxy/GraphProxy' +import { deployImplementation } from '../proxy/implementation' + +import ControllerModule from './Controller' +import GraphProxyAdminModule from './GraphProxyAdmin' + +import CurationArtifact from '@graphprotocol/contracts/build/contracts/contracts/l2/curation/L2Curation.sol/L2Curation.json' +import GraphCurationTokenArtifact from '@graphprotocol/contracts/build/contracts/contracts/curation/GraphCurationToken.sol/GraphCurationToken.json' +import GraphProxyAdminArtifact from '@graphprotocol/contracts/build/contracts/contracts/upgrades/GraphProxyAdmin.sol/GraphProxyAdmin.json' +import GraphProxyArtifact from '@graphprotocol/contracts/build/contracts/contracts/upgrades/GraphProxy.sol/GraphProxy.json' + +export default buildModule('L2Curation', (m) => { + const { Controller } = m.useModule(ControllerModule) + const { GraphProxyAdmin } = m.useModule(GraphProxyAdminModule) + + const curationTaxPercentage = m.getParameter('curationTaxPercentage') + const minimumCurationDeposit = m.getParameter('minimumCurationDeposit') + const subgraphServiceAddress = m.getParameter('subgraphServiceAddress') + + const GraphCurationToken = m.contract('GraphCurationToken', GraphCurationTokenArtifact, []) + + const { proxy: L2Curation, implementation: L2CurationImplementation } = deployWithGraphProxy(m, GraphProxyAdmin, { + name: 'L2Curation', + artifact: CurationArtifact, + initArgs: [Controller, GraphCurationToken, curationTaxPercentage, minimumCurationDeposit], + }) + m.call(L2Curation, 'setSubgraphService', [subgraphServiceAddress]) + + return { L2Curation, L2CurationImplementation } +}) + +export const MigrateCurationDeployerModule = buildModule('L2CurationDeployer', (m: IgnitionModuleBuilder) => { + const curationAddress = m.getParameter('curationAddress') + + const L2CurationProxy = m.contractAt('L2CurationProxy', GraphProxyArtifact, curationAddress) + + const implementationMetadata = { + name: 'L2Curation', + artifact: CurationArtifact, + } + const L2CurationImplementation = deployImplementation(m, implementationMetadata) + + return { L2CurationProxy, L2CurationImplementation } +}) + +export const MigrateCurationGovernorModule = buildModule('L2CurationGovernor', (m: IgnitionModuleBuilder) => { + const curationAddress = m.getParameter('curationAddress') + const curationImplementationAddress = m.getParameter('curationImplementationAddress') + const subgraphServiceAddress = m.getParameter('subgraphServiceAddress') + const graphProxyAdminAddress = m.getParameter('graphProxyAdminAddress') + + const GraphProxyAdmin = m.contractAt('GraphProxyAdmin', GraphProxyAdminArtifact, graphProxyAdminAddress) + const L2CurationProxy = m.contractAt('L2CurationProxy', GraphProxyArtifact, curationAddress) + const L2CurationImplementation = m.contractAt('L2CurationImplementation', CurationArtifact, curationImplementationAddress) + + const implementationMetadata = { + name: 'L2Curation', + artifact: CurationArtifact, + } + + const L2Curation = upgradeGraphProxy(m, GraphProxyAdmin, L2CurationProxy, L2CurationImplementation, implementationMetadata) + m.call(L2Curation, 'setSubgraphService', [subgraphServiceAddress]) + + return { L2Curation, L2CurationImplementation } +}) diff --git a/packages/horizon/ignition/modules/periphery/EpochManager.ts b/packages/horizon/ignition/modules/periphery/EpochManager.ts new file mode 100644 index 000000000..a7e08bf22 --- /dev/null +++ b/packages/horizon/ignition/modules/periphery/EpochManager.ts @@ -0,0 +1,30 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployWithGraphProxy } from '../proxy/GraphProxy' + +import ControllerModule from './Controller' +import GraphProxyAdminModule from './GraphProxyAdmin' + +import EpochManagerArtifact from '@graphprotocol/contracts/build/contracts/contracts/epochs/EpochManager.sol/EpochManager.json' + +export default buildModule('EpochManager', (m) => { + const { Controller } = m.useModule(ControllerModule) + const { GraphProxyAdmin } = m.useModule(GraphProxyAdminModule) + + const epochLength = m.getParameter('epochLength') + + const { proxy: EpochManager, implementation: EpochManagerImplementation } = deployWithGraphProxy(m, GraphProxyAdmin, { + name: 'EpochManager', + artifact: EpochManagerArtifact, + initArgs: [Controller, epochLength], + }) + + return { EpochManager, EpochManagerImplementation } +}) + +export const MigrateEpochManagerModule = buildModule('EpochManager', (m) => { + const epochManagerAddress = m.getParameter('epochManagerAddress') + + const EpochManager = m.contractAt('EpochManager', EpochManagerArtifact, epochManagerAddress) + + return { EpochManager } +}) diff --git a/packages/horizon/ignition/modules/periphery/GraphProxyAdmin.ts b/packages/horizon/ignition/modules/periphery/GraphProxyAdmin.ts new file mode 100644 index 000000000..c727620c2 --- /dev/null +++ b/packages/horizon/ignition/modules/periphery/GraphProxyAdmin.ts @@ -0,0 +1,20 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import GraphProxyAdminArtifact from '@graphprotocol/contracts/build/contracts/contracts/upgrades/GraphProxyAdmin.sol/GraphProxyAdmin.json' + +export default buildModule('GraphProxyAdmin', (m) => { + const governor = m.getAccount(1) + + const GraphProxyAdmin = m.contract('GraphProxyAdmin', GraphProxyAdminArtifact) + m.call(GraphProxyAdmin, 'transferOwnership', [governor]) + + return { GraphProxyAdmin } +}) + +export const MigrateGraphProxyAdminModule = buildModule('GraphProxyAdmin', (m) => { + const graphProxyAdminAddress = m.getParameter('graphProxyAdminAddress') + + const GraphProxyAdmin = m.contractAt('GraphProxyAdmin', GraphProxyAdminArtifact, graphProxyAdminAddress) + + return { GraphProxyAdmin } +}) diff --git a/packages/horizon/ignition/modules/periphery/GraphToken.ts b/packages/horizon/ignition/modules/periphery/GraphToken.ts new file mode 100644 index 000000000..dd8c1a129 --- /dev/null +++ b/packages/horizon/ignition/modules/periphery/GraphToken.ts @@ -0,0 +1,43 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployWithGraphProxy } from '../proxy/GraphProxy' + +import GraphProxyAdminModule from '../periphery/GraphProxyAdmin' +import GraphTokenGatewayModule from '../periphery/GraphTokenGateway' +import RewardsManagerModule from '../periphery/RewardsManager' + +import GraphTokenArtifact from '@graphprotocol/contracts/build/contracts/contracts/l2/token/L2GraphToken.sol/L2GraphToken.json' + +export default buildModule('L2GraphToken', (m) => { + const { GraphProxyAdmin } = m.useModule(GraphProxyAdminModule) + const { RewardsManager } = m.useModule(RewardsManagerModule) + const { L2GraphTokenGateway } = m.useModule(GraphTokenGatewayModule) + + const deployer = m.getAccount(0) + const governor = m.getAccount(1) + const initialSupply = m.getParameter('initialSupply') + + const { proxy: L2GraphToken, implementation: L2GraphTokenImplementation } = deployWithGraphProxy(m, GraphProxyAdmin, { + name: 'L2GraphToken', + artifact: GraphTokenArtifact, + initArgs: [deployer], + }) + + const mintCall = m.call(L2GraphToken, 'mint', [deployer, initialSupply]) + const renounceMinterCall = m.call(L2GraphToken, 'renounceMinter', []) + const addMinterRewardsManagerCall = m.call(L2GraphToken, 'addMinter', [RewardsManager], { id: 'addMinterRewardsManager' }) + const addMinterGatewayCall = m.call(L2GraphToken, 'addMinter', [L2GraphTokenGateway], { id: 'addMinterGateway' }) + + // No further calls are needed so we can transfer ownership now + const transferOwnershipCall = m.call(L2GraphToken, 'transferOwnership', [governor], { after: [mintCall, renounceMinterCall, addMinterRewardsManagerCall, addMinterGatewayCall] }) + m.call(L2GraphToken, 'acceptOwnership', [], { from: governor, after: [transferOwnershipCall] }) + + return { L2GraphToken, L2GraphTokenImplementation } +}) + +export const MigrateGraphTokenModule = buildModule('L2GraphToken', (m) => { + const graphTokenAddress = m.getParameter('graphTokenAddress') + + const L2GraphToken = m.contractAt('L2GraphToken', GraphTokenArtifact, graphTokenAddress) + + return { L2GraphToken } +}) diff --git a/packages/horizon/ignition/modules/periphery/GraphTokenGateway.ts b/packages/horizon/ignition/modules/periphery/GraphTokenGateway.ts new file mode 100644 index 000000000..fc679e8ad --- /dev/null +++ b/packages/horizon/ignition/modules/periphery/GraphTokenGateway.ts @@ -0,0 +1,32 @@ +import { buildModule } from '@nomicfoundation/ignition-core' + +import { deployWithGraphProxy } from '../proxy/GraphProxy' + +import ControllerModule from '../periphery/Controller' +import GraphProxyAdminModule from '../periphery/GraphProxyAdmin' + +import GraphTokenGatewayArtifact from '@graphprotocol/contracts/build/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol/L2GraphTokenGateway.json' + +export default buildModule('L2GraphTokenGateway', (m) => { + const { Controller } = m.useModule(ControllerModule) + const { GraphProxyAdmin } = m.useModule(GraphProxyAdminModule) + + const pauseGuardian = m.getParameter('pauseGuardian') + + const { proxy: L2GraphTokenGateway, implementation: L2GraphTokenGatewayImplementation } = deployWithGraphProxy(m, GraphProxyAdmin, { + name: 'L2GraphTokenGateway', + artifact: GraphTokenGatewayArtifact, + initArgs: [Controller], + }) + m.call(L2GraphTokenGateway, 'setPauseGuardian', [pauseGuardian]) + + return { L2GraphTokenGateway, L2GraphTokenGatewayImplementation } +}) + +export const MigrateGraphTokenGatewayModule = buildModule('L2GraphTokenGateway', (m) => { + const graphTokenGatewayAddress = m.getParameter('graphTokenGatewayAddress') + + const L2GraphTokenGateway = m.contractAt('L2GraphTokenGateway', GraphTokenGatewayArtifact, graphTokenGatewayAddress) + + return { L2GraphTokenGateway } +}) diff --git a/packages/horizon/ignition/modules/periphery/RewardsManager.ts b/packages/horizon/ignition/modules/periphery/RewardsManager.ts new file mode 100644 index 000000000..c1dc8e285 --- /dev/null +++ b/packages/horizon/ignition/modules/periphery/RewardsManager.ts @@ -0,0 +1,66 @@ +import { buildModule, IgnitionModuleBuilder } from '@nomicfoundation/ignition-core' +import { deployWithGraphProxy, upgradeGraphProxy } from '../proxy/GraphProxy' +import { deployImplementation } from '../proxy/implementation' + +import ControllerModule from './Controller' +import GraphProxyAdminModule from './GraphProxyAdmin' + +import GraphProxyAdminArtifact from '@graphprotocol/contracts/build/contracts/contracts/upgrades/GraphProxyAdmin.sol/GraphProxyAdmin.json' +import GraphProxyArtifact from '@graphprotocol/contracts/build/contracts/contracts/upgrades/GraphProxy.sol/GraphProxy.json' +import RewardsManagerArtifact from '@graphprotocol/contracts/build/contracts/contracts/rewards/RewardsManager.sol/RewardsManager.json' + +export default buildModule('RewardsManager', (m) => { + const { Controller } = m.useModule(ControllerModule) + const { GraphProxyAdmin } = m.useModule(GraphProxyAdminModule) + + const issuancePerBlock = m.getParameter('issuancePerBlock') + const subgraphAvailabilityOracle = m.getParameter('subgraphAvailabilityOracle') + const subgraphServiceAddress = m.getParameter('subgraphServiceAddress') + + const { proxy: RewardsManager, implementation: RewardsManagerImplementation } = deployWithGraphProxy(m, GraphProxyAdmin, { + name: 'RewardsManager', + artifact: RewardsManagerArtifact, + initArgs: [Controller], + }) + m.call(RewardsManager, 'setSubgraphAvailabilityOracle', [subgraphAvailabilityOracle]) + m.call(RewardsManager, 'setIssuancePerBlock', [issuancePerBlock]) + m.call(RewardsManager, 'setSubgraphService', [subgraphServiceAddress]) + + return { RewardsManager, RewardsManagerImplementation } +}) + +export const MigrateRewardsManagerDeployerModule = buildModule('RewardsManagerDeployer', (m: IgnitionModuleBuilder) => { + const rewardsManagerAddress = m.getParameter('rewardsManagerAddress') + + const RewardsManagerProxy = m.contractAt('RewardsManagerProxy', GraphProxyArtifact, rewardsManagerAddress) + + const implementationMetadata = { + name: 'RewardsManager', + artifact: RewardsManagerArtifact, + } + const RewardsManagerImplementation = deployImplementation(m, implementationMetadata) + + return { RewardsManagerProxy, RewardsManagerImplementation } +}) + +export const MigrateRewardsManagerGovernorModule = buildModule('RewardsManagerGovernor', (m: IgnitionModuleBuilder) => { + const rewardsManagerAddress = m.getParameter('rewardsManagerAddress') + const rewardsManagerImplementationAddress = m.getParameter('rewardsManagerImplementationAddress') + const graphProxyAdminAddress = m.getParameter('graphProxyAdminAddress') + + const GraphProxyAdmin = m.contractAt('GraphProxyAdmin', GraphProxyAdminArtifact, graphProxyAdminAddress) + const RewardsManagerProxy = m.contractAt('RewardsManagerProxy', GraphProxyArtifact, rewardsManagerAddress) + const RewardsManagerImplementation = m.contractAt('RewardsManagerImplementation', RewardsManagerArtifact, rewardsManagerImplementationAddress) + + const subgraphServiceAddress = m.getParameter('subgraphServiceAddress') + + const implementationMetadata = { + name: 'RewardsManager', + artifact: RewardsManagerArtifact, + } + + const RewardsManager = upgradeGraphProxy(m, GraphProxyAdmin, RewardsManagerProxy, RewardsManagerImplementation, implementationMetadata) + m.call(RewardsManager, 'setSubgraphService', [subgraphServiceAddress]) + + return { RewardsManager, RewardsManagerImplementation } +}) diff --git a/packages/horizon/ignition/modules/periphery/periphery.ts b/packages/horizon/ignition/modules/periphery/periphery.ts new file mode 100644 index 000000000..f02f9b2cc --- /dev/null +++ b/packages/horizon/ignition/modules/periphery/periphery.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-secrets/no-secrets */ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { ethers } from 'ethers' + +import ControllerModule, { MigrateControllerDeployerModule } from './Controller' +import CurationModule, { MigrateCurationDeployerModule } from './Curation' +import EpochManagerModule, { MigrateEpochManagerModule } from './EpochManager' +import GraphProxyAdminModule, { MigrateGraphProxyAdminModule } from './GraphProxyAdmin' +import GraphTokenGatewayModule, { MigrateGraphTokenGatewayModule } from './GraphTokenGateway' +import GraphTokenModule, { MigrateGraphTokenModule } from './GraphToken' +import RewardsManagerModule, { MigrateRewardsManagerDeployerModule } from './RewardsManager' + +export default buildModule('GraphHorizon_Periphery', (m) => { + const { Controller } = m.useModule(ControllerModule) + const { GraphProxyAdmin } = m.useModule(GraphProxyAdminModule) + + const { EpochManager, EpochManagerImplementation } = m.useModule(EpochManagerModule) + const { L2Curation, L2CurationImplementation } = m.useModule(CurationModule) + const { RewardsManager, RewardsManagerImplementation } = m.useModule(RewardsManagerModule) + const { L2GraphTokenGateway, L2GraphTokenGatewayImplementation } = m.useModule(GraphTokenGatewayModule) + const { L2GraphToken, L2GraphTokenImplementation } = m.useModule(GraphTokenModule) + + m.call(Controller, 'setContractProxy', [ethers.keccak256(ethers.toUtf8Bytes('EpochManager')), EpochManager], { id: 'setContractProxy_EpochManager' }) + m.call(Controller, 'setContractProxy', [ethers.keccak256(ethers.toUtf8Bytes('RewardsManager')), RewardsManager], { id: 'setContractProxy_RewardsManager' }) + m.call(Controller, 'setContractProxy', [ethers.keccak256(ethers.toUtf8Bytes('GraphToken')), L2GraphToken], { id: 'setContractProxy_GraphToken' }) + m.call(Controller, 'setContractProxy', [ethers.keccak256(ethers.toUtf8Bytes('GraphTokenGateway')), L2GraphTokenGateway], { id: 'setContractProxy_GraphTokenGateway' }) + m.call(Controller, 'setContractProxy', [ethers.keccak256(ethers.toUtf8Bytes('GraphProxyAdmin')), GraphProxyAdmin], { id: 'setContractProxy_GraphProxyAdmin' }) + m.call(Controller, 'setContractProxy', [ethers.keccak256(ethers.toUtf8Bytes('Curation')), L2Curation], { id: 'setContractProxy_L2Curation' }) + + return { + Controller, + EpochManager, + EpochManagerImplementation, + L2Curation, + L2CurationImplementation, + GraphProxyAdmin, + L2GraphToken, + L2GraphTokenImplementation, + L2GraphTokenGateway, + L2GraphTokenGatewayImplementation, + RewardsManager, + RewardsManagerImplementation, + } +}) + +export const MigratePeripheryModule = buildModule('GraphHorizon_Periphery', (m) => { + const { L2CurationProxy: L2Curation, L2CurationImplementation } = m.useModule(MigrateCurationDeployerModule) + const { RewardsManagerProxy: RewardsManager, RewardsManagerImplementation } = m.useModule(MigrateRewardsManagerDeployerModule) + const { Controller } = m.useModule(MigrateControllerDeployerModule) + const { GraphProxyAdmin } = m.useModule(MigrateGraphProxyAdminModule) + const { EpochManager } = m.useModule(MigrateEpochManagerModule) + const { L2GraphToken } = m.useModule(MigrateGraphTokenModule) + const { L2GraphTokenGateway } = m.useModule(MigrateGraphTokenGatewayModule) + + // Load these contracts so they are available in the address book + + return { + Controller, + EpochManager, + L2Curation, + L2CurationImplementation, + GraphProxyAdmin, + L2GraphToken, + L2GraphTokenGateway, + RewardsManager, + RewardsManagerImplementation, + } +}) diff --git a/packages/horizon/ignition/modules/proxy/GraphProxy.ts b/packages/horizon/ignition/modules/proxy/GraphProxy.ts new file mode 100644 index 000000000..efc6fe4ef --- /dev/null +++ b/packages/horizon/ignition/modules/proxy/GraphProxy.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + CallableContractFuture, + ContractFuture, + ContractOptions, + IgnitionModuleBuilder, +} from '@nomicfoundation/ignition-core' + +import { deployImplementation, type ImplementationMetadata } from './implementation' +import { loadProxyWithABI } from './utils' + +import GraphProxyArtifact from '@graphprotocol/contracts/build/contracts/contracts/upgrades/GraphProxy.sol/GraphProxy.json' + +export function deployGraphProxy( + m: IgnitionModuleBuilder, + proxyAdmin: ContractFuture, + implementation?: ContractFuture, + metadata?: ImplementationMetadata, + options?: ContractOptions, +) { + if (implementation === undefined || metadata === undefined) { + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + return m.contract('GraphProxy', GraphProxyArtifact, [ZERO_ADDRESS, proxyAdmin], options) + } else { + const GraphProxy = m.contract('GraphProxy', GraphProxyArtifact, [implementation, proxyAdmin], options) + return loadProxyWithABI(m, GraphProxy, metadata, options) + } +} + +export function upgradeGraphProxy( + m: IgnitionModuleBuilder, + proxyAdmin: CallableContractFuture, + proxy: CallableContractFuture, + implementation: ContractFuture, + metadata: ImplementationMetadata, + options?: ContractOptions, +) { + const upgradeCall = m.call(proxyAdmin, 'upgrade', [proxy, implementation], options) + const acceptCall = m.call(proxyAdmin, 'acceptProxy', [implementation, proxy], { ...options, after: [upgradeCall] }) + + return loadProxyWithABI(m, proxy, metadata, { ...options, after: [acceptCall] }) +} + +export function acceptUpgradeGraphProxy( + m: IgnitionModuleBuilder, + proxyAdmin: CallableContractFuture, + proxy: CallableContractFuture, + implementation: ContractFuture, + metadata: ImplementationMetadata, + options?: ContractOptions, +) { + const acceptCall = m.call(proxyAdmin, 'acceptProxy', [implementation, proxy], { ...options }) + + return loadProxyWithABI(m, proxy, metadata, { ...options, after: [acceptCall] }) +} + +export function deployWithGraphProxy( + m: IgnitionModuleBuilder, + proxyAdmin: CallableContractFuture, + metadata: ImplementationMetadata, + options?: ContractOptions, +) { + options = options || {} + + // Deploy implementation + const implementation = deployImplementation(m, metadata, options) + + // Deploy proxy and initialize + const proxy = deployGraphProxy(m, proxyAdmin, implementation, metadata, options) + if (metadata.initArgs === undefined) { + m.call(proxyAdmin, 'acceptProxy', [implementation, proxy], options) + } else { + m.call(proxyAdmin, 'acceptProxyAndCall', [implementation, proxy, m.encodeFunctionCall(implementation, 'initialize', metadata.initArgs)], options) + } + + return { proxy, implementation } +} diff --git a/packages/horizon/ignition/modules/proxy/TransparentUpgradeableProxy.ts b/packages/horizon/ignition/modules/proxy/TransparentUpgradeableProxy.ts new file mode 100644 index 000000000..371b06eb5 --- /dev/null +++ b/packages/horizon/ignition/modules/proxy/TransparentUpgradeableProxy.ts @@ -0,0 +1,61 @@ +import { CallableContractFuture, ContractFuture, ContractOptions, IgnitionModuleBuilder } from '@nomicfoundation/ignition-core' +import { ImplementationMetadata } from './implementation' +import { loadProxyWithABI } from './utils' + +// Importing artifacts from build directory so we have all build artifacts for contract verification +import DummyArtifact from '../../../build/contracts/contracts/mocks/Dummy.sol/Dummy.json' +import ProxyAdminArtifact from '../../../build/contracts/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol/ProxyAdmin.json' +import TransparentUpgradeableProxyArtifact from '../../../build/contracts/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json' + +// Deploy a TransparentUpgradeableProxy +// The TransparentUpgradeableProxy contract creates the ProxyAdmin within its constructor. +export function deployTransparentUpgradeableProxy( + m: IgnitionModuleBuilder, + metadata: ImplementationMetadata, + implementation?: ContractFuture, + options?: ContractOptions, +) { + const deployer = m.getAccount(0) + + // The proxy requires a valid contract as initial implementation so we use a dummy + if (implementation === undefined) { + implementation = m.contract('Dummy', DummyArtifact, [], { ...options, id: `OZProxyDummy_${metadata.name}` }) + } + + const Proxy = m.contract('TransparentUpgradeableProxy', TransparentUpgradeableProxyArtifact, [ + implementation, + deployer, + '0x', + ], + { ...options, id: `TransparentUpgradeableProxy_${metadata.name}` }) + + const proxyAdminAddress = m.readEventArgument( + Proxy, + 'AdminChanged', + 'newAdmin', + { ...options, id: `TransparentUpgradeableProxy_${metadata.name}_AdminChanged` }, + ) + + const ProxyAdmin = m.contractAt('ProxyAdmin', ProxyAdminArtifact, proxyAdminAddress, { ...options, id: `ProxyAdmin_${metadata.name}` }) + + if (implementation !== undefined) { + return { ProxyAdmin, Proxy: loadProxyWithABI(m, Proxy, metadata, options) } + } else { + return { ProxyAdmin, Proxy } + } +} + +export function upgradeTransparentUpgradeableProxy( + m: IgnitionModuleBuilder, + proxyAdmin: CallableContractFuture, + proxy: CallableContractFuture, + implementation: CallableContractFuture, + metadata: ImplementationMetadata, + options?: ContractOptions, +) { + const upgradeCall = m.call(proxyAdmin, 'upgradeAndCall', + [proxy, implementation, m.encodeFunctionCall(implementation, 'initialize', metadata.initArgs)], + options, + ) + return loadProxyWithABI(m, proxy, metadata, { ...options, after: [upgradeCall] }) +} diff --git a/packages/horizon/ignition/modules/proxy/implementation.ts b/packages/horizon/ignition/modules/proxy/implementation.ts new file mode 100644 index 000000000..a1cd30c4c --- /dev/null +++ b/packages/horizon/ignition/modules/proxy/implementation.ts @@ -0,0 +1,22 @@ +import { ArgumentType, Artifact, ContractOptions, IgnitionModuleBuilder } from '@nomicfoundation/ignition-core' + +export type ImplementationMetadata = { + name: string + artifact?: Artifact + constructorArgs?: ArgumentType[] + initArgs?: ArgumentType[] +} + +export function deployImplementation( + m: IgnitionModuleBuilder, + contract: ImplementationMetadata, + options?: ContractOptions, +) { + let implementation + if (contract.artifact === undefined) { + implementation = m.contract(contract.name, contract.constructorArgs, options) + } else { + implementation = m.contract(contract.name, contract.artifact, contract.constructorArgs, options) + } + return implementation +} diff --git a/packages/horizon/ignition/modules/proxy/utils.ts b/packages/horizon/ignition/modules/proxy/utils.ts new file mode 100644 index 000000000..c6b7f4c2a --- /dev/null +++ b/packages/horizon/ignition/modules/proxy/utils.ts @@ -0,0 +1,23 @@ +import { + ContractAtFuture, + ContractFuture, + ContractOptions, + IgnitionModuleBuilder, +} from '@nomicfoundation/ignition-core' + +import type { ImplementationMetadata } from './implementation' + +export function loadProxyWithABI( + m: IgnitionModuleBuilder, + proxy: ContractFuture | ContractAtFuture, + contract: ImplementationMetadata, + options?: ContractOptions, +) { + let proxyWithABI + if (contract.artifact === undefined) { + proxyWithABI = m.contractAt(contract.name, proxy, options) + } else { + proxyWithABI = m.contractAt(`${contract.name}_ProxyWithABI`, contract.artifact, proxy, options) + } + return proxyWithABI +} diff --git a/packages/horizon/lib/forge-std b/packages/horizon/lib/forge-std new file mode 160000 index 000000000..e4aef94c1 --- /dev/null +++ b/packages/horizon/lib/forge-std @@ -0,0 +1 @@ +Subproject commit e4aef94c1768803a16fe19f7ce8b65defd027cfd diff --git a/packages/horizon/lib/openzeppelin-foundry-upgrades b/packages/horizon/lib/openzeppelin-foundry-upgrades new file mode 160000 index 000000000..4cd15fc50 --- /dev/null +++ b/packages/horizon/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit 4cd15fc50b141c77d8cc9ff8efb44d00e841a299 diff --git a/packages/horizon/natspec-smells.config.js b/packages/horizon/natspec-smells.config.js new file mode 100644 index 000000000..d6b05e812 --- /dev/null +++ b/packages/horizon/natspec-smells.config.js @@ -0,0 +1,10 @@ +/** + * List of supported options: https://github.com/defi-wonderland/natspec-smells?tab=readme-ov-file#options + */ + +/** @type {import('@defi-wonderland/natspec-smells').Config} */ +module.exports = { + include: 'contracts/**/*.sol', + exclude: ['test/**/*.sol', 'contracts/mocks/**/*.sol', 'contracts/**/LibFixedMath.sol'], + constructorNatspec: true, +} diff --git a/packages/horizon/package.json b/packages/horizon/package.json new file mode 100644 index 000000000..1cd779abe --- /dev/null +++ b/packages/horizon/package.json @@ -0,0 +1,78 @@ +{ + "name": "@graphprotocol/horizon", + "version": "0.0.1", + "description": "", + "author": "The Graph core devs", + "license": "GPL-2.0-or-later", + "types": "typechain-types/index.ts", + "files": [ + "build/contracts/**/*", + "README.md", + "addresses.json" + ], + "scripts": { + "lint": "yarn lint:ts && yarn lint:sol", + "lint:ts": "eslint '**/*.{js,ts}' --fix", + "lint:sol": "yarn lint:sol:prettier && yarn lint:sol:solhint", + "lint:sol:prettier": "prettier --write contracts/**/*.sol test/**/*.sol", + "lint:sol:solhint": "solhint --noPrompt --fix contracts/**/*.sol --config node_modules/solhint-graph-config/index.js", + "lint:sol:natspec": "natspec-smells --config natspec-smells.config.js", + "clean": "rm -rf build dist cache cache_forge typechain-types", + "build": "BUILD_RUN=true hardhat compile", + "test": "forge test && hardhat test" + }, + "devDependencies": { + "@defi-wonderland/natspec-smells": "^1.1.6", + "@graphprotocol/contracts": "workspace:^7.0.0", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.8", + "@nomicfoundation/hardhat-foundry": "^1.1.1", + "@nomicfoundation/hardhat-ignition": "^0.15.9", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.9", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.10", + "@nomicfoundation/ignition-core": "^0.15.9", + "@openzeppelin/contracts": "^5.0.2", + "@openzeppelin/contracts-upgradeable": "^5.0.2", + "@typechain/ethers-v6": "^0.5.0", + "@typechain/hardhat": "^9.0.0", + "@types/chai": "^4.2.0", + "@types/mocha": ">=9.1.0", + "@types/node": ">=16.0.0", + "chai": "^4.2.0", + "eslint": "^8.56.0", + "eslint-graph-config": "workspace:^0.0.1", + "ethers": "^6.13.4", + "hardhat": "^2.22.18", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-gas-reporter": "^1.0.8", + "hardhat-graph-protocol": "workspace:^0.0.1", + "hardhat-secure-accounts": "^1.0.5", + "hardhat-storage-layout": "^0.1.7", + "lint-staged": "^15.2.2", + "prettier": "^3.2.5", + "prettier-plugin-solidity": "^1.3.1", + "solhint": "^4.5.2", + "solhint-graph-config": "workspace:^0.0.1", + "solhint-plugin-graph": "workspace:^0.0.1", + "solidity-coverage": "^0.8.0", + "ts-node": ">=8.0.0", + "typechain": "^8.3.0", + "typescript": "^5.6.3" + }, + "lint-staged": { + "contracts/**/*.sol": [ + "yarn lint:sol" + ], + "**/*.ts": [ + "yarn lint:ts" + ], + "**/*.js": [ + "yarn lint:ts" + ], + "**/*.json": [ + "yarn lint:ts" + ] + } +} diff --git a/packages/horizon/prettier.config.js b/packages/horizon/prettier.config.js new file mode 100644 index 000000000..5b8e866f2 --- /dev/null +++ b/packages/horizon/prettier.config.js @@ -0,0 +1,2 @@ +const prettierGraphConfig = require('solhint-graph-config/prettier') +module.exports = prettierGraphConfig diff --git a/packages/horizon/remappings.txt b/packages/horizon/remappings.txt new file mode 100644 index 000000000..d27549557 --- /dev/null +++ b/packages/horizon/remappings.txt @@ -0,0 +1,8 @@ +@graphprotocol/contracts/=node_modules/@graphprotocol/contracts/ +forge-std/=lib/forge-std/src/ +ds-test/=lib/forge-std/lib/ds-test/src/ +eth-gas-reporter/=node_modules/eth-gas-reporter/ +hardhat/=node_modules/hardhat/ +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src \ No newline at end of file diff --git a/packages/horizon/scripts/pre-verify b/packages/horizon/scripts/pre-verify new file mode 100755 index 000000000..ec9884e63 --- /dev/null +++ b/packages/horizon/scripts/pre-verify @@ -0,0 +1,57 @@ +#!/bin/bash + +# Move external artifacts +cp -r ../contracts/build/contracts/contracts/* build/contracts/contracts +cp -r ../contracts/build/contracts/build-info/* build/contracts/build-info +cp -r build/contracts/@openzeppelin/contracts/proxy/transparent/* build/contracts/contracts + +# HardHat Ignition deployment ID +DEPLOYMENT_ID="${1:-chain-421614}" + +# .dbg.json files +DBG_DIR_SRC="./build/contracts/contracts" +DBG_DIR_DEST="./ignition/deployments/${DEPLOYMENT_ID}/artifacts" + +# build-info files +BUILD_INFO_DIR_SRC="./build/contracts/build-info" +BUILD_INFO_DIR_DEST="./ignition/deployments/${DEPLOYMENT_ID}/build-info" + +# Ensure the destination directories exist +mkdir -p "$DBG_DIR_DEST" +mkdir -p "$BUILD_INFO_DIR_DEST" + +# Copy .dbg.json files +echo "Searching for .dbg.json files in $DBG_DIR_SRC and copying them to $DBG_DIR_DEST..." +find "$DBG_DIR_SRC" -type f -name "*.dbg.json" | while read -r file; do + base_name=$(basename "$file" .dbg.json) + new_name="${base_name}#${base_name}.dbg.json" + + if [ ! -f "$DBG_DIR_DEST/$new_name" ]; then + cp "$file" "$DBG_DIR_DEST/$new_name" + fi + + jq '.buildInfo |= sub("../../../../"; "../") | .buildInfo |= sub("../../../"; "../") | .buildInfo |= sub("../../"; "../")' "$DBG_DIR_DEST/$new_name" > "${DBG_DIR_DEST}/${new_name}.tmp" && mv "${DBG_DIR_DEST}/${new_name}.tmp" "$DBG_DIR_DEST/$new_name" +done + +# Copy build-info files +echo "Searching for build-info files in $BUILD_INFO_DIR_SRC and copying them to $BUILD_INFO_DIR_DEST..." +find "$BUILD_INFO_DIR_SRC" -type f -name "*.json" | while read -r file; do + base_name=$(basename "$file" .json) + if [ ! -f "$BUILD_INFO_DIR_DEST/$base_name.json" ]; then + cp "$file" "$BUILD_INFO_DIR_DEST/$base_name.json" + fi +done + +echo "All files have been processed." + +# Patch proxy artifacts +cp "$DBG_DIR_DEST/GraphProxy#GraphProxy.dbg.json" "$DBG_DIR_DEST/HorizonProxies#GraphProxy_HorizonStaking.dbg.json" +cp "$DBG_DIR_DEST/GraphProxy#GraphProxy.dbg.json" "$DBG_DIR_DEST/L2Curation#GraphProxy.dbg.json" +cp "$DBG_DIR_DEST/GraphProxy#GraphProxy.dbg.json" "$DBG_DIR_DEST/L2GraphToken#GraphProxy.dbg.json" +cp "$DBG_DIR_DEST/GraphProxy#GraphProxy.dbg.json" "$DBG_DIR_DEST/L2GraphTokenGateway#GraphProxy.dbg.json" +cp "$DBG_DIR_DEST/GraphProxy#GraphProxy.dbg.json" "$DBG_DIR_DEST/RewardsManager#GraphProxy.dbg.json" +cp "$DBG_DIR_DEST/GraphProxy#GraphProxy.dbg.json" "$DBG_DIR_DEST/BridgeEscrow#GraphProxy.dbg.json" +cp "$DBG_DIR_DEST/GraphProxy#GraphProxy.dbg.json" "$DBG_DIR_DEST/EpochManager#GraphProxy.dbg.json" + +cp "$DBG_DIR_DEST/TransparentUpgradeableProxy#TransparentUpgradeableProxy.dbg.json" "$DBG_DIR_DEST/HorizonProxiesDeployer#TransparentUpgradeableProxy_GraphPayments.dbg.json" +cp "$DBG_DIR_DEST/TransparentUpgradeableProxy#TransparentUpgradeableProxy.dbg.json" "$DBG_DIR_DEST/HorizonProxiesDeployer#TransparentUpgradeableProxy_PaymentsEscrow.dbg.json" \ No newline at end of file diff --git a/packages/horizon/tasks/deploy.ts b/packages/horizon/tasks/deploy.ts new file mode 100644 index 000000000..9e19beb58 --- /dev/null +++ b/packages/horizon/tasks/deploy.ts @@ -0,0 +1,178 @@ +/* eslint-disable no-case-declarations */ +import { task, types } from 'hardhat/config' +import { IgnitionHelper } from 'hardhat-graph-protocol/sdk' + +import type { AddressBook } from '../../hardhat-graph-protocol/src/sdk/address-book' +import type { HardhatRuntimeEnvironment } from 'hardhat/types' + +import DeployModule from '../ignition/modules/deploy' + +task('deploy:protocol', 'Deploy a new version of the Graph Protocol Horizon contracts - no data services deployed') + .addOptionalParam('horizonConfig', 'Name of the Horizon configuration file to use. Format is "protocol..json5", file must be in the "ignition/configs/" directory. Defaults to network name.', undefined, types.string) + .setAction(async (args, hre: HardhatRuntimeEnvironment) => { + const graph = hre.graph() + + // Load configuration for the deployment + console.log('\n========== ⚙️ Deployment configuration ==========') + const { config: HorizonConfig, file } = IgnitionHelper.loadConfig('./ignition/configs/', 'protocol', args.horizonConfig ?? hre.network.name) + console.log(`Loaded migration configuration from ${file}`) + + // Display the deployer -- this also triggers the secure accounts prompt if being used + console.log('\n========== 🔑 Deployer account ==========') + const signers = await hre.ethers.getSigners() + const deployer = signers[0] + console.log('Using deployer account:', deployer.address) + const balance = await hre.ethers.provider.getBalance(deployer.address) + console.log('Deployer balance:', hre.ethers.formatEther(balance), 'ETH') + if (balance === 0n) { + console.error('Error: Deployer account has no ETH balance') + process.exit(1) + } + + // Deploy the contracts + console.log(`\n========== 🚧 Deploy protocol ==========`) + const deployment = await hre.ignition.deploy(DeployModule, { + displayUi: true, + parameters: HorizonConfig, + }) + + // Save the addresses to the address book + console.log('\n========== 📖 Updating address book ==========') + IgnitionHelper.saveToAddressBook(deployment, hre.network.config.chainId, graph.horizon!.addressBook) + console.log(`Address book at ${graph.horizon!.addressBook.file} updated!`) + + console.log('\n\n🎉 ✨ 🚀 ✅ Deployment complete! 🎉 ✨ 🚀 ✅') + }) + +task('deploy:migrate', 'Upgrade an existing version of the Graph Protocol v1 to Horizon - no data services deployed') + .addOptionalParam('horizonConfig', 'Name of the Horizon configuration file to use. Format is "migrate..json5", file must be in the "ignition/configs/" directory. Defaults to network name.', undefined, types.string) + .addOptionalParam('step', 'Migration step to run (1, 2, 3 or 4)', undefined, types.int) + .addFlag('patchConfig', 'Patch configuration file using address book values - does not save changes') + .setAction(async (args, hre: HardhatRuntimeEnvironment) => { + // Task parameters + const step: number = args.step ?? 0 + const patchConfig: boolean = args.patchConfig ?? false + + const graph = hre.graph() + console.log(getHorizonBanner()) + + // Migration step to run + console.log('\n========== 🏗️ Migration steps ==========') + const validSteps = [1, 2, 3, 4] + if (!validSteps.includes(step)) { + console.error(`Error: Invalid migration step provided: ${step}`) + console.error(`Valid steps are: ${validSteps.join(', ')}`) + process.exit(1) + } + console.log(`Running migration step: ${step}`) + + // Load configuration for the migration + console.log('\n========== ⚙️ Deployment configuration ==========') + const { config: HorizonMigrateConfig, file } = IgnitionHelper.loadConfig('./ignition/configs/', 'migrate', args.horizonConfig ?? hre.network.name) + console.log(`Loaded migration configuration from ${file}`) + + // Display the deployer -- this also triggers the secure accounts prompt if being used + console.log('\n========== 🔑 Deployer account ==========') + const signers = await hre.ethers.getSigners() + const deployer = signers[0] + console.log('Using deployer account:', deployer.address) + const balance = await hre.ethers.provider.getBalance(deployer.address) + console.log('Deployer balance:', hre.ethers.formatEther(balance), 'ETH') + if (balance === 0n) { + console.error('Error: Deployer account has no ETH balance') + process.exit(1) + } + + // Run migration step + console.log(`\n========== 🚧 Running migration: step ${step} ==========`) + const MigrationModule = require(`../ignition/modules/migrate/migrate-${step}`).default + const deployment = await hre.ignition.deploy( + MigrationModule, + { + displayUi: true, + parameters: patchConfig ? _patchStepConfig(step, HorizonMigrateConfig, graph.horizon!.addressBook, graph.subgraphService!.addressBook) : HorizonMigrateConfig, + deploymentId: `horizon-${hre.network.name}`, + }) + + // Update address book + console.log('\n========== 📖 Updating address book ==========') + IgnitionHelper.saveToAddressBook(deployment, hre.network.config.chainId, graph.horizon!.addressBook) + console.log(`Address book at ${graph.horizon!.addressBook.file} updated!`) + + console.log('\n\n🎉 ✨ 🚀 ✅ Migration complete! 🎉 ✨ 🚀 ✅') + }) + +// This function patches the Ignition configuration object using an address book to fill in the gaps +// The resulting configuration is not saved back to the configuration file +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function _patchStepConfig( + step: number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: any, + horizonAddressBook: AddressBook, + subgraphServiceAddressBook: AddressBook, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + let patchedConfig = config + + switch (step) { + case 2: + const GraphPayments = horizonAddressBook.getEntry('GraphPayments') + const PaymentsEscrow = horizonAddressBook.getEntry('PaymentsEscrow') + patchedConfig = IgnitionHelper.patchConfig(config, { + HorizonProxiesGovernor: { + graphPaymentsAddress: GraphPayments.address, + paymentsEscrowAddress: PaymentsEscrow.address, + }, + }) + break + case 3: + const SubgraphService3 = subgraphServiceAddressBook.getEntry('SubgraphService') + patchedConfig = IgnitionHelper.patchConfig(patchedConfig, { + $global: { + subgraphServiceAddress: SubgraphService3.address, + }, + }) + break + case 4: + const HorizonStaking = horizonAddressBook.getEntry('HorizonStaking') + const L2Curation = horizonAddressBook.getEntry('L2Curation') + const RewardsManager = horizonAddressBook.getEntry('RewardsManager') + const SubgraphService4 = subgraphServiceAddressBook.getEntry('SubgraphService') + patchedConfig = IgnitionHelper.patchConfig(patchedConfig, { + $global: { + subgraphServiceAddress: SubgraphService4.address, + }, + HorizonStakingGovernor: { + horizonStakingImplementationAddress: HorizonStaking.implementation, + }, + L2CurationGovernor: { + curationImplementationAddress: L2Curation.implementation, + }, + RewardsManagerGovernor: { + rewardsManagerImplementationAddress: RewardsManager.implementation, + }, + }) + break + } + + return patchedConfig +} + +function getHorizonBanner(): string { + return ` + ██╗ ██╗ ██████╗ ██████╗ ██╗███████╗ ██████╗ ███╗ ██╗ + ██║ ██║██╔═══██╗██╔══██╗██║╚══███╔╝██╔═══██╗████╗ ██║ + ███████║██║ ██║██████╔╝██║ ███╔╝ ██║ ██║██╔██╗ ██║ + ██╔══██║██║ ██║██╔══██╗██║ ███╔╝ ██║ ██║██║╚██╗██║ + ██║ ██║╚██████╔╝██║ ██║██║███████╗╚██████╔╝██║ ╚████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ + + ██╗ ██╗██████╗ ██████╗ ██████╗ █████╗ ██████╗ ███████╗ + ██║ ██║██╔══██╗██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██╔════╝ + ██║ ██║██████╔╝██║ ███╗██████╔╝███████║██║ ██║█████╗ + ██║ ██║██╔═══╝ ██║ ██║██╔══██╗██╔══██║██║ ██║██╔══╝ + ╚██████╔╝██║ ╚██████╔╝██║ ██║██║ ██║██████╔╝███████╗ + ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝ + ` +} diff --git a/packages/horizon/test/GraphBase.t.sol b/packages/horizon/test/GraphBase.t.sol new file mode 100644 index 000000000..fcbbf7b48 --- /dev/null +++ b/packages/horizon/test/GraphBase.t.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; +import { GraphProxyAdmin } from "@graphprotocol/contracts/contracts/upgrades/GraphProxyAdmin.sol"; +import { GraphProxy } from "@graphprotocol/contracts/contracts/upgrades/GraphProxy.sol"; +import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import { PaymentsEscrow } from "contracts/payments/PaymentsEscrow.sol"; +import { GraphPayments } from "contracts/payments/GraphPayments.sol"; +import { GraphTallyCollector } from "contracts/payments/collectors/GraphTallyCollector.sol"; +import { IHorizonStaking } from "contracts/interfaces/IHorizonStaking.sol"; +import { HorizonStaking } from "contracts/staking/HorizonStaking.sol"; +import { HorizonStakingExtension } from "contracts/staking/HorizonStakingExtension.sol"; +import { IHorizonStakingTypes } from "contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { MockGRTToken } from "../contracts/mocks/MockGRTToken.sol"; +import { EpochManagerMock } from "../contracts/mocks/EpochManagerMock.sol"; +import { RewardsManagerMock } from "../contracts/mocks/RewardsManagerMock.sol"; +import { CurationMock } from "../contracts/mocks/CurationMock.sol"; +import { Constants } from "./utils/Constants.sol"; +import { Users } from "./utils/Users.sol"; +import { Utils } from "./utils/Utils.sol"; + +abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { + /* + * VARIABLES + */ + + /* Contracts */ + + GraphProxyAdmin public proxyAdmin; + Controller public controller; + MockGRTToken public token; + GraphPayments public payments; + PaymentsEscrow public escrow; + IHorizonStaking public staking; + EpochManagerMock public epochManager; + RewardsManagerMock public rewardsManager; + CurationMock public curation; + GraphTallyCollector graphTallyCollector; + + HorizonStaking private stakingBase; + HorizonStakingExtension private stakingExtension; + + address subgraphDataServiceLegacyAddress = makeAddr("subgraphDataServiceLegacyAddress"); + address subgraphDataServiceAddress = makeAddr("subgraphDataServiceAddress"); + + address graphTokenGatewayAddress = makeAddr("GraphTokenGateway"); + + /* Users */ + + Users internal users; + + /* + * SET UP + */ + + function setUp() public virtual { + // Deploy ERC20 token + vm.prank(users.deployer); + token = new MockGRTToken(); + + // Setup Users + users = Users({ + governor: createUser("governor"), + deployer: createUser("deployer"), + indexer: createUser("indexer"), + operator: createUser("operator"), + gateway: createUser("gateway"), + verifier: createUser("verifier"), + delegator: createUser("delegator"), + legacySlasher: createUser("legacySlasher") + }); + + // Deploy protocol contracts + deployProtocolContracts(); + setupProtocol(); + unpauseProtocol(); + + // Label contracts + vm.label({ account: address(controller), newLabel: "Controller" }); + vm.label({ account: address(token), newLabel: "GraphToken" }); + vm.label({ account: address(payments), newLabel: "GraphPayments" }); + vm.label({ account: address(escrow), newLabel: "PaymentsEscrow" }); + vm.label({ account: address(staking), newLabel: "HorizonStaking" }); + vm.label({ account: address(stakingExtension), newLabel: "HorizonStakingExtension" }); + vm.label({ account: address(graphTallyCollector), newLabel: "GraphTallyCollector" }); + + // Ensure caller is back to the original msg.sender + vm.stopPrank(); + } + + function deployProtocolContracts() private { + vm.startPrank(users.governor); + proxyAdmin = new GraphProxyAdmin(); + controller = new Controller(); + + // Staking Proxy + resetPrank(users.deployer); + GraphProxy stakingProxy = new GraphProxy(address(0), address(proxyAdmin)); + + // GraphPayments predict address + bytes memory paymentsImplementationParameters = abi.encode(address(controller), protocolPaymentCut); + bytes memory paymentsImplementationBytecode = abi.encodePacked( + type(GraphPayments).creationCode, + paymentsImplementationParameters + ); + address predictedPaymentsImplementationAddress = _computeAddress( + "GraphPayments", + paymentsImplementationBytecode, + users.deployer + ); + + bytes memory paymentsProxyParameters = abi.encode( + predictedPaymentsImplementationAddress, + users.governor, + abi.encodeCall(GraphPayments.initialize, ()) + ); + bytes memory paymentsProxyBytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + paymentsProxyParameters + ); + address predictedPaymentsProxyAddress = _computeAddress( + "TransparentUpgradeableProxy", + paymentsProxyBytecode, + users.deployer + ); + + // PaymentsEscrow + bytes memory escrowImplementationParameters = abi.encode(address(controller), withdrawEscrowThawingPeriod); + bytes memory escrowImplementationBytecode = abi.encodePacked( + type(PaymentsEscrow).creationCode, + escrowImplementationParameters + ); + address predictedEscrowImplementationAddress = _computeAddress( + "PaymentsEscrow", + escrowImplementationBytecode, + users.deployer + ); + + bytes memory escrowProxyParameters = abi.encode( + predictedEscrowImplementationAddress, + users.governor, + abi.encodeCall(PaymentsEscrow.initialize, ()) + ); + bytes memory escrowProxyBytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + escrowProxyParameters + ); + address predictedEscrowProxyAddress = _computeAddress( + "TransparentUpgradeableProxy", + escrowProxyBytecode, + users.deployer + ); + + // Epoch Manager + epochManager = new EpochManagerMock(); + + // Rewards Manager + rewardsManager = new RewardsManagerMock(token, ALLOCATIONS_REWARD_CUT); + + // Curation + curation = new CurationMock(); + + // Setup controller + resetPrank(users.governor); + controller.setContractProxy(keccak256("GraphToken"), address(token)); + controller.setContractProxy(keccak256("PaymentsEscrow"), predictedEscrowProxyAddress); + controller.setContractProxy(keccak256("GraphPayments"), predictedPaymentsProxyAddress); + controller.setContractProxy(keccak256("Staking"), address(stakingProxy)); + controller.setContractProxy(keccak256("EpochManager"), address(epochManager)); + controller.setContractProxy(keccak256("RewardsManager"), address(rewardsManager)); + controller.setContractProxy(keccak256("Curation"), address(curation)); + controller.setContractProxy(keccak256("GraphTokenGateway"), graphTokenGatewayAddress); + controller.setContractProxy(keccak256("GraphProxyAdmin"), address(proxyAdmin)); + + resetPrank(users.deployer); + { + address paymentsImplementationAddress = _deployContract("GraphPayments", paymentsImplementationBytecode); + address paymentsProxyAddress = _deployContract("TransparentUpgradeableProxy", paymentsProxyBytecode); + assertEq(paymentsImplementationAddress, predictedPaymentsImplementationAddress); + assertEq(paymentsProxyAddress, predictedPaymentsProxyAddress); + payments = GraphPayments(paymentsProxyAddress); + } + + { + address escrowImplementationAddress = _deployContract("PaymentsEscrow", escrowImplementationBytecode); + address escrowProxyAddress = _deployContract("TransparentUpgradeableProxy", escrowProxyBytecode); + assertEq(escrowImplementationAddress, predictedEscrowImplementationAddress); + assertEq(escrowProxyAddress, predictedEscrowProxyAddress); + escrow = PaymentsEscrow(escrowProxyAddress); + } + + stakingExtension = new HorizonStakingExtension(address(controller), subgraphDataServiceLegacyAddress); + stakingBase = new HorizonStaking( + address(controller), + address(stakingExtension), + subgraphDataServiceLegacyAddress + ); + + graphTallyCollector = new GraphTallyCollector( + "GraphTallyCollector", + "1", + address(controller), + revokeSignerThawingPeriod + ); + + resetPrank(users.governor); + proxyAdmin.upgrade(stakingProxy, address(stakingBase)); + proxyAdmin.acceptProxy(stakingBase, stakingProxy); + staking = IHorizonStaking(address(stakingProxy)); + } + + function setupProtocol() private { + resetPrank(users.governor); + staking.setMaxThawingPeriod(MAX_THAWING_PERIOD); + epochManager.setEpochLength(EPOCH_LENGTH); + } + + function unpauseProtocol() private { + resetPrank(users.governor); + controller.setPaused(false); + } + + function createUser(string memory name) internal returns (address) { + address user = makeAddr(name); + vm.deal({ account: user, newBalance: 100 ether }); + deal({ token: address(token), to: user, give: type(uint256).max }); + vm.label({ account: user, newLabel: name }); + return user; + } + + /* + * TOKEN HELPERS + */ + + function mint(address _address, uint256 amount) internal { + deal({ token: address(token), to: _address, give: amount }); + } + + function approve(address spender, uint256 amount) internal { + token.approve(spender, amount); + } + + /* + * PRIVATE + */ + + function _computeAddress( + string memory contractName, + bytes memory bytecode, + address deployer + ) private pure returns (address) { + bytes32 salt = keccak256(abi.encodePacked(contractName, "Salt")); + return Create2.computeAddress(salt, keccak256(bytecode), deployer); + } + + function _deployContract(string memory contractName, bytes memory bytecode) private returns (address) { + bytes32 salt = keccak256(abi.encodePacked(contractName, "Salt")); + return Create2.deploy(0, salt, bytecode); + } +} diff --git a/packages/horizon/test/data-service/DataService.t.sol b/packages/horizon/test/data-service/DataService.t.sol new file mode 100644 index 000000000..4a29a523f --- /dev/null +++ b/packages/horizon/test/data-service/DataService.t.sol @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { IHorizonStakingMain } from "../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; +import { DataServiceBase } from "./implementations/DataServiceBase.sol"; +import { DataServiceOverride } from "./implementations/DataServiceOverride.sol"; +import { ProvisionManager } from "./../../contracts/data-service/utilities/ProvisionManager.sol"; +import { PPMMath } from "./../../contracts/libraries/PPMMath.sol"; + +contract DataServiceTest is HorizonStakingSharedTest { + DataServiceBase dataService; + DataServiceOverride dataServiceOverride; + + function setUp() public override { + super.setUp(); + + dataService = new DataServiceBase(address(controller)); + dataServiceOverride = new DataServiceOverride(address(controller)); + } + + function test_Constructor_WhenTheContractIsDeployedWithAValidController() external view { + _assert_delegationRatio(type(uint32).min); + _assert_provisionTokens_range(type(uint256).min, type(uint256).max); + _assert_verifierCut_range(type(uint32).min, uint32(PPMMath.MAX_PPM)); + _assert_thawingPeriod_range(type(uint64).min, type(uint64).max); + } + + // -- Delegation ratio -- + + function test_DelegationRatio_WhenSettingTheDelegationRatio(uint32 delegationRatio) external { + _assert_set_delegationRatio(delegationRatio); + } + + function test_DelegationRatio_WhenGettingTheDelegationRatio(uint32 ratio) external { + dataService.setDelegationRatio(ratio); + _assert_delegationRatio(ratio); + } + + // -- Provision tokens -- + + function test_ProvisionTokens_WhenSettingAValidRange(uint256 min, uint256 max) external { + vm.assume(min <= max); + _assert_set_provisionTokens_range(min, max); + } + + function test_ProvisionTokens_RevertWhen_SettingAnInvalidRange(uint256 min, uint256 max) external { + vm.assume(min > max); + + vm.expectRevert(abi.encodeWithSelector(ProvisionManager.ProvisionManagerInvalidRange.selector, min, max)); + dataService.setProvisionTokensRange(min, max); + } + + function test_ProvisionTokens_WhenGettingTheRange() external { + dataService.setProvisionTokensRange(dataService.PROVISION_TOKENS_MIN(), dataService.PROVISION_TOKENS_MAX()); + _assert_provisionTokens_range(dataService.PROVISION_TOKENS_MIN(), dataService.PROVISION_TOKENS_MAX()); + } + + function test_ProvisionTokens_WhenGettingTheRangeWithAnOverridenGetter() external { + // Overriden getter returns the const values regardless of the set range + dataServiceOverride.setProvisionTokensRange(0, 1); + (uint256 min, uint256 max) = dataServiceOverride.getProvisionTokensRange(); + + assertEq(min, dataServiceOverride.PROVISION_TOKENS_MIN()); + assertEq(max, dataServiceOverride.PROVISION_TOKENS_MAX()); + } + + function test_ProvisionTokens_WhenCheckingAValidProvision_WithThawing( + uint256 tokens, + uint256 tokensThaw + ) external useIndexer { + dataService.setProvisionTokensRange(dataService.PROVISION_TOKENS_MIN(), dataService.PROVISION_TOKENS_MAX()); + tokens = bound(tokens, dataService.PROVISION_TOKENS_MIN(), dataService.PROVISION_TOKENS_MAX()); + tokensThaw = bound(tokensThaw, tokens - dataService.PROVISION_TOKENS_MIN() + 1, tokens); + + _createProvision(users.indexer, address(dataService), tokens, 0, 0); + staking.thaw(users.indexer, address(dataService), tokensThaw); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "tokens", + tokens - tokensThaw, + dataService.PROVISION_TOKENS_MIN(), + dataService.PROVISION_TOKENS_MAX() + ) + ); + dataService.checkProvisionTokens(users.indexer); + } + + function test_ProvisionTokens_WhenCheckingAValidProvision(uint256 tokens) external useIndexer { + dataService.setProvisionTokensRange(dataService.PROVISION_TOKENS_MIN(), dataService.PROVISION_TOKENS_MAX()); + tokens = bound(tokens, dataService.PROVISION_TOKENS_MIN(), dataService.PROVISION_TOKENS_MAX()); + + _createProvision(users.indexer, address(dataService), tokens, 0, 0); + dataService.checkProvisionTokens(users.indexer); + } + + function test_ProvisionTokens_WhenCheckingWithAnOverridenChecker(uint256 tokens) external useIndexer { + vm.assume(tokens != 0); + dataServiceOverride.setProvisionTokensRange( + dataService.PROVISION_TOKENS_MIN(), + dataService.PROVISION_TOKENS_MAX() + ); + + // this checker accepts provisions with any amount of tokens + _createProvision(users.indexer, address(dataServiceOverride), tokens, 0, 0); + dataServiceOverride.checkProvisionTokens(users.indexer); + } + + function test_ProvisionTokens_RevertWhen_CheckingAnInvalidProvision(uint256 tokens) external useIndexer { + dataService.setProvisionTokensRange(dataService.PROVISION_TOKENS_MIN(), dataService.PROVISION_TOKENS_MAX()); + tokens = bound(tokens, 1, dataService.PROVISION_TOKENS_MIN() - 1); + + _createProvision(users.indexer, address(dataService), tokens, 0, 0); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "tokens", + tokens, + dataService.PROVISION_TOKENS_MIN(), + dataService.PROVISION_TOKENS_MAX() + ) + ); + dataService.checkProvisionTokens(users.indexer); + } + + // -- Verifier cut -- + + function test_VerifierCut_WhenSettingAValidRange(uint32 min, uint32 max) external { + vm.assume(min <= max); + vm.assume(max <= uint32(PPMMath.MAX_PPM)); + _assert_set_verifierCut_range(min, max); + } + + function test_VerifierCut_RevertWhen_SettingAnInvalidRange(uint32 min, uint32 max) external { + vm.assume(min > max); + + vm.expectRevert(abi.encodeWithSelector(ProvisionManager.ProvisionManagerInvalidRange.selector, min, max)); + dataService.setVerifierCutRange(min, max); + } + + function test_VerifierCut_RevertWhen_SettingAnInvalidMax(uint32 min, uint32 max) external { + vm.assume(max > uint32(PPMMath.MAX_PPM)); + vm.assume(min <= max); + + vm.expectRevert(abi.encodeWithSelector(ProvisionManager.ProvisionManagerInvalidRange.selector, min, max)); + dataService.setVerifierCutRange(min, max); + } + + function test_VerifierCut_WhenGettingTheRange() external { + dataService.setVerifierCutRange(dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX()); + _assert_verifierCut_range(dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX()); + } + + function test_VerifierCut_WhenGettingTheRangeWithAnOverridenGetter() external { + // Overriden getter returns the const values regardless of the set range + dataServiceOverride.setVerifierCutRange(0, 1); + (uint32 min, uint32 max) = dataServiceOverride.getVerifierCutRange(); + assertEq(min, dataServiceOverride.VERIFIER_CUT_MIN()); + assertEq(max, dataServiceOverride.VERIFIER_CUT_MAX()); + } + + function test_VerifierCut_WhenCheckingAValidProvision(uint32 verifierCut) external useIndexer { + dataService.setVerifierCutRange(dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX()); + verifierCut = uint32(bound(verifierCut, dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX())); + + _createProvision(users.indexer, address(dataService), dataService.PROVISION_TOKENS_MIN(), verifierCut, 0); + dataService.checkProvisionParameters(users.indexer, false); + } + + function test_VerifierCut_WhenCheckingWithAnOverridenChecker(uint32 verifierCut) external useIndexer { + verifierCut = uint32(bound(verifierCut, 0, uint32(PPMMath.MAX_PPM))); + dataServiceOverride.setVerifierCutRange(dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX()); + + // this checker accepts provisions with any verifier cut range + _createProvision(users.indexer, address(dataService), dataService.PROVISION_TOKENS_MIN(), verifierCut, 0); + dataServiceOverride.checkProvisionParameters(users.indexer, false); + } + + function test_VerifierCut_RevertWhen_CheckingAnInvalidProvision(uint32 verifierCut) external useIndexer { + dataService.setVerifierCutRange(dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX()); + verifierCut = uint32(bound(verifierCut, 0, dataService.VERIFIER_CUT_MIN() - 1)); + + _createProvision(users.indexer, address(dataService), dataService.PROVISION_TOKENS_MIN(), verifierCut, 0); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "maxVerifierCut", + verifierCut, + dataService.VERIFIER_CUT_MIN(), + dataService.VERIFIER_CUT_MAX() + ) + ); + dataService.checkProvisionParameters(users.indexer, false); + } + + // -- Thawing period -- + + function test_ThawingPeriod_WhenSettingAValidRange(uint64 min, uint64 max) external { + vm.assume(min <= max); + _assert_set_thawingPeriod_range(min, max); + } + + function test_ThawingPeriod_RevertWhen_SettingAnInvalidRange(uint64 min, uint64 max) external { + vm.assume(min > max); + + vm.expectRevert(abi.encodeWithSelector(ProvisionManager.ProvisionManagerInvalidRange.selector, min, max)); + dataService.setThawingPeriodRange(min, max); + } + + function test_ThawingPeriod_WhenGettingTheRange() external { + dataService.setThawingPeriodRange(dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()); + _assert_thawingPeriod_range(dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()); + } + + function test_ThawingPeriod_WhenGettingTheRangeWithAnOverridenGetter() external { + // Overriden getter returns the const values regardless of the set range + dataServiceOverride.setThawingPeriodRange(0, 1); + (uint64 min, uint64 max) = dataServiceOverride.getThawingPeriodRange(); + assertEq(min, dataServiceOverride.THAWING_PERIOD_MIN()); + assertEq(max, dataServiceOverride.THAWING_PERIOD_MAX()); + } + + function test_ThawingPeriod_WhenCheckingAValidProvision(uint64 thawingPeriod) external useIndexer { + dataService.setThawingPeriodRange(dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()); + thawingPeriod = uint32( + bound(thawingPeriod, dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()) + ); + + _createProvision(users.indexer, address(dataService), dataService.PROVISION_TOKENS_MIN(), 0, thawingPeriod); + dataService.checkProvisionParameters(users.indexer, false); + } + + function test_ThawingPeriod_WhenCheckingWithAnOverridenChecker(uint64 thawingPeriod) external useIndexer { + thawingPeriod = uint32(bound(thawingPeriod, 0, staking.getMaxThawingPeriod())); + dataServiceOverride.setThawingPeriodRange(dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()); + + // this checker accepts provisions with any verifier cut range + _createProvision(users.indexer, address(dataService), dataService.PROVISION_TOKENS_MIN(), 0, thawingPeriod); + dataServiceOverride.checkProvisionParameters(users.indexer, false); + } + + function test_ThawingPeriod_RevertWhen_CheckingAnInvalidProvision(uint64 thawingPeriod) external useIndexer { + dataService.setThawingPeriodRange(dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()); + thawingPeriod = uint32(bound(thawingPeriod, 0, dataService.THAWING_PERIOD_MIN() - 1)); + + _createProvision(users.indexer, address(dataService), dataService.PROVISION_TOKENS_MIN(), 0, thawingPeriod); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "thawingPeriod", + thawingPeriod, + dataService.THAWING_PERIOD_MIN(), + dataService.THAWING_PERIOD_MAX() + ) + ); + dataService.checkProvisionParameters(users.indexer, false); + } + + modifier givenProvisionParametersChanged() { + _; + } + + function test_ProvisionParameters_WhenTheNewParametersAreValid( + uint32 maxVerifierCut, + uint64 thawingPeriod + ) external givenProvisionParametersChanged useIndexer { + // bound to valid values + maxVerifierCut = uint32(bound(maxVerifierCut, dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX())); + thawingPeriod = uint64( + bound(thawingPeriod, dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()) + ); + + // set provision parameter ranges + dataService.setVerifierCutRange(dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX()); + dataService.setThawingPeriodRange(dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()); + + // stage provision parameter changes + _createProvision( + users.indexer, + address(dataService), + dataService.PROVISION_TOKENS_MIN(), + dataService.VERIFIER_CUT_MIN(), + dataService.THAWING_PERIOD_MIN() + ); + _setProvisionParameters(users.indexer, address(dataService), maxVerifierCut, thawingPeriod); + + // accept provision parameters + if (maxVerifierCut != dataService.VERIFIER_CUT_MIN() || thawingPeriod != dataService.THAWING_PERIOD_MIN()) { + vm.expectEmit(); + emit IHorizonStakingMain.ProvisionParametersSet( + users.indexer, + address(dataService), + maxVerifierCut, + thawingPeriod + ); + } + dataService.acceptProvisionParameters(users.indexer); + } + + function test_ProvisionParameters_RevertWhen_TheNewThawingPeriodIsInvalid( + uint64 thawingPeriod + ) external givenProvisionParametersChanged useIndexer { + // bound to invalid values + thawingPeriod = uint64(bound(thawingPeriod, 0, dataService.THAWING_PERIOD_MIN() - 1)); + + // set provision parameter ranges + dataService.setVerifierCutRange(dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX()); + dataService.setThawingPeriodRange(dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()); + + // stage provision parameter changes + _createProvision( + users.indexer, + address(dataService), + dataService.PROVISION_TOKENS_MIN(), + dataService.VERIFIER_CUT_MIN(), + dataService.THAWING_PERIOD_MIN() + ); + _setProvisionParameters(users.indexer, address(dataService), dataService.VERIFIER_CUT_MIN(), thawingPeriod); + + // accept provision parameters + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "thawingPeriod", + thawingPeriod, + dataService.THAWING_PERIOD_MIN(), + dataService.THAWING_PERIOD_MAX() + ) + ); + dataService.acceptProvisionParameters(users.indexer); + } + + function test_ProvisionParameters_RevertWhen_TheNewVerifierCutIsInvalid( + uint32 maxVerifierCut + ) external givenProvisionParametersChanged useIndexer { + // bound to valid values + maxVerifierCut = uint32(bound(maxVerifierCut, dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX())); + + // set provision parameter ranges + dataService.setVerifierCutRange(dataService.VERIFIER_CUT_MIN(), dataService.VERIFIER_CUT_MAX()); + dataService.setThawingPeriodRange(dataService.THAWING_PERIOD_MIN(), dataService.THAWING_PERIOD_MAX()); + + // stage provision parameter changes + _createProvision( + users.indexer, + address(dataService), + dataService.PROVISION_TOKENS_MIN(), + dataService.VERIFIER_CUT_MIN(), + dataService.THAWING_PERIOD_MIN() + ); + _setProvisionParameters(users.indexer, address(dataService), maxVerifierCut, dataService.THAWING_PERIOD_MIN()); + + // accept provision parameters + if (maxVerifierCut != dataService.VERIFIER_CUT_MIN()) { + vm.expectEmit(); + emit IHorizonStakingMain.ProvisionParametersSet( + users.indexer, + address(dataService), + maxVerifierCut, + dataService.THAWING_PERIOD_MIN() + ); + } + dataService.acceptProvisionParameters(users.indexer); + } + + // -- Assert functions -- + + function _assert_set_delegationRatio(uint32 ratio) internal { + vm.expectEmit(); + emit ProvisionManager.DelegationRatioSet(ratio); + dataService.setDelegationRatio(ratio); + _assert_delegationRatio(ratio); + } + + function _assert_delegationRatio(uint32 ratio) internal view { + uint32 _delegationRatio = dataService.getDelegationRatio(); + assertEq(_delegationRatio, ratio); + } + + function _assert_set_provisionTokens_range(uint256 min, uint256 max) internal { + vm.expectEmit(); + emit ProvisionManager.ProvisionTokensRangeSet(min, max); + dataService.setProvisionTokensRange(min, max); + _assert_provisionTokens_range(min, max); + } + + function _assert_provisionTokens_range(uint256 min, uint256 max) internal view { + (uint256 _min, uint256 _max) = dataService.getProvisionTokensRange(); + assertEq(_min, min); + assertEq(_max, max); + } + + function _assert_set_verifierCut_range(uint32 min, uint32 max) internal { + vm.expectEmit(); + emit ProvisionManager.VerifierCutRangeSet(min, max); + dataService.setVerifierCutRange(min, max); + _assert_verifierCut_range(min, max); + } + + function _assert_verifierCut_range(uint32 min, uint32 max) internal view { + (uint32 _min, uint32 _max) = dataService.getVerifierCutRange(); + assertEq(_min, min); + assertEq(_max, max); + } + + function _assert_set_thawingPeriod_range(uint64 min, uint64 max) internal { + vm.expectEmit(); + emit ProvisionManager.ThawingPeriodRangeSet(min, max); + dataService.setThawingPeriodRange(min, max); + _assert_thawingPeriod_range(min, max); + } + + function _assert_thawingPeriod_range(uint64 min, uint64 max) internal view { + (uint64 _min, uint64 _max) = dataService.getThawingPeriodRange(); + assertEq(_min, min); + assertEq(_max, max); + } +} diff --git a/packages/horizon/test/data-service/DataService.tree b/packages/horizon/test/data-service/DataService.tree new file mode 100644 index 000000000..cd016608e --- /dev/null +++ b/packages/horizon/test/data-service/DataService.tree @@ -0,0 +1,71 @@ +DataServiceTest::constructor_ +└── when the contract is deployed with a valid controller + └── it should set all it's ranges to max + +DataServiceTest::delegationRatio_ +├── when setting the delegation ratio +│ ├── it should emit an event +│ └── it should set the range +└── when getting the delegation ratio + └── it should return the correct value + +DataServiceTest::provisionTokens_ +├── when setting a valid range +│ ├── it should emit an event +│ └── it should set the range +├── when setting an invalid range +│ └── it should revert +├── when getting the range +│ └── it should return the correct value +├── when getting the range with an overriden getter +│ └── it should return the correct value +├── when checking a valid provision +│ └── it should not revert +├── when checking with an overriden checker +│ └── it should not revert +└── when checking an invalid provision + └── it should revert + +DataServiceTest::verifierCut_ +├── when setting a valid range +│ ├── it should emit an event +│ └── it should set the range +├── when setting an invalid range +│ └── it should revert +├── when getting the range +│ └── it should return the correct value +├── when getting the range with an overriden getter +│ └── it should return the correct value +├── when checking a valid provision +│ └── it should not revert +├── when checking with an overriden checker +│ └── it should not revert +└── when checking an invalid provision + └── it should revert + +DataServiceTest::thawingPeriod_ +├── when setting a valid range +│ ├── it should emit an event +│ └── it should set the range +├── when setting an invalid range +│ └── it should revert +├── when getting the range +│ └── it should return the correct value +├── when getting the range with an overriden getter +│ └── it should return the correct value +├── when checking a valid provision +│ └── it should not revert +├── when checking with an overriden checker +│ └── it should not revert +└── when checking an invalid provision + └── it should revert + +DataServiceTest::provisionParameters_ +└── given provision parameters changed + ├── when the new parameters are valid + │ ├── it should emit an event + │ └── it should set the new parameters + ├── when the new thawing period is invalid + │ └── it should revert + └── when the new verifier cut is invalid + └── it should revert \ No newline at end of file diff --git a/packages/horizon/test/data-service/DataServiceUpgradeable.t.sol b/packages/horizon/test/data-service/DataServiceUpgradeable.t.sol new file mode 100644 index 000000000..be33173f8 --- /dev/null +++ b/packages/horizon/test/data-service/DataServiceUpgradeable.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { GraphBaseTest } from "../GraphBase.t.sol"; +import { DataServiceBaseUpgradeable } from "./implementations/DataServiceBaseUpgradeable.sol"; +import { UnsafeUpgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import { PPMMath } from "./../../contracts/libraries/PPMMath.sol"; + +contract DataServiceUpgradeableTest is GraphBaseTest { + function test_WhenTheContractIsDeployed() external { + (DataServiceBaseUpgradeable dataService, DataServiceBaseUpgradeable implementation) = _deployDataService(); + + // via proxy - ensure that the proxy was initialized correctly + // these calls validate proxy storage was correctly initialized + uint32 delegationRatio = dataService.getDelegationRatio(); + assertEq(delegationRatio, type(uint32).min); + + (uint256 minTokens, uint256 maxTokens) = dataService.getProvisionTokensRange(); + assertEq(minTokens, type(uint256).min); + assertEq(maxTokens, type(uint256).max); + + (uint32 minVerifierCut, uint32 maxVerifierCut) = dataService.getVerifierCutRange(); + assertEq(minVerifierCut, type(uint32).min); + assertEq(maxVerifierCut, uint32(PPMMath.MAX_PPM)); + + (uint64 minThawingPeriod, uint64 maxThawingPeriod) = dataService.getThawingPeriodRange(); + assertEq(minThawingPeriod, type(uint64).min); + assertEq(maxThawingPeriod, type(uint64).max); + + // this ensures that implementation immutables were correctly initialized + // and they can be read via the proxy + assertEq(implementation.controller(), address(controller)); + assertEq(dataService.controller(), address(controller)); + } + + function _deployDataService() internal returns (DataServiceBaseUpgradeable, DataServiceBaseUpgradeable) { + // Deploy implementation + address implementation = address(new DataServiceBaseUpgradeable(address(controller))); + + // Deploy proxy + address proxy = UnsafeUpgrades.deployTransparentProxy( + implementation, + users.governor, + abi.encodeCall(DataServiceBaseUpgradeable.initialize, ()) + ); + + return (DataServiceBaseUpgradeable(proxy), DataServiceBaseUpgradeable(implementation)); + } +} diff --git a/packages/horizon/test/data-service/extensions/DataServiceFees.t.sol b/packages/horizon/test/data-service/extensions/DataServiceFees.t.sol new file mode 100644 index 000000000..345a86db7 --- /dev/null +++ b/packages/horizon/test/data-service/extensions/DataServiceFees.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { HorizonStakingSharedTest } from "../../shared/horizon-staking/HorizonStakingShared.t.sol"; +import { DataServiceImpFees } from "../implementations/DataServiceImpFees.sol"; +import { IDataServiceFees } from "../../../contracts/data-service/interfaces/IDataServiceFees.sol"; +import { ProvisionTracker } from "../../../contracts/data-service/libraries/ProvisionTracker.sol"; +import { LinkedList } from "../../../contracts/libraries/LinkedList.sol"; + +contract DataServiceFeesTest is HorizonStakingSharedTest { + function test_Lock_RevertWhen_ZeroTokensAreLocked() + external + useIndexer + useProvisionDataService(address(dataService), PROVISION_TOKENS, 0, 0) + { + vm.expectRevert(abi.encodeWithSignature("DataServiceFeesZeroTokens()")); + dataService.lockStake(users.indexer, 0); + } + + uint256 public constant PROVISION_TOKENS = 10_000_000 ether; + DataServiceImpFees dataService; + + function setUp() public override { + super.setUp(); + + dataService = new DataServiceImpFees(address(controller)); + } + + function test_Lock_WhenTheProvisionHasEnoughTokens( + uint256 tokens + ) external useIndexer useProvisionDataService(address(dataService), PROVISION_TOKENS, 0, 0) { + tokens = bound(tokens, 1, PROVISION_TOKENS / dataService.STAKE_TO_FEES_RATIO()); + + _assert_lockStake(users.indexer, tokens); + } + + function test_Lock_WhenTheProvisionHasJustEnoughTokens( + uint256 tokens, + uint256 steps + ) external useIndexer useProvisionDataService(address(dataService), PROVISION_TOKENS, 0, 0) { + // lock all provisioned stake in steps + // limit tokens to at least 1 per step + tokens = bound(tokens, 50, PROVISION_TOKENS / dataService.STAKE_TO_FEES_RATIO()); + steps = bound(steps, 1, 50); + uint256 stepAmount = tokens / steps; + + for (uint256 i = 0; i < steps; i++) { + _assert_lockStake(users.indexer, stepAmount); + } + + uint256 lockedStake = dataService.feesProvisionTracker(users.indexer); + uint256 delta = (tokens % steps); + assertEq(lockedStake, stepAmount * dataService.STAKE_TO_FEES_RATIO() * steps); + assertEq(tokens * dataService.STAKE_TO_FEES_RATIO() - lockedStake, delta * dataService.STAKE_TO_FEES_RATIO()); + } + + function test_Lock_RevertWhen_TheProvisionHasNotEnoughTokens( + uint256 tokens + ) external useIndexer useProvisionDataService(address(dataService), PROVISION_TOKENS, 0, 0) { + tokens = bound(tokens, 1, PROVISION_TOKENS / dataService.STAKE_TO_FEES_RATIO()); + + // lock everything + _assert_lockStake(users.indexer, PROVISION_TOKENS / dataService.STAKE_TO_FEES_RATIO()); + + // tryna lock some more + uint256 additionalTokens = 10000; + uint256 tokensRequired = dataService.feesProvisionTracker(users.indexer) + + additionalTokens * + dataService.STAKE_TO_FEES_RATIO(); + uint256 tokensAvailable = staking.getTokensAvailable(users.indexer, address(dataService), 0); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionTracker.ProvisionTrackerInsufficientTokens.selector, + tokensAvailable, + tokensRequired + ) + ); + dataService.lockStake(users.indexer, additionalTokens); + } + + function test_Release_WhenNIsValid( + uint256 tokens, + uint256 steps, + uint256 numClaimsToRelease + ) external useIndexer useProvisionDataService(address(dataService), PROVISION_TOKENS, 0, 0) { + // lock all provisioned stake in steps + // limit tokens to at least 1 per step + // limit steps to at least 15 so we stagger locks every 5 seconds to have some expired + tokens = bound(tokens, 50, PROVISION_TOKENS / dataService.STAKE_TO_FEES_RATIO()); + steps = bound(steps, 15, 50); + numClaimsToRelease = bound(numClaimsToRelease, 0, steps); + + uint256 stepAmount = tokens / steps; + + // lock tokens staggering the release + for (uint256 i = 0; i < steps; i++) { + _assert_lockStake(users.indexer, stepAmount); + vm.warp(block.timestamp + 5 seconds); + } + + // it should release all expired claims + _assert_releaseStake(users.indexer, numClaimsToRelease); + } + + function test_Release_WhenNIsNotValid( + uint256 tokens, + uint256 steps + ) external useIndexer useProvisionDataService(address(dataService), PROVISION_TOKENS, 0, 0) { + // lock all provisioned stake in steps + // limit tokens to at least 1 per step + // limit steps to at least 15 so we stagger locks every 5 seconds to have some expired + tokens = bound(tokens, 50, PROVISION_TOKENS / dataService.STAKE_TO_FEES_RATIO()); + steps = bound(steps, 15, 50); + + uint256 stepAmount = tokens / steps; + + // lock tokens staggering the release + for (uint256 i = 0; i < steps; i++) { + _assert_lockStake(users.indexer, stepAmount); + vm.warp(block.timestamp + 5 seconds); + } + + // it should revert + vm.expectRevert(abi.encodeWithSelector(LinkedList.LinkedListInvalidIterations.selector)); + dataService.releaseStake(steps + 1); + } + + // -- Assertion functions -- + // use struct to avoid 'stack too deep' error + struct CalcValues_LockStake { + uint256 unlockTimestamp; + uint256 stakeToLock; + bytes32 predictedClaimId; + } + function _assert_lockStake(address serviceProvider, uint256 tokens) private { + // before state + (bytes32 beforeHead, , uint256 beforeNonce, uint256 beforeCount) = dataService.claimsLists(serviceProvider); + uint256 beforeLockedStake = dataService.feesProvisionTracker(serviceProvider); + + // calc + CalcValues_LockStake memory calcValues = CalcValues_LockStake({ + unlockTimestamp: block.timestamp + dataService.LOCK_DURATION(), + stakeToLock: tokens * dataService.STAKE_TO_FEES_RATIO(), + predictedClaimId: keccak256(abi.encodePacked(address(dataService), serviceProvider, beforeNonce)) + }); + + // it should emit a an event + vm.expectEmit(); + emit IDataServiceFees.StakeClaimLocked( + serviceProvider, + calcValues.predictedClaimId, + calcValues.stakeToLock, + calcValues.unlockTimestamp + ); + dataService.lockStake(serviceProvider, tokens); + + // after state + uint256 afterLockedStake = dataService.feesProvisionTracker(serviceProvider); + (bytes32 afterHead, bytes32 afterTail, uint256 afterNonce, uint256 afterCount) = dataService.claimsLists( + serviceProvider + ); + + // it should lock the tokens + assertEq(beforeLockedStake + calcValues.stakeToLock, afterLockedStake); + + // it should create a stake claim + (uint256 claimTokens, uint256 createdAt, uint256 releasableAt, bytes32 nextClaim) = dataService.claims( + calcValues.predictedClaimId + ); + assertEq(claimTokens, calcValues.stakeToLock); + assertEq(createdAt, block.timestamp); + assertEq(releasableAt, calcValues.unlockTimestamp); + assertEq(nextClaim, bytes32(0)); + + // it should update the list + assertEq(afterCount, beforeCount + 1); + assertEq(afterNonce, beforeNonce + 1); + assertEq(afterHead, beforeCount == 0 ? calcValues.predictedClaimId : beforeHead); + assertEq(afterTail, calcValues.predictedClaimId); + } + + // use struct to avoid 'stack too deep' error + struct CalcValues_ReleaseStake { + uint256 claimsCount; + uint256 tokensReleased; + bytes32 head; + } + function _assert_releaseStake(address serviceProvider, uint256 numClaimsToRelease) private { + // before state + (bytes32 beforeHead, bytes32 beforeTail, uint256 beforeNonce, uint256 beforeCount) = dataService.claimsLists( + serviceProvider + ); + uint256 beforeLockedStake = dataService.feesProvisionTracker(serviceProvider); + + // calc and set events + vm.expectEmit(); + + CalcValues_ReleaseStake memory calcValues = CalcValues_ReleaseStake({ + claimsCount: 0, + tokensReleased: 0, + head: beforeHead + }); + while ( + calcValues.head != bytes32(0) && (calcValues.claimsCount < numClaimsToRelease || numClaimsToRelease == 0) + ) { + (uint256 claimTokens, , uint256 releasableAt, bytes32 nextClaim) = dataService.claims(calcValues.head); + if (releasableAt > block.timestamp) { + break; + } + + emit IDataServiceFees.StakeClaimReleased(serviceProvider, calcValues.head, claimTokens, releasableAt); + calcValues.head = nextClaim; + calcValues.tokensReleased += claimTokens; + calcValues.claimsCount++; + } + + // it should emit a an event + emit IDataServiceFees.StakeClaimsReleased(serviceProvider, calcValues.claimsCount, calcValues.tokensReleased); + dataService.releaseStake(numClaimsToRelease); + + // after state + (bytes32 afterHead, bytes32 afterTail, uint256 afterNonce, uint256 afterCount) = dataService.claimsLists( + serviceProvider + ); + uint256 afterLockedStake = dataService.feesProvisionTracker(serviceProvider); + + // it should release the tokens + assertEq(beforeLockedStake - calcValues.tokensReleased, afterLockedStake); + + // it should remove the processed claims from the list + assertEq(afterCount, beforeCount - calcValues.claimsCount); + assertEq(afterNonce, beforeNonce); + if (calcValues.claimsCount != 0) { + assertNotEq(afterHead, beforeHead); + } else { + assertEq(afterHead, beforeHead); + } + assertEq(afterHead, calcValues.head); + assertEq(afterTail, calcValues.claimsCount == beforeCount ? bytes32(0) : beforeTail); + } +} diff --git a/packages/horizon/test/data-service/extensions/DataServiceFees.tree b/packages/horizon/test/data-service/extensions/DataServiceFees.tree new file mode 100644 index 000000000..331627179 --- /dev/null +++ b/packages/horizon/test/data-service/extensions/DataServiceFees.tree @@ -0,0 +1,21 @@ +DataServiceFeesTest::lock_ +├── when zero tokens are locked +│ └── it should revert +├── when the provision has enough tokens +│ ├── it should lock the tokens +│ ├── it should create a stake claim +│ ├── it should update the claims list +│ └── it should emit a an event +├── when the provision has just enough tokens +│ ├── it should lock the tokens +│ ├── it should create a stake claim +│ ├── it should update the claims list +│ └── it should emit a an event +└──when the provision has not enough tokens + └── it should revert + +DataServiceFeesTest::release_ +├── when n is valid +│ └── it should release all expired claims +└── when n is not valid + └── it should release at most n expired claims \ No newline at end of file diff --git a/packages/horizon/test/data-service/extensions/DataServicePausable.t.sol b/packages/horizon/test/data-service/extensions/DataServicePausable.t.sol new file mode 100644 index 000000000..46cded2eb --- /dev/null +++ b/packages/horizon/test/data-service/extensions/DataServicePausable.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { HorizonStakingSharedTest } from "../../shared/horizon-staking/HorizonStakingShared.t.sol"; +import { DataServiceImpPausable } from "../implementations/DataServiceImpPausable.sol"; +import { IDataServicePausable } from "./../../../contracts/data-service/interfaces/IDataServicePausable.sol"; + +contract DataServicePausableTest is HorizonStakingSharedTest { + DataServiceImpPausable dataService; + + event Paused(address pauser); + event Unpaused(address unpauser); + + function setUp() public override { + super.setUp(); + + dataService = new DataServiceImpPausable(address(controller)); + } + + modifier whenTheCallerIsAPauseGuardian() { + _assert_setPauseGuardian(address(this), true); + _; + } + + function test_Pause_WhenTheProtocolIsNotPaused() external whenTheCallerIsAPauseGuardian { + _assert_pause(); + } + + function test_Pause_RevertWhen_TheProtocolIsPaused() external whenTheCallerIsAPauseGuardian { + _assert_pause(); + + vm.expectRevert(abi.encodeWithSignature("EnforcedPause()")); + dataService.pause(); + assertEq(dataService.paused(), true); + } + + function test_Pause_RevertWhen_TheCallerIsNotAPauseGuardian() external { + vm.expectRevert(abi.encodeWithSignature("DataServicePausableNotPauseGuardian(address)", address(this))); + dataService.pause(); + assertEq(dataService.paused(), false); + } + + function test_Unpause_WhenTheProtocolIsPaused() external whenTheCallerIsAPauseGuardian { + _assert_pause(); + _assert_unpause(); + } + + function test_Unpause_RevertWhen_TheProtocolIsNotPaused() external whenTheCallerIsAPauseGuardian { + vm.expectRevert(abi.encodeWithSignature("ExpectedPause()")); + dataService.unpause(); + assertEq(dataService.paused(), false); + } + + function test_Unpause_RevertWhen_TheCallerIsNotAPauseGuardian() external { + _assert_setPauseGuardian(address(this), true); + _assert_pause(); + _assert_setPauseGuardian(address(this), false); + + vm.expectRevert(abi.encodeWithSignature("DataServicePausableNotPauseGuardian(address)", address(this))); + dataService.unpause(); + assertEq(dataService.paused(), true); + } + + function test_SetPauseGuardian_WhenSettingAPauseGuardian() external { + _assert_setPauseGuardian(address(this), true); + } + + function test_SetPauseGuardian_WhenRemovingAPauseGuardian() external { + _assert_setPauseGuardian(address(this), true); + _assert_setPauseGuardian(address(this), false); + } + + function test_SetPauseGuardian_RevertWhen_AlreadyPauseGuardian() external { + _assert_setPauseGuardian(address(this), true); + vm.expectRevert( + abi.encodeWithSignature("DataServicePausablePauseGuardianNoChange(address,bool)", address(this), true) + ); + dataService.setPauseGuardian(address(this), true); + } + + function test_SetPauseGuardian_RevertWhen_AlreadyNotPauseGuardian() external { + _assert_setPauseGuardian(address(this), true); + _assert_setPauseGuardian(address(this), false); + vm.expectRevert( + abi.encodeWithSignature("DataServicePausablePauseGuardianNoChange(address,bool)", address(this), false) + ); + dataService.setPauseGuardian(address(this), false); + } + + function test_PausedProtectedFn_RevertWhen_TheProtocolIsPaused() external { + _assert_setPauseGuardian(address(this), true); + _assert_pause(); + + vm.expectRevert(abi.encodeWithSignature("EnforcedPause()")); + dataService.pausedProtectedFn(); + } + + function test_PausedProtectedFn_WhenTheProtocolIsNotPaused() external { + vm.expectEmit(); + emit DataServiceImpPausable.PausedProtectedFn(); + dataService.pausedProtectedFn(); + } + + function test_UnpausedProtectedFn_WhenTheProtocolIsPaused() external { + _assert_setPauseGuardian(address(this), true); + _assert_pause(); + + vm.expectEmit(); + emit DataServiceImpPausable.UnpausedProtectedFn(); + dataService.unpausedProtectedFn(); + } + + function test_UnpausedProtectedFn_RevertWhen_TheProtocolIsNotPaused() external { + vm.expectRevert(abi.encodeWithSignature("ExpectedPause()")); + dataService.unpausedProtectedFn(); + } + + function _assert_pause() private { + vm.expectEmit(); + emit Paused(address(this)); + dataService.pause(); + assertEq(dataService.paused(), true); + } + + function _assert_unpause() private { + vm.expectEmit(); + emit Unpaused(address(this)); + dataService.unpause(); + assertEq(dataService.paused(), false); + } + + function _assert_setPauseGuardian(address pauseGuardian, bool allowed) private { + vm.expectEmit(); + emit IDataServicePausable.PauseGuardianSet(pauseGuardian, allowed); + dataService.setPauseGuardian(pauseGuardian, allowed); + assertEq(dataService.pauseGuardians(pauseGuardian), allowed); + } +} diff --git a/packages/horizon/test/data-service/extensions/DataServicePausable.tree b/packages/horizon/test/data-service/extensions/DataServicePausable.tree new file mode 100644 index 000000000..1c162c55d --- /dev/null +++ b/packages/horizon/test/data-service/extensions/DataServicePausable.tree @@ -0,0 +1,35 @@ +DataServicePausableTest::pause_ +├── when the caller is a pause guardian +│ ├── when the protocol is not paused +│ │ └── it should pause the contract +│ └── when the protocol is paused +│ └── it should revert +└── when the caller is not a pause guardian + └── it should revert + +DataServicePausableTest::unpause_ +├── when the caller is a pause guardian +│ ├── when the protocol is paused +│ │ └── it should unpause the contract +│ └── when the protocol is not paused +│ └── it should revert +└── when the caller is not a pause guardian + └── it should revert + +DataServicePausableTest::setPauseGuardian_ +├── when setting a pause guardian +│ └── it should emit an event +└── when removing a pause guardian + └── it should emit an event + +DataServicePausableTest::pausedProtectedFn_ +├── when the protocol is paused +│ └── it should revert +└── when the protocol is not paused + └── it should emit an event + +DataServicePausableTest::unpausedProtectedFn_ +├── when the protocol is paused +│ └── it should emit an event +└── when the protocol is not paused + └── it should revert diff --git a/packages/horizon/test/data-service/extensions/DataServicePausableUpgradeable.t.sol b/packages/horizon/test/data-service/extensions/DataServicePausableUpgradeable.t.sol new file mode 100644 index 000000000..6e58810c1 --- /dev/null +++ b/packages/horizon/test/data-service/extensions/DataServicePausableUpgradeable.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { GraphBaseTest } from "../../GraphBase.t.sol"; +import { DataServiceImpPausableUpgradeable } from "../implementations/DataServiceImpPausableUpgradeable.sol"; +import { UnsafeUpgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import { PPMMath } from "./../../../contracts/libraries/PPMMath.sol"; + +contract DataServicePausableUpgradeableTest is GraphBaseTest { + function test_WhenTheContractIsDeployed() external { + ( + DataServiceImpPausableUpgradeable dataService, + DataServiceImpPausableUpgradeable implementation + ) = _deployDataService(); + + // via proxy - ensure that the proxy was initialized correctly + // these calls validate proxy storage was correctly initialized + uint32 delegationRatio = dataService.getDelegationRatio(); + assertEq(delegationRatio, type(uint32).min); + + (uint256 minTokens, uint256 maxTokens) = dataService.getProvisionTokensRange(); + assertEq(minTokens, type(uint256).min); + assertEq(maxTokens, type(uint256).max); + + (uint32 minVerifierCut, uint32 maxVerifierCut) = dataService.getVerifierCutRange(); + assertEq(minVerifierCut, type(uint32).min); + assertEq(maxVerifierCut, uint32(PPMMath.MAX_PPM)); + + (uint64 minThawingPeriod, uint64 maxThawingPeriod) = dataService.getThawingPeriodRange(); + assertEq(minThawingPeriod, type(uint64).min); + assertEq(maxThawingPeriod, type(uint64).max); + + // this ensures that implementation immutables were correctly initialized + // and they can be read via the proxy + assertEq(implementation.controller(), address(controller)); + assertEq(dataService.controller(), address(controller)); + } + + function _deployDataService() + internal + returns (DataServiceImpPausableUpgradeable, DataServiceImpPausableUpgradeable) + { + // Deploy implementation + address implementation = address(new DataServiceImpPausableUpgradeable(address(controller))); + + // Deploy proxy + address proxy = UnsafeUpgrades.deployTransparentProxy( + implementation, + users.governor, + abi.encodeCall(DataServiceImpPausableUpgradeable.initialize, ()) + ); + + return (DataServiceImpPausableUpgradeable(proxy), DataServiceImpPausableUpgradeable(implementation)); + } +} diff --git a/packages/horizon/test/data-service/implementations/DataServiceBase.sol b/packages/horizon/test/data-service/implementations/DataServiceBase.sol new file mode 100644 index 000000000..e31b21a7b --- /dev/null +++ b/packages/horizon/test/data-service/implementations/DataServiceBase.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { DataService } from "../../../contracts/data-service/DataService.sol"; +import { IGraphPayments } from "./../../../contracts/interfaces/IGraphPayments.sol"; + +contract DataServiceBase is DataService { + uint32 public constant DELEGATION_RATIO = 100; + uint256 public constant PROVISION_TOKENS_MIN = 50; + uint256 public constant PROVISION_TOKENS_MAX = 5000; + uint32 public constant VERIFIER_CUT_MIN = 5; + uint32 public constant VERIFIER_CUT_MAX = 100000; + uint64 public constant THAWING_PERIOD_MIN = 15; + uint64 public constant THAWING_PERIOD_MAX = 76; + + constructor(address controller) DataService(controller) initializer { + __DataService_init(); + } + + function register(address serviceProvider, bytes calldata data) external {} + + function acceptProvisionPendingParameters(address serviceProvider, bytes calldata data) external {} + + function startService(address serviceProvider, bytes calldata data) external {} + + function stopService(address serviceProvider, bytes calldata data) external {} + + function collect( + address serviceProvider, + IGraphPayments.PaymentTypes feeType, + bytes calldata data + ) external returns (uint256) {} + + function slash(address serviceProvider, bytes calldata data) external {} + + function setDelegationRatio(uint32 ratio) external { + _setDelegationRatio(ratio); + } + + function setProvisionTokensRange(uint256 min, uint256 max) external { + _setProvisionTokensRange(min, max); + } + + function setVerifierCutRange(uint32 min, uint32 max) external { + _setVerifierCutRange(min, max); + } + + function setThawingPeriodRange(uint64 min, uint64 max) external { + _setThawingPeriodRange(min, max); + } + + function checkProvisionTokens(address serviceProvider) external view { + _checkProvisionTokens(serviceProvider); + } + + function checkProvisionParameters(address serviceProvider, bool pending) external view { + _checkProvisionParameters(serviceProvider, pending); + } + + function acceptProvisionParameters(address serviceProvider) external { + _acceptProvisionParameters(serviceProvider); + } +} diff --git a/packages/horizon/test/data-service/implementations/DataServiceBaseUpgradeable.sol b/packages/horizon/test/data-service/implementations/DataServiceBaseUpgradeable.sol new file mode 100644 index 000000000..907ad6e3a --- /dev/null +++ b/packages/horizon/test/data-service/implementations/DataServiceBaseUpgradeable.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { DataService } from "../../../contracts/data-service/DataService.sol"; +import { IGraphPayments } from "./../../../contracts/interfaces/IGraphPayments.sol"; + +contract DataServiceBaseUpgradeable is DataService { + constructor(address controller_) DataService(controller_) { + _disableInitializers(); + } + + function initialize() external initializer { + __DataService_init(); + } + + function register(address serviceProvider, bytes calldata data) external {} + + function acceptProvisionPendingParameters(address serviceProvider, bytes calldata data) external {} + + function startService(address serviceProvider, bytes calldata data) external {} + + function stopService(address serviceProvider, bytes calldata data) external {} + + function collect( + address serviceProvider, + IGraphPayments.PaymentTypes feeType, + bytes calldata data + ) external returns (uint256) {} + + function slash(address serviceProvider, bytes calldata data) external {} + + function controller() external view returns (address) { + return address(_graphController()); + } +} diff --git a/packages/horizon/test/data-service/implementations/DataServiceImpFees.sol b/packages/horizon/test/data-service/implementations/DataServiceImpFees.sol new file mode 100644 index 000000000..163281d32 --- /dev/null +++ b/packages/horizon/test/data-service/implementations/DataServiceImpFees.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { DataService } from "../../../contracts/data-service/DataService.sol"; +import { DataServiceFees } from "../../../contracts/data-service/extensions/DataServiceFees.sol"; +import { IGraphPayments } from "./../../../contracts/interfaces/IGraphPayments.sol"; + +contract DataServiceImpFees is DataServiceFees { + uint256 public constant STAKE_TO_FEES_RATIO = 1000; + uint256 public constant LOCK_DURATION = 1 minutes; + + constructor(address controller) DataService(controller) initializer { + __DataService_init(); + } + + function register(address serviceProvider, bytes calldata data) external {} + + function acceptProvisionPendingParameters(address serviceProvider, bytes calldata data) external {} + + function startService(address serviceProvider, bytes calldata data) external {} + + function stopService(address serviceProvider, bytes calldata data) external {} + + function collect( + address serviceProvider, + IGraphPayments.PaymentTypes, + bytes calldata data + ) external returns (uint256) { + uint256 amount = abi.decode(data, (uint256)); + _releaseStake(serviceProvider, 0); + _lockStake(serviceProvider, amount * STAKE_TO_FEES_RATIO, block.timestamp + LOCK_DURATION); + return amount; + } + + function lockStake(address serviceProvider, uint256 amount) external { + _lockStake(serviceProvider, amount * STAKE_TO_FEES_RATIO, block.timestamp + LOCK_DURATION); + } + + function slash(address serviceProvider, bytes calldata data) external {} +} diff --git a/packages/horizon/test/data-service/implementations/DataServiceImpPausable.sol b/packages/horizon/test/data-service/implementations/DataServiceImpPausable.sol new file mode 100644 index 000000000..678f46cc0 --- /dev/null +++ b/packages/horizon/test/data-service/implementations/DataServiceImpPausable.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { DataService } from "../../../contracts/data-service/DataService.sol"; +import { DataServicePausable } from "../../../contracts/data-service/extensions/DataServicePausable.sol"; +import { IGraphPayments } from "./../../../contracts/interfaces/IGraphPayments.sol"; + +contract DataServiceImpPausable is DataServicePausable { + uint32 public constant DELEGATION_RATIO = 100; + uint256 public constant PROVISION_TOKENS_MIN = 50; + uint256 public constant PROVISION_TOKENS_MAX = 5000; + uint32 public constant VERIFIER_CUT_MIN = 5; + uint32 public constant VERIFIER_CUT_MAX = 100000; + uint64 public constant THAWING_PERIOD_MIN = 15; + uint64 public constant THAWING_PERIOD_MAX = 76; + + event PausedProtectedFn(); + event UnpausedProtectedFn(); + + constructor(address controller) DataService(controller) initializer { + __DataService_init(); + } + + function register(address serviceProvider, bytes calldata data) external {} + + function acceptProvisionPendingParameters(address serviceProvider, bytes calldata data) external {} + + function startService(address serviceProvider, bytes calldata data) external {} + + function stopService(address serviceProvider, bytes calldata data) external {} + + function collect( + address serviceProvider, + IGraphPayments.PaymentTypes feeType, + bytes calldata data + ) external returns (uint256) {} + + function slash(address serviceProvider, bytes calldata data) external {} + + function setPauseGuardian(address pauseGuardian, bool allowed) external { + _setPauseGuardian(pauseGuardian, allowed); + } + + function pausedProtectedFn() external whenNotPaused { + emit PausedProtectedFn(); + } + + function unpausedProtectedFn() external whenPaused { + emit UnpausedProtectedFn(); + } +} diff --git a/packages/horizon/test/data-service/implementations/DataServiceImpPausableUpgradeable.sol b/packages/horizon/test/data-service/implementations/DataServiceImpPausableUpgradeable.sol new file mode 100644 index 000000000..dd2ea21f1 --- /dev/null +++ b/packages/horizon/test/data-service/implementations/DataServiceImpPausableUpgradeable.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { DataService } from "../../../contracts/data-service/DataService.sol"; +import { DataServicePausableUpgradeable } from "../../../contracts/data-service/extensions/DataServicePausableUpgradeable.sol"; +import { IGraphPayments } from "./../../../contracts/interfaces/IGraphPayments.sol"; + +contract DataServiceImpPausableUpgradeable is DataServicePausableUpgradeable { + constructor(address controller_) DataService(controller_) { + _disableInitializers(); + } + + function initialize() external initializer { + __DataService_init(); + __DataServicePausable_init(); + } + + function register(address serviceProvider, bytes calldata data) external {} + + function acceptProvisionPendingParameters(address serviceProvider, bytes calldata data) external {} + + function startService(address serviceProvider, bytes calldata data) external {} + + function stopService(address serviceProvider, bytes calldata data) external {} + + function collect( + address serviceProvider, + IGraphPayments.PaymentTypes feeType, + bytes calldata data + ) external returns (uint256) {} + + function slash(address serviceProvider, bytes calldata data) external {} + + function controller() external view returns (address) { + return address(_graphController()); + } +} diff --git a/packages/horizon/test/data-service/implementations/DataServiceOverride.sol b/packages/horizon/test/data-service/implementations/DataServiceOverride.sol new file mode 100644 index 000000000..c5d50ca74 --- /dev/null +++ b/packages/horizon/test/data-service/implementations/DataServiceOverride.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { DataServiceBase } from "./DataServiceBase.sol"; + +contract DataServiceOverride is DataServiceBase { + constructor(address controller) DataServiceBase(controller) initializer { + __DataService_init(); + } + + function _getProvisionTokensRange() internal pure override returns (uint256, uint256) { + return (PROVISION_TOKENS_MIN, PROVISION_TOKENS_MAX); + } + + function _getVerifierCutRange() internal pure override returns (uint32, uint32) { + return (VERIFIER_CUT_MIN, VERIFIER_CUT_MAX); + } + + function _getThawingPeriodRange() internal pure override returns (uint64, uint64) { + return (THAWING_PERIOD_MIN, THAWING_PERIOD_MAX); + } + + function _checkProvisionTokens(address _serviceProvider) internal pure override {} + function _checkProvisionParameters(address _serviceProvider, bool pending) internal pure override {} +} diff --git a/packages/horizon/test/data-service/libraries/ProvisionTracker.t.sol b/packages/horizon/test/data-service/libraries/ProvisionTracker.t.sol new file mode 100644 index 000000000..7d8541cec --- /dev/null +++ b/packages/horizon/test/data-service/libraries/ProvisionTracker.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { HorizonStakingSharedTest } from "../../shared/horizon-staking/HorizonStakingShared.t.sol"; +import { ProvisionTrackerImplementation } from "./ProvisionTrackerImplementation.sol"; +import { ProvisionTracker } from "../../../contracts/data-service/libraries/ProvisionTracker.sol"; +import { IHorizonStaking } from "./../../../contracts/interfaces/IHorizonStaking.sol"; + +// Wrapper required because when using vm.expectRevert, the error is expected in the next immediate call +// Which in the case of this library is an internal call to the staking contract +// See: https://github.com/foundry-rs/foundry/issues/5454 +library ProvisionTrackerWrapper { + function lock( + mapping(address => uint256) storage self, + IHorizonStaking graphStaking, + address serviceProvider, + uint256 tokens, + uint32 delegationRatio + ) external { + ProvisionTracker.lock(self, graphStaking, serviceProvider, tokens, delegationRatio); + } + + function release(mapping(address => uint256) storage self, address serviceProvider, uint256 tokens) external { + ProvisionTracker.release(self, serviceProvider, tokens); + } +} + +contract ProvisionTrackerTest is HorizonStakingSharedTest, ProvisionTrackerImplementation { + using ProvisionTrackerWrapper for mapping(address => uint256); + + function test_Lock_GivenTheProvisionHasSufficientAvailableTokens( + uint256 tokens, + uint256 steps + ) external useIndexer useProvisionDataService(address(this), tokens, 0, 0) { + vm.assume(tokens > 0); + vm.assume(steps > 0); + vm.assume(steps < 100); + uint256 stepAmount = tokens / steps; + + for (uint256 i = 0; i < steps; i++) { + uint256 beforeLockedAmount = provisionTracker[users.indexer]; + provisionTracker.lock(staking, users.indexer, stepAmount, uint32(0)); + uint256 afterLockedAmount = provisionTracker[users.indexer]; + assertEq(afterLockedAmount, beforeLockedAmount + stepAmount); + } + + assertEq(provisionTracker[users.indexer], stepAmount * steps); + uint256 delta = (tokens % steps); + uint256 tokensAvailable = staking.getTokensAvailable(users.indexer, address(this), 0); + assertEq(tokensAvailable - provisionTracker[users.indexer], delta); + } + + function test_Lock_RevertGiven_TheProvisionHasInsufficientAvailableTokens( + uint256 tokens + ) external useIndexer useProvisionDataService(address(this), tokens, 0, 0) { + uint256 tokensToLock = tokens + 1; + vm.expectRevert( + abi.encodeWithSelector(ProvisionTracker.ProvisionTrackerInsufficientTokens.selector, tokens, tokensToLock) + ); + provisionTracker.lock(staking, users.indexer, tokensToLock, uint32(0)); + } + + function test_Release_GivenTheProvisionHasSufficientLockedTokens( + uint256 tokens, + uint256 steps + ) external useIndexer useProvisionDataService(address(this), tokens, 0, 0) { + vm.assume(tokens > 0); + vm.assume(steps > 0); + vm.assume(steps < 100); + + // setup + provisionTracker.lock(staking, users.indexer, tokens, uint32(0)); + + // lock entire provision, then unlock in steps + uint256 stepAmount = tokens / steps; + + for (uint256 i = 0; i < steps; i++) { + uint256 beforeLockedAmount = provisionTracker[users.indexer]; + provisionTracker.release(users.indexer, stepAmount); + uint256 afterLockedAmount = provisionTracker[users.indexer]; + assertEq(afterLockedAmount, beforeLockedAmount - stepAmount); + } + + assertEq(provisionTracker[users.indexer], tokens - stepAmount * steps); + uint256 delta = (tokens % steps); + assertEq(provisionTracker[users.indexer], delta); + } + + function test_Release_RevertGiven_TheProvisionHasInsufficientLockedTokens( + uint256 tokens + ) external useIndexer useProvisionDataService(address(this), tokens, 0, 0) { + // setup + provisionTracker.lock(staking, users.indexer, tokens, uint32(0)); + + uint256 tokensToRelease = tokens + 1; + vm.expectRevert( + abi.encodeWithSelector( + ProvisionTracker.ProvisionTrackerInsufficientTokens.selector, + tokens, + tokensToRelease + ) + ); + provisionTracker.release(users.indexer, tokensToRelease); + } +} diff --git a/packages/horizon/test/data-service/libraries/ProvisionTracker.tree b/packages/horizon/test/data-service/libraries/ProvisionTracker.tree new file mode 100644 index 000000000..34955ed76 --- /dev/null +++ b/packages/horizon/test/data-service/libraries/ProvisionTracker.tree @@ -0,0 +1,11 @@ +ProvisionTrackerTest::lock_ +├── given the provision has sufficient available tokens +│ └── it should lock the tokens +└── given the provision has insufficient available tokens + └── it should revert + +ProvisionTrackerTest::release_ +├── given the provision has sufficient locked tokens +│ └── it should release the tokens +└── given the provision has insufficient locked tokens + └── it should revert \ No newline at end of file diff --git a/packages/horizon/test/data-service/libraries/ProvisionTrackerImplementation.sol b/packages/horizon/test/data-service/libraries/ProvisionTrackerImplementation.sol new file mode 100644 index 000000000..d79d13da0 --- /dev/null +++ b/packages/horizon/test/data-service/libraries/ProvisionTrackerImplementation.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { ProvisionTracker } from "../../../contracts/data-service/libraries/ProvisionTracker.sol"; + +contract ProvisionTrackerImplementation { + mapping(address => uint256) public provisionTracker; +} diff --git a/packages/horizon/test/escrow/GraphEscrow.t.sol b/packages/horizon/test/escrow/GraphEscrow.t.sol new file mode 100644 index 000000000..4c6933adf --- /dev/null +++ b/packages/horizon/test/escrow/GraphEscrow.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; +import { IPaymentsEscrow } from "../../contracts/interfaces/IPaymentsEscrow.sol"; +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; + +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; +import { PaymentsEscrowSharedTest } from "../shared/payments-escrow/PaymentsEscrowShared.t.sol"; +import { PPMMath } from "../../contracts/libraries/PPMMath.sol"; + +contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { + using PPMMath for uint256; + + /* + * MODIFIERS + */ + + modifier approveEscrow(uint256 tokens) { + _approveEscrow(tokens); + _; + } + + modifier useDeposit(uint256 tokens) { + vm.assume(tokens > 0); + vm.assume(tokens <= MAX_STAKING_TOKENS); + _depositTokens(users.verifier, users.indexer, tokens); + _; + } + + modifier depositAndThawTokens(uint256 amount, uint256 thawAmount) { + vm.assume(amount > 0); + vm.assume(thawAmount > 0); + vm.assume(amount <= MAX_STAKING_TOKENS); + vm.assume(amount > thawAmount); + _depositTokens(users.verifier, users.indexer, amount); + escrow.thaw(users.verifier, users.indexer, thawAmount); + _; + } + + /* + * HELPERS + */ + + function _approveEscrow(uint256 tokens) internal { + token.approve(address(escrow), tokens); + } + + function _thawEscrow(address collector, address receiver, uint256 amount) internal { + (, address msgSender, ) = vm.readCallers(); + uint256 expectedThawEndTimestamp = block.timestamp + withdrawEscrowThawingPeriod; + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.Thaw(msgSender, collector, receiver, amount, expectedThawEndTimestamp); + escrow.thaw(collector, receiver, amount); + + (, uint256 amountThawing, uint256 thawEndTimestamp) = escrow.escrowAccounts(msgSender, collector, receiver); + assertEq(amountThawing, amount); + assertEq(thawEndTimestamp, expectedThawEndTimestamp); + } + + function _cancelThawEscrow(address collector, address receiver) internal { + (, address msgSender, ) = vm.readCallers(); + (, uint256 amountThawingBefore, uint256 thawEndTimestampBefore) = escrow.escrowAccounts( + msgSender, + collector, + receiver + ); + + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.CancelThaw(msgSender, collector, receiver, amountThawingBefore, thawEndTimestampBefore); + escrow.cancelThaw(collector, receiver); + + (, uint256 amountThawing, uint256 thawEndTimestamp) = escrow.escrowAccounts(msgSender, collector, receiver); + assertEq(amountThawing, 0); + assertEq(thawEndTimestamp, 0); + } + + function _withdrawEscrow(address collector, address receiver) internal { + (, address msgSender, ) = vm.readCallers(); + + (uint256 balanceBefore, uint256 amountThawingBefore, ) = escrow.escrowAccounts(msgSender, collector, receiver); + uint256 tokenBalanceBeforeSender = token.balanceOf(msgSender); + uint256 tokenBalanceBeforeEscrow = token.balanceOf(address(escrow)); + + uint256 amountToWithdraw = amountThawingBefore > balanceBefore ? balanceBefore : amountThawingBefore; + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.Withdraw(msgSender, collector, receiver, amountToWithdraw); + escrow.withdraw(collector, receiver); + + (uint256 balanceAfter, uint256 tokensThawingAfter, uint256 thawEndTimestampAfter) = escrow.escrowAccounts( + msgSender, + collector, + receiver + ); + uint256 tokenBalanceAfterSender = token.balanceOf(msgSender); + uint256 tokenBalanceAfterEscrow = token.balanceOf(address(escrow)); + + assertEq(balanceAfter, balanceBefore - amountToWithdraw); + assertEq(tokensThawingAfter, 0); + assertEq(thawEndTimestampAfter, 0); + + assertEq(tokenBalanceAfterSender, tokenBalanceBeforeSender + amountToWithdraw); + assertEq(tokenBalanceAfterEscrow, tokenBalanceBeforeEscrow - amountToWithdraw); + } + + struct CollectPaymentData { + uint256 escrowBalance; + uint256 paymentsBalance; + uint256 receiverBalance; + uint256 delegationPoolBalance; + uint256 dataServiceBalance; + uint256 payerEscrowBalance; + } + + function _collectEscrow( + IGraphPayments.PaymentTypes _paymentType, + address _payer, + address _receiver, + uint256 _tokens, + address _dataService, + uint256 _dataServiceCut + ) internal { + (, address _collector, ) = vm.readCallers(); + + // Previous balances + CollectPaymentData memory previousBalances = CollectPaymentData({ + escrowBalance: token.balanceOf(address(escrow)), + paymentsBalance: token.balanceOf(address(payments)), + receiverBalance: token.balanceOf(_receiver), + delegationPoolBalance: staking.getDelegatedTokensAvailable(_receiver, _dataService), + dataServiceBalance: token.balanceOf(_dataService), + payerEscrowBalance: 0 + }); + + { + (uint256 payerEscrowBalance, , ) = escrow.escrowAccounts(_payer, _collector, _receiver); + previousBalances.payerEscrowBalance = payerEscrowBalance; + } + + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.EscrowCollected(_paymentType, _payer, _collector, _receiver, _tokens); + escrow.collect(_paymentType, _payer, _receiver, _tokens, _dataService, _dataServiceCut); + + // Calculate cuts + // this is nasty but stack is indeed too deep + uint256 tokensDataService = (_tokens - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT())).mulPPMRoundUp( + _dataServiceCut + ); + uint256 tokensDelegation = 0; + IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool(_receiver, _dataService); + if (pool.shares > 0) { + tokensDelegation = (_tokens - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()) - tokensDataService) + .mulPPMRoundUp(staking.getDelegationFeeCut(_receiver, _dataService, _paymentType)); + } + uint256 receiverExpectedPayment = _tokens - + _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()) - + tokensDataService - + tokensDelegation; + + // After balances + CollectPaymentData memory afterBalances = CollectPaymentData({ + escrowBalance: token.balanceOf(address(escrow)), + paymentsBalance: token.balanceOf(address(payments)), + receiverBalance: token.balanceOf(_receiver), + delegationPoolBalance: staking.getDelegatedTokensAvailable(_receiver, _dataService), + dataServiceBalance: token.balanceOf(_dataService), + payerEscrowBalance: 0 + }); + { + (uint256 afterPayerEscrowBalance, , ) = escrow.escrowAccounts(_payer, _collector, _receiver); + afterBalances.payerEscrowBalance = afterPayerEscrowBalance; + } + + // Check receiver balance after payment + assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, receiverExpectedPayment); + assertEq(token.balanceOf(address(payments)), 0); + + // Check delegation pool balance after payment + assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, tokensDelegation); + + // Check that the escrow account has been updated + assertEq(previousBalances.escrowBalance, afterBalances.escrowBalance + _tokens); + + // Check that payments balance didn't change + assertEq(previousBalances.paymentsBalance, afterBalances.paymentsBalance); + + // Check data service balance after payment + assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, tokensDataService); + + // Check payers escrow balance after payment + assertEq(previousBalances.payerEscrowBalance - _tokens, afterBalances.payerEscrowBalance); + } +} diff --git a/packages/horizon/test/escrow/collect.t.sol b/packages/horizon/test/escrow/collect.t.sol new file mode 100644 index 000000000..f4357d213 --- /dev/null +++ b/packages/horizon/test/escrow/collect.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowCollectTest is GraphEscrowTest { + /* + * TESTS + */ + + // use users.verifier as collector + function testCollect_Tokens( + uint256 tokens, + uint256 tokensToCollect, + uint256 delegationTokens, + uint256 dataServiceCut + ) + public + useIndexer + useProvision(tokens, 0, 0) + useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) + { + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + tokensToCollect = bound(tokensToCollect, 1, MAX_STAKING_TOKENS); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + resetPrank(users.gateway); + _depositTokens(users.verifier, users.indexer, tokensToCollect); + + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + + resetPrank(users.verifier); + _collectEscrow( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + tokensToCollect, + subgraphDataServiceAddress, + dataServiceCut + ); + } + + function testCollect_Tokens_NoProvision( + uint256 tokens, + uint256 dataServiceCut + ) public useIndexer useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + + resetPrank(users.gateway); + _depositTokens(users.verifier, users.indexer, tokens); + + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + + resetPrank(users.verifier); + _collectEscrow( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + tokens, + subgraphDataServiceAddress, + dataServiceCut + ); + } + + function testCollect_RevertWhen_SenderHasInsufficientAmountInEscrow( + uint256 amount, + uint256 insufficientAmount + ) public useGateway useDeposit(insufficientAmount) { + vm.assume(amount > 0); + vm.assume(insufficientAmount < amount); + + vm.startPrank(users.verifier); + bytes memory expectedError = abi.encodeWithSignature( + "PaymentsEscrowInsufficientBalance(uint256,uint256)", + insufficientAmount, + amount + ); + vm.expectRevert(expectedError); + escrow.collect( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + amount, + subgraphDataServiceAddress, + 0 + ); + vm.stopPrank(); + } + + function testCollect_MultipleCollections( + uint256 depositAmount, + uint256 firstCollect, + uint256 secondCollect + ) public useIndexer { + // Tests multiple collect operations from the same escrow account + vm.assume(firstCollect < MAX_STAKING_TOKENS); + vm.assume(secondCollect < MAX_STAKING_TOKENS); + vm.assume(depositAmount > 0); + vm.assume(firstCollect > 0 && firstCollect < depositAmount); + vm.assume(secondCollect > 0 && secondCollect <= depositAmount - firstCollect); + + resetPrank(users.gateway); + _depositTokens(users.verifier, users.indexer, depositAmount); + + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + + resetPrank(users.verifier); + _collectEscrow( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + firstCollect, + subgraphDataServiceAddress, + 0 + ); + + // _collectEscrow( + // IGraphPayments.PaymentTypes.QueryFee, + // users.gateway, + // users.indexer, + // secondCollect, + // subgraphDataServiceAddress, + // 0 + // ); + } +} diff --git a/packages/horizon/test/escrow/deposit.t.sol b/packages/horizon/test/escrow/deposit.t.sol new file mode 100644 index 000000000..bab8d0e5f --- /dev/null +++ b/packages/horizon/test/escrow/deposit.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowDepositTest is GraphEscrowTest { + /* + * TESTS + */ + + function testDeposit_Tokens(uint256 amount) public useGateway useDeposit(amount) { + (uint256 indexerEscrowBalance, , ) = escrow.escrowAccounts(users.gateway, users.verifier, users.indexer); + assertEq(indexerEscrowBalance, amount); + } + + function testDepositTo_Tokens(uint256 amount) public { + resetPrank(users.delegator); + token.approve(address(escrow), amount); + _depositToTokens(users.gateway, users.verifier, users.indexer, amount); + } + + // Tests multiple deposits accumulate correctly in the escrow account + function testDeposit_MultipleDeposits(uint256 amount1, uint256 amount2) public useGateway { + vm.assume(amount1 > 0); + vm.assume(amount2 > 0); + vm.assume(amount1 <= MAX_STAKING_TOKENS); + vm.assume(amount2 <= MAX_STAKING_TOKENS); + + _depositTokens(users.verifier, users.indexer, amount1); + _depositTokens(users.verifier, users.indexer, amount2); + + (uint256 balance, , ) = escrow.escrowAccounts(users.gateway, users.verifier, users.indexer); + assertEq(balance, amount1 + amount2); + } +} diff --git a/packages/horizon/test/escrow/getters.t.sol b/packages/horizon/test/escrow/getters.t.sol new file mode 100644 index 000000000..6434e1b30 --- /dev/null +++ b/packages/horizon/test/escrow/getters.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowGettersTest is GraphEscrowTest { + /* + * TESTS + */ + + function testGetBalance(uint256 amount) public useGateway useDeposit(amount) { + uint256 balance = escrow.getBalance(users.gateway, users.verifier, users.indexer); + assertEq(balance, amount); + } + + function testGetBalance_WhenThawing( + uint256 amountDeposit, + uint256 amountThawing + ) public useGateway useDeposit(amountDeposit) { + vm.assume(amountThawing > 0); + vm.assume(amountDeposit >= amountThawing); + + // thaw some funds + _thawEscrow(users.verifier, users.indexer, amountThawing); + + uint256 balance = escrow.getBalance(users.gateway, users.verifier, users.indexer); + assertEq(balance, amountDeposit - amountThawing); + } + + function testGetBalance_WhenCollectedOverThawing( + uint256 amountDeposit, + uint256 amountThawing, + uint256 amountCollected + ) public useGateway useDeposit(amountDeposit) { + vm.assume(amountThawing > 0); + vm.assume(amountDeposit > 0); + vm.assume(amountDeposit >= amountThawing); + vm.assume(amountDeposit >= amountCollected); + vm.assume(amountDeposit - amountCollected < amountThawing); + + // thaw some funds + _thawEscrow(users.verifier, users.indexer, amountThawing); + + // users start with max uint256 balance so we burn to avoid overflow + // TODO: we should modify all tests to consider users have a max balance thats less than max uint256 + resetPrank(users.indexer); + token.burn(amountCollected); + + // collect some funds to get the balance of the account below the amount thawing + resetPrank(users.verifier); + _collectEscrow( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + amountCollected, + subgraphDataServiceAddress, + 0 + ); + + // balance should always be 0 since thawing funds > available funds + uint256 balance = escrow.getBalance(users.gateway, users.verifier, users.indexer); + assertEq(balance, 0); + } +} diff --git a/packages/horizon/test/escrow/paused.t.sol b/packages/horizon/test/escrow/paused.t.sol new file mode 100644 index 000000000..a75532ed6 --- /dev/null +++ b/packages/horizon/test/escrow/paused.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; +import { IPaymentsEscrow } from "../../contracts/interfaces/IPaymentsEscrow.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowPausedTest is GraphEscrowTest { + /* + * MODIFIERS + */ + + modifier usePaused(bool paused) { + address msgSender; + (, msgSender, ) = vm.readCallers(); + resetPrank(users.governor); + controller.setPaused(paused); + resetPrank(msgSender); + _; + } + + /* + * TESTS + */ + + // Escrow + + function testPaused_RevertWhen_Deposit(uint256 tokens) public useGateway usePaused(true) { + vm.expectRevert(abi.encodeWithSelector(IPaymentsEscrow.PaymentsEscrowIsPaused.selector)); + escrow.deposit(users.verifier, users.indexer, tokens); + } + + function testPaused_RevertWhen_DepositTo(uint256 tokens) public usePaused(true) { + resetPrank(users.operator); + vm.expectRevert(abi.encodeWithSelector(IPaymentsEscrow.PaymentsEscrowIsPaused.selector)); + escrow.depositTo(users.gateway, users.verifier, users.indexer, tokens); + } + + function testPaused_RevertWhen_ThawTokens(uint256 tokens) public useGateway useDeposit(tokens) usePaused(true) { + vm.expectRevert(abi.encodeWithSelector(IPaymentsEscrow.PaymentsEscrowIsPaused.selector)); + escrow.thaw(users.verifier, users.indexer, tokens); + } + + function testPaused_RevertWhen_WithdrawTokens( + uint256 tokens, + uint256 thawAmount + ) public useGateway depositAndThawTokens(tokens, thawAmount) usePaused(true) { + // advance time + skip(withdrawEscrowThawingPeriod + 1); + + vm.expectRevert(abi.encodeWithSelector(IPaymentsEscrow.PaymentsEscrowIsPaused.selector)); + escrow.withdraw(users.verifier, users.indexer); + } + + // Collect + + function testPaused_RevertWhen_CollectTokens(uint256 tokens, uint256 tokensDataService) public usePaused(true) { + resetPrank(users.verifier); + vm.expectRevert(abi.encodeWithSelector(IPaymentsEscrow.PaymentsEscrowIsPaused.selector)); + escrow.collect( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + tokens, + subgraphDataServiceAddress, + tokensDataService + ); + } +} diff --git a/packages/horizon/test/escrow/thaw.t.sol b/packages/horizon/test/escrow/thaw.t.sol new file mode 100644 index 000000000..f7c23371b --- /dev/null +++ b/packages/horizon/test/escrow/thaw.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowThawTest is GraphEscrowTest { + /* + * TESTS + */ + + function testThaw_PartialBalanceThaw( + uint256 amountDeposited, + uint256 amountThawed + ) public useGateway useDeposit(amountDeposited) { + vm.assume(amountThawed > 0); + vm.assume(amountThawed <= amountDeposited); + _thawEscrow(users.verifier, users.indexer, amountThawed); + } + + function testThaw_FullBalanceThaw(uint256 amount) public useGateway useDeposit(amount) { + vm.assume(amount > 0); + _thawEscrow(users.verifier, users.indexer, amount); + + uint256 availableBalance = escrow.getBalance(users.gateway, users.verifier, users.indexer); + assertEq(availableBalance, 0); + } + + function testThaw_Tokens_SuccesiveCalls(uint256 amount) public useGateway { + amount = bound(amount, 2, type(uint256).max - 10); + _depositTokens(users.verifier, users.indexer, amount); + + uint256 firstAmountToThaw = (amount + 2 - 1) / 2; + uint256 secondAmountToThaw = (amount + 10 - 1) / 10; + _thawEscrow(users.verifier, users.indexer, firstAmountToThaw); + _thawEscrow(users.verifier, users.indexer, secondAmountToThaw); + + (, address msgSender, ) = vm.readCallers(); + (, uint256 amountThawing, uint256 thawEndTimestamp) = escrow.escrowAccounts( + msgSender, + users.verifier, + users.indexer + ); + assertEq(amountThawing, secondAmountToThaw); + assertEq(thawEndTimestamp, block.timestamp + withdrawEscrowThawingPeriod); + } + + function testThaw_Tokens_RevertWhen_AmountIsZero() public useGateway { + bytes memory expectedError = abi.encodeWithSignature("PaymentsEscrowInvalidZeroTokens()"); + vm.expectRevert(expectedError); + escrow.thaw(users.verifier, users.indexer, 0); + } + + function testThaw_RevertWhen_InsufficientAmount( + uint256 amount, + uint256 overAmount + ) public useGateway useDeposit(amount) { + overAmount = bound(overAmount, amount + 1, type(uint256).max); + bytes memory expectedError = abi.encodeWithSignature( + "PaymentsEscrowInsufficientBalance(uint256,uint256)", + amount, + overAmount + ); + vm.expectRevert(expectedError); + escrow.thaw(users.verifier, users.indexer, overAmount); + } + + function testThaw_CancelRequest(uint256 amount) public useGateway useDeposit(amount) { + _thawEscrow(users.verifier, users.indexer, amount); + _cancelThawEscrow(users.verifier, users.indexer); + } + + function testThaw_CancelRequest_RevertWhen_NoThawing(uint256 amount) public useGateway useDeposit(amount) { + bytes memory expectedError = abi.encodeWithSignature("PaymentsEscrowNotThawing()"); + vm.expectRevert(expectedError); + escrow.cancelThaw(users.verifier, users.indexer); + } +} diff --git a/packages/horizon/test/escrow/withdraw.t.sol b/packages/horizon/test/escrow/withdraw.t.sol new file mode 100644 index 000000000..ff4d98650 --- /dev/null +++ b/packages/horizon/test/escrow/withdraw.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowWithdrawTest is GraphEscrowTest { + /* + * TESTS + */ + + function testWithdraw_Tokens( + uint256 amount, + uint256 thawAmount + ) public useGateway depositAndThawTokens(amount, thawAmount) { + // advance time + skip(withdrawEscrowThawingPeriod + 1); + + _withdrawEscrow(users.verifier, users.indexer); + vm.stopPrank(); + } + + function testWithdraw_RevertWhen_NotThawing(uint256 amount) public useGateway useDeposit(amount) { + bytes memory expectedError = abi.encodeWithSignature("PaymentsEscrowNotThawing()"); + vm.expectRevert(expectedError); + escrow.withdraw(users.verifier, users.indexer); + } + + function testWithdraw_RevertWhen_StillThawing( + uint256 amount, + uint256 thawAmount + ) public useGateway depositAndThawTokens(amount, thawAmount) { + bytes memory expectedError = abi.encodeWithSignature( + "PaymentsEscrowStillThawing(uint256,uint256)", + block.timestamp, + block.timestamp + withdrawEscrowThawingPeriod + ); + vm.expectRevert(expectedError); + escrow.withdraw(users.verifier, users.indexer); + } + + function testWithdraw_BalanceAfterCollect( + uint256 amountDeposited, + uint256 amountThawed, + uint256 amountCollected + ) public useGateway depositAndThawTokens(amountDeposited, amountThawed) { + vm.assume(amountCollected > 0); + vm.assume(amountCollected <= amountDeposited); + + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + + // collect + resetPrank(users.verifier); + _collectEscrow( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + amountCollected, + subgraphDataServiceAddress, + 0 + ); + + // Advance time to simulate the thawing period + skip(withdrawEscrowThawingPeriod + 1); + + // withdraw the remaining thawed balance + resetPrank(users.gateway); + _withdrawEscrow(users.verifier, users.indexer); + } +} diff --git a/packages/horizon/test/libraries/LinkedList.t.sol b/packages/horizon/test/libraries/LinkedList.t.sol new file mode 100644 index 000000000..1f7cb6ea9 --- /dev/null +++ b/packages/horizon/test/libraries/LinkedList.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import "forge-std/console.sol"; +import { Test } from "forge-std/Test.sol"; +import { LinkedList } from "../../contracts/libraries/LinkedList.sol"; + +import { ListImplementation } from "./ListImplementation.sol"; + +contract LinkedListTest is Test, ListImplementation { + using LinkedList for LinkedList.List; + + function setUp() internal { + list = LinkedList.List({ head: bytes32(0), tail: bytes32(0), nonce: 0, count: 0 }); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_Add_RevertGiven_TheItemIdIsZero() external { + vm.expectRevert(LinkedList.LinkedListInvalidZeroId.selector); + list.addTail(bytes32(0)); + } + + function test_Add_GivenTheListIsEmpty() external { + _assert_addItem(_buildItemId(list.nonce), 0); + } + + function test_Add_GivenTheListIsNotEmpty() external { + // init list + _assert_addItem(_buildItemId(list.nonce), 0); + + // add to a non empty list + _assert_addItem(_buildItemId(list.nonce), 1); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_Add_RevertGiven_TheListIsAtMaxSize() external { + for (uint256 i = 0; i < LinkedList.MAX_ITEMS; i++) { + bytes32 id = _buildItemId(list.nonce); + _addItemToList(list, id, i); + } + + vm.expectRevert(LinkedList.LinkedListMaxElementsExceeded.selector); + list.addTail(_buildItemId(list.nonce)); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_Remove_RevertGiven_TheListIsEmpty() external { + vm.expectRevert(LinkedList.LinkedListEmptyList.selector); + list.removeHead(_getNextItem, _deleteItem); + } + + function test_Remove_GivenTheListIsNotEmpty() external { + _assert_addItem(_buildItemId(list.nonce), 0); + _assert_removeItem(); + } + + function test_TraverseGivenTheListIsEmpty() external { + _assert_traverseList(_processItemAddition, abi.encode(0), 0, abi.encode(0)); + } + + modifier givenTheListIsNotEmpty() { + for (uint256 i = 0; i < LIST_LENGTH; i++) { + bytes32 id = _buildItemId(list.nonce); + _assert_addItem(id, i); + } + _; + } + + function test_TraverseWhenIterationsAreNotSpecified() external givenTheListIsNotEmpty { + // calculate sum of all item idexes - it's what _processItemAddition does + uint256 sum = 0; + for (uint256 i = 0; i < list.count; i++) { + sum += i; + } + _assert_traverseList(_processItemAddition, abi.encode(0), 0, abi.encode(sum)); + } + + function test_TraverseWhenIterationsAreSpecified(uint256 n) external givenTheListIsNotEmpty { + vm.assume(n > 0); + vm.assume(n < LIST_LENGTH); + uint256 sum = 0; + for (uint256 i = 0; i < n; i++) { + sum += i; + } + _assert_traverseList(_processItemAddition, abi.encode(0), n, abi.encode(sum)); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_TraverseWhenIterationsAreInvalid() external givenTheListIsNotEmpty { + uint256 n = LIST_LENGTH + 1; + uint256 sum = 0; + for (uint256 i = 0; i < n; i++) { + sum += i; + } + vm.expectRevert(LinkedList.LinkedListInvalidIterations.selector); + _assert_traverseList(_processItemAddition, abi.encode(0), n, abi.encode(sum)); + } + + // -- Assertions -- + function _assert_addItem(bytes32 id, uint256 idIndex) internal { + uint256 beforeNonce = list.nonce; + uint256 beforeCount = list.count; + bytes32 beforeHead = list.head; + + ids[idIndex] = _addItemToList(list, id, idIndex); + + uint256 afterNonce = list.nonce; + uint256 afterCount = list.count; + bytes32 afterTail = list.tail; + bytes32 afterHead = list.head; + + assertEq(afterNonce, beforeNonce + 1); + assertEq(afterCount, beforeCount + 1); + + if (beforeCount == 0) { + assertEq(afterHead, id); + } else { + assertEq(afterHead, beforeHead); + } + assertEq(afterTail, id); + } + + function _assert_removeItem() internal { + uint256 beforeNonce = list.nonce; + uint256 beforeCount = list.count; + bytes32 beforeTail = list.tail; + bytes32 beforeHead = list.head; + + Item memory beforeHeadItem = items[beforeHead]; + + list.removeHead(_getNextItem, _deleteItem); + + uint256 afterNonce = list.nonce; + uint256 afterCount = list.count; + bytes32 afterTail = list.tail; + bytes32 afterHead = list.head; + + assertEq(afterNonce, beforeNonce); + assertEq(afterCount, beforeCount - 1); + + if (afterCount == 0) { + assertEq(afterTail, bytes32(0)); + } else { + assertEq(afterTail, beforeTail); + } + assertEq(afterHead, beforeHeadItem.next); + } + + function _assert_traverseList( + function(bytes32, bytes memory) internal returns (bool, bytes memory) _processItem, + bytes memory _initAcc, + uint256 _n, + bytes memory _expectedAcc + ) internal { + uint256 beforeNonce = list.nonce; + uint256 beforeCount = list.count; + bytes32 beforeTail = list.tail; + bytes32 beforeHead = list.head; + + // calculate after head item + bytes32 calcAfterHead = beforeHead; + if (_n != 0) { + for (uint256 i = 0; i < _n; i++) { + calcAfterHead = _getNextItem(calcAfterHead); + } + } + + (uint256 processedCount, bytes memory acc) = list.traverse( + _getNextItem, + _processItem, + _deleteItem, + _initAcc, + _n + ); + uint256 afterNonce = list.nonce; + uint256 afterCount = list.count; + bytes32 afterTail = list.tail; + bytes32 afterHead = list.head; + + assertEq(processedCount, _n == 0 ? beforeCount : _n); + assertEq(acc, _expectedAcc); + + assertEq(afterNonce, beforeNonce); + assertEq(afterCount, _n == 0 ? 0 : beforeCount - _n); + + if (_n == 0) { + assertEq(afterTail, bytes32(0)); + assertEq(afterHead, bytes32(0)); + } else { + assertEq(afterTail, beforeTail); + assertEq(afterHead, calcAfterHead); + } + } +} diff --git a/packages/horizon/test/libraries/LinkedList.tree b/packages/horizon/test/libraries/LinkedList.tree new file mode 100644 index 000000000..dc5143cb4 --- /dev/null +++ b/packages/horizon/test/libraries/LinkedList.tree @@ -0,0 +1,34 @@ +LinkedListTest::add_ +├── given the list is empty +│ ├── it should have a count of 1 and nonce of 1 +│ ├── it should have the tail set to the element +│ └── it should have the head set to the element +├── given the list is not empty +│ ├── it should have a count of 2 and nonce of 2 +│ ├── it should have the tail set to the new element +│ └── it should have the head set to the old element +└── given the list is at max size + └── it should revert + +LinkedListTest::remove_ +├── given the list is empty +│ └── it should revert +└── given the list is not empty + ├── it should have a count of 0 + ├── it should have the tail set to bytes32(0) + └── it should have the head set to bytes32(0) + +LinkedListTest::traverse +├── given the list is empty +│ ├── it should return zero processed elements +│ └── it should return accumulator value same as the initial value +└── given the list is not empty + └── when iterations are not specified + │ ├── it should return process all elements + │ └── it should return accumulator value equal to the processed value + └── when iterations are specified + │ ├── it should return process all elements + │ └── it should return accumulator value equal to the processed value + └── when iterations are invalid + ├── it should return process all elements + └── it should return accumulator value equal to the processed value diff --git a/packages/horizon/test/libraries/ListImplementation.sol b/packages/horizon/test/libraries/ListImplementation.sol new file mode 100644 index 000000000..fda762c6d --- /dev/null +++ b/packages/horizon/test/libraries/ListImplementation.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { LinkedList } from "../../contracts/libraries/LinkedList.sol"; + +contract ListImplementation { + using LinkedList for LinkedList.List; + + uint256 constant LIST_LENGTH = 100; + + struct Item { + uint256 data; + bytes32 next; + } + + LinkedList.List public list; + mapping(bytes32 id => Item data) public items; + bytes32[LIST_LENGTH] public ids; + + function _addItemToList(LinkedList.List storage _list, bytes32 _id, uint256 _data) internal returns (bytes32) { + items[_id] = Item({ data: _data, next: bytes32(0) }); + if (_list.count != 0) { + items[_list.tail].next = _id; + } + _list.addTail(_id); + return _id; + } + + function _deleteItem(bytes32 _id) internal { + delete items[_id]; + } + + function _getNextItem(bytes32 _id) internal view returns (bytes32) { + return items[_id].next; + } + + function _processItemAddition(bytes32 _id, bytes memory _acc) internal view returns (bool, bytes memory) { + uint256 sum = abi.decode(_acc, (uint256)); + sum += items[_id].data; + return (false, abi.encode(sum)); // dont break, do delete + } + + function _buildItemId(uint256 nonce) internal view returns (bytes32) { + // use block.number to salt the id generation to avoid + // accidentally using dirty state on repeat tests + return bytes32(keccak256(abi.encode(nonce, block.number))); + } +} diff --git a/packages/horizon/test/libraries/PPMMath.t.sol b/packages/horizon/test/libraries/PPMMath.t.sol new file mode 100644 index 000000000..e2240dbcd --- /dev/null +++ b/packages/horizon/test/libraries/PPMMath.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import "forge-std/console.sol"; +import { Test } from "forge-std/Test.sol"; +import { PPMMath } from "../../contracts/libraries/PPMMath.sol"; + +contract PPMMathTest is Test { + uint32 private constant MAX_PPM = 1000000; + + function test_mulPPM(uint256 a, uint256 b) public pure { + a = bound(a, 0, MAX_PPM); + b = bound(b, 0, type(uint256).max / MAX_PPM); + + uint256 result = PPMMath.mulPPM(a, b); + assertEq(result, (a * b) / MAX_PPM); + } + + function test_mulPPMRoundUp(uint256 a, uint256 b) public pure { + a = bound(a, 0, type(uint256).max / MAX_PPM); + b = bound(b, 0, MAX_PPM); + + uint256 result = PPMMath.mulPPMRoundUp(a, b); + assertEq(result, a - PPMMath.mulPPM(a, MAX_PPM - b)); + } + + function test_isValidPPM(uint256 value) public pure { + bool result = PPMMath.isValidPPM(value); + assert(result == (value <= MAX_PPM)); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_mullPPM_RevertWhen_InvalidPPM(uint256 a, uint256 b) public { + a = bound(a, MAX_PPM + 1, type(uint256).max); + b = bound(b, MAX_PPM + 1, type(uint256).max); + bytes memory expectedError = abi.encodeWithSelector(PPMMath.PPMMathInvalidMulPPM.selector, a, b); + vm.expectRevert(expectedError); + PPMMath.mulPPM(a, b); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_mullPPMRoundUp_RevertWhen_InvalidPPM(uint256 a, uint256 b) public { + b = bound(b, MAX_PPM + 1, type(uint256).max); + bytes memory expectedError = abi.encodeWithSelector(PPMMath.PPMMathInvalidPPM.selector, b); + vm.expectRevert(expectedError); + PPMMath.mulPPMRoundUp(a, b); + } +} diff --git a/packages/horizon/test/payments/GraphPayments.t.sol b/packages/horizon/test/payments/GraphPayments.t.sol new file mode 100644 index 000000000..9ab5fae1d --- /dev/null +++ b/packages/horizon/test/payments/GraphPayments.t.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingTypes } from "../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; +import { GraphPayments } from "../../contracts/payments/GraphPayments.sol"; + +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; +import { PPMMath } from "../../contracts/libraries/PPMMath.sol"; +import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; + +contract GraphPaymentsExtended is GraphPayments { + constructor(address controller, uint256 protocolPaymentCut) GraphPayments(controller, protocolPaymentCut) {} + + function readController() external view returns (address) { + return address(_graphController()); + } +} + +contract GraphPaymentsTest is HorizonStakingSharedTest { + using PPMMath for uint256; + + struct CollectPaymentData { + uint256 escrowBalance; + uint256 paymentsBalance; + uint256 receiverBalance; + uint256 delegationPoolBalance; + uint256 dataServiceBalance; + } + + function _collect( + IGraphPayments.PaymentTypes _paymentType, + address _receiver, + uint256 _tokens, + address _dataService, + uint256 _dataServiceCut + ) private { + // Previous balances + CollectPaymentData memory previousBalances = CollectPaymentData({ + escrowBalance: token.balanceOf(address(escrow)), + paymentsBalance: token.balanceOf(address(payments)), + receiverBalance: token.balanceOf(_receiver), + delegationPoolBalance: staking.getDelegatedTokensAvailable(_receiver, _dataService), + dataServiceBalance: token.balanceOf(_dataService) + }); + + // Calculate cuts + uint256 tokensProtocol = _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()); + uint256 tokensDataService = (_tokens - tokensProtocol).mulPPMRoundUp(_dataServiceCut); + uint256 tokensDelegation = 0; + { + IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool(_receiver, _dataService); + if (pool.shares > 0) { + tokensDelegation = (_tokens - tokensProtocol - tokensDataService).mulPPMRoundUp( + staking.getDelegationFeeCut(_receiver, _dataService, _paymentType) + ); + } + } + + uint256 receiverExpectedPayment = _tokens - tokensProtocol - tokensDataService - tokensDelegation; + + (, address msgSender, ) = vm.readCallers(); + vm.expectEmit(address(payments)); + emit IGraphPayments.GraphPaymentCollected( + _paymentType, + msgSender, + _receiver, + _dataService, + _tokens, + tokensProtocol, + tokensDataService, + tokensDelegation, + receiverExpectedPayment + ); + payments.collect(_paymentType, _receiver, _tokens, _dataService, _dataServiceCut); + + // After balances + CollectPaymentData memory afterBalances = CollectPaymentData({ + escrowBalance: token.balanceOf(address(escrow)), + paymentsBalance: token.balanceOf(address(payments)), + receiverBalance: token.balanceOf(_receiver), + delegationPoolBalance: staking.getDelegatedTokensAvailable(_receiver, _dataService), + dataServiceBalance: token.balanceOf(_dataService) + }); + + // Check receiver balance after payment + assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, receiverExpectedPayment); + assertEq(token.balanceOf(address(payments)), 0); + + // Check delegation pool balance after payment + assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, tokensDelegation); + + // Check that the escrow account has been updated + assertEq(previousBalances.escrowBalance, afterBalances.escrowBalance + _tokens); + + // Check that payments balance didn't change + assertEq(previousBalances.paymentsBalance, afterBalances.paymentsBalance); + + // Check data service balance after payment + assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, tokensDataService); + } + + /* + * TESTS + */ + + function testConstructor() public { + uint256 protocolCut = 100_000; + GraphPaymentsExtended newPayments = new GraphPaymentsExtended(address(controller), protocolCut); + assertEq(address(newPayments.readController()), address(controller)); + assertEq(newPayments.PROTOCOL_PAYMENT_CUT(), protocolCut); + } + + function testConstructor_RevertIf_InvalidProtocolPaymentCut(uint256 protocolPaymentCut) public { + protocolPaymentCut = bound(protocolPaymentCut, MAX_PPM + 1, type(uint256).max); + + resetPrank(users.deployer); + bytes memory expectedError = abi.encodeWithSelector( + IGraphPayments.GraphPaymentsInvalidCut.selector, + protocolPaymentCut + ); + vm.expectRevert(expectedError); + new GraphPayments(address(controller), protocolPaymentCut); + } + + function testInitialize() public { + // Deploy new instance to test initialization + GraphPayments newPayments = new GraphPayments(address(controller), 100_000); + + // Should revert if not called by onlyInitializer + vm.expectRevert(); + newPayments.initialize(); + } + + function testCollect( + uint256 amount, + uint256 amountToCollect, + uint256 dataServiceCut, + uint256 tokensDelegate, + uint256 delegationFeeCut + ) public useIndexer useProvision(amount, 0, 0) { + amountToCollect = bound(amountToCollect, 1, MAX_STAKING_TOKENS); + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + tokensDelegate = bound(tokensDelegate, 1, MAX_STAKING_TOKENS); + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); // Covers zero, max, and everything in between + + // Set delegation fee cut + _setDelegationFeeCut( + users.indexer, + subgraphDataServiceAddress, + IGraphPayments.PaymentTypes.QueryFee, + delegationFeeCut + ); + + // Delegate tokens + tokensDelegate = bound(tokensDelegate, MIN_DELEGATION, MAX_STAKING_TOKENS); + vm.startPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, tokensDelegate, 0); + + // Add tokens in escrow + address escrowAddress = address(escrow); + mint(escrowAddress, amount); + vm.startPrank(escrowAddress); + approve(address(payments), amount); + + // Collect payments through GraphPayments + _collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + dataServiceCut + ); + vm.stopPrank(); + } + + function testCollect_NoProvision( + uint256 amount, + uint256 dataServiceCut, + uint256 delegationFeeCut + ) public useIndexer { + amount = bound(amount, 1, MAX_STAKING_TOKENS); + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); // Covers zero, max, and everything in between + + // Set delegation fee cut + _setDelegationFeeCut( + users.indexer, + subgraphDataServiceAddress, + IGraphPayments.PaymentTypes.QueryFee, + delegationFeeCut + ); + + // Add tokens in escrow + address escrowAddress = address(escrow); + mint(escrowAddress, amount); + vm.startPrank(escrowAddress); + approve(address(payments), amount); + + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + + // Collect payments through GraphPayments + vm.startPrank(escrowAddress); + _collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + dataServiceCut + ); + vm.stopPrank(); + } + + function testCollect_RevertWhen_InvalidDataServiceCut( + uint256 amount, + uint256 dataServiceCut + ) + public + useIndexer + useProvision(amount, 0, 0) + useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) + { + dataServiceCut = bound(dataServiceCut, MAX_PPM + 1, type(uint256).max); + + resetPrank(users.deployer); + bytes memory expectedError = abi.encodeWithSelector( + IGraphPayments.GraphPaymentsInvalidCut.selector, + dataServiceCut + ); + vm.expectRevert(expectedError); + payments.collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + dataServiceCut + ); + } + + function testCollect_WithZeroAmount(uint256 amount) public useIndexer useProvision(amount, 0, 0) { + _collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, 0, subgraphDataServiceAddress, 0); + } + + function testCollect_RevertWhen_UnauthorizedCaller(uint256 amount) public useIndexer useProvision(amount, 0, 0) { + vm.assume(amount > 0 && amount <= MAX_STAKING_TOKENS); + + // Try to collect without being the escrow + resetPrank(users.indexer); + + vm.expectRevert( + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(payments), 0, amount) + ); + + payments.collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, 0); + } + + function testCollect_WithNoDelegation( + uint256 amount, + uint256 dataServiceCut, + uint256 delegationFeeCut + ) public useIndexer useProvision(amount, 0, 0) { + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); + + // Set delegation fee cut + _setDelegationFeeCut( + users.indexer, + subgraphDataServiceAddress, + IGraphPayments.PaymentTypes.QueryFee, + delegationFeeCut + ); + + // Add tokens in escrow + address escrowAddress = address(escrow); + mint(escrowAddress, amount); + vm.startPrank(escrowAddress); + approve(address(payments), amount); + + // Collect payments through GraphPayments + _collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + dataServiceCut + ); + vm.stopPrank(); + } + + function testCollect_ViaMulticall(uint256 amount) public useIndexer { + amount = bound(amount, 1, MAX_STAKING_TOKENS / 2); // Divide by 2 as we'll make two calls + + address escrowAddress = address(escrow); + mint(escrowAddress, amount * 2); + vm.startPrank(escrowAddress); + approve(address(payments), amount * 2); + + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector( + payments.collect.selector, + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + 100_000 // 10% + ); + data[1] = abi.encodeWithSelector( + payments.collect.selector, + IGraphPayments.PaymentTypes.IndexingFee, + users.indexer, + amount, + subgraphDataServiceAddress, + 200_000 // 20% + ); + + payments.multicall(data); + } +} diff --git a/packages/horizon/test/payments/graph-tally-collector/GraphTallyCollector.t.sol b/packages/horizon/test/payments/graph-tally-collector/GraphTallyCollector.t.sol new file mode 100644 index 000000000..4d0bb4233 --- /dev/null +++ b/packages/horizon/test/payments/graph-tally-collector/GraphTallyCollector.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IGraphTallyCollector } from "../../../contracts/interfaces/IGraphTallyCollector.sol"; +import { IPaymentsCollector } from "../../../contracts/interfaces/IPaymentsCollector.sol"; +import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol"; +import { IAuthorizable } from "../../../contracts/interfaces/IAuthorizable.sol"; +import { GraphTallyCollector } from "../../../contracts/payments/collectors/GraphTallyCollector.sol"; +import { PPMMath } from "../../../contracts/libraries/PPMMath.sol"; + +import { HorizonStakingSharedTest } from "../../shared/horizon-staking/HorizonStakingShared.t.sol"; +import { PaymentsEscrowSharedTest } from "../../shared/payments-escrow/PaymentsEscrowShared.t.sol"; + +contract GraphTallyTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { + using PPMMath for uint256; + + address signer; + uint256 signerPrivateKey; + + /* + * MODIFIERS + */ + + modifier useSigner() { + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + _authorizeSigner(signer, proofDeadline, signerProof); + _; + } + + /* + * SET UP + */ + + function setUp() public virtual override { + super.setUp(); + (signer, signerPrivateKey) = makeAddrAndKey("signer"); + vm.label({ account: signer, newLabel: "signer" }); + } + + /* + * HELPERS + */ + + function _getSignerProof(uint256 _proofDeadline, uint256 _signer) internal returns (bytes memory) { + (, address msgSender, ) = vm.readCallers(); + bytes32 messageHash = keccak256( + abi.encodePacked( + block.chainid, + address(graphTallyCollector), + "authorizeSignerProof", + _proofDeadline, + msgSender + ) + ); + bytes32 proofToDigest = MessageHashUtils.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signer, proofToDigest); + return abi.encodePacked(r, s, v); + } + + /* + * ACTIONS + */ + + function _authorizeSigner(address _signer, uint256 _proofDeadline, bytes memory _proof) internal { + (, address msgSender, ) = vm.readCallers(); + + vm.expectEmit(address(graphTallyCollector)); + emit IAuthorizable.SignerAuthorized(msgSender, _signer); + + graphTallyCollector.authorizeSigner(_signer, _proofDeadline, _proof); + assertTrue(graphTallyCollector.isAuthorized(msgSender, _signer)); + assertEq(graphTallyCollector.getThawEnd(_signer), 0); + } + + function _thawSigner(address _signer) internal { + (, address msgSender, ) = vm.readCallers(); + uint256 expectedThawEndTimestamp = block.timestamp + revokeSignerThawingPeriod; + + vm.expectEmit(address(graphTallyCollector)); + emit IAuthorizable.SignerThawing(msgSender, _signer, expectedThawEndTimestamp); + + graphTallyCollector.thawSigner(_signer); + + assertTrue(graphTallyCollector.isAuthorized(msgSender, _signer)); + assertEq(graphTallyCollector.getThawEnd(_signer), expectedThawEndTimestamp); + } + + function _cancelThawSigner(address _signer) internal { + (, address msgSender, ) = vm.readCallers(); + + vm.expectEmit(address(graphTallyCollector)); + emit IAuthorizable.SignerThawCanceled(msgSender, _signer, graphTallyCollector.getThawEnd(_signer)); + + graphTallyCollector.cancelThawSigner(_signer); + + assertTrue(graphTallyCollector.isAuthorized(msgSender, _signer)); + assertEq(graphTallyCollector.getThawEnd(_signer), 0); + } + + function _revokeAuthorizedSigner(address _signer) internal { + (, address msgSender, ) = vm.readCallers(); + + assertTrue(graphTallyCollector.isAuthorized(msgSender, _signer)); + assertLt(graphTallyCollector.getThawEnd(_signer), block.timestamp); + + vm.expectEmit(address(graphTallyCollector)); + emit IAuthorizable.SignerRevoked(msgSender, _signer); + + graphTallyCollector.revokeAuthorizedSigner(_signer); + + assertFalse(graphTallyCollector.isAuthorized(msgSender, _signer)); + } + + function _collect(IGraphPayments.PaymentTypes _paymentType, bytes memory _data) internal { + __collect(_paymentType, _data, 0); + } + + function _collect(IGraphPayments.PaymentTypes _paymentType, bytes memory _data, uint256 _tokensToCollect) internal { + __collect(_paymentType, _data, _tokensToCollect); + } + + function __collect( + IGraphPayments.PaymentTypes _paymentType, + bytes memory _data, + uint256 _tokensToCollect + ) internal { + (IGraphTallyCollector.SignedRAV memory signedRAV, ) = abi.decode( + _data, + (IGraphTallyCollector.SignedRAV, uint256) + ); + uint256 tokensAlreadyCollected = graphTallyCollector.tokensCollected( + signedRAV.rav.dataService, + signedRAV.rav.collectionId, + signedRAV.rav.serviceProvider, + signedRAV.rav.payer + ); + uint256 tokensToCollect = _tokensToCollect == 0 + ? signedRAV.rav.valueAggregate - tokensAlreadyCollected + : _tokensToCollect; + + vm.expectEmit(address(graphTallyCollector)); + emit IPaymentsCollector.PaymentCollected( + _paymentType, + signedRAV.rav.collectionId, + signedRAV.rav.payer, + signedRAV.rav.serviceProvider, + signedRAV.rav.dataService, + tokensToCollect + ); + vm.expectEmit(address(graphTallyCollector)); + emit IGraphTallyCollector.RAVCollected( + signedRAV.rav.collectionId, + signedRAV.rav.payer, + signedRAV.rav.serviceProvider, + signedRAV.rav.dataService, + signedRAV.rav.timestampNs, + signedRAV.rav.valueAggregate, + signedRAV.rav.metadata, + signedRAV.signature + ); + uint256 tokensCollected = _tokensToCollect == 0 + ? graphTallyCollector.collect(_paymentType, _data) + : graphTallyCollector.collect(_paymentType, _data, _tokensToCollect); + + uint256 tokensCollectedAfter = graphTallyCollector.tokensCollected( + signedRAV.rav.dataService, + signedRAV.rav.collectionId, + signedRAV.rav.serviceProvider, + signedRAV.rav.payer + ); + assertEq(tokensCollected, tokensToCollect); + assertEq( + tokensCollectedAfter, + _tokensToCollect == 0 ? signedRAV.rav.valueAggregate : tokensAlreadyCollected + _tokensToCollect + ); + } +} diff --git a/packages/horizon/test/payments/graph-tally-collector/collect/collect.t.sol b/packages/horizon/test/payments/graph-tally-collector/collect/collect.t.sol new file mode 100644 index 000000000..364536fbb --- /dev/null +++ b/packages/horizon/test/payments/graph-tally-collector/collect/collect.t.sol @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IGraphTallyCollector } from "../../../../contracts/interfaces/IGraphTallyCollector.sol"; +import { IGraphPayments } from "../../../../contracts/interfaces/IGraphPayments.sol"; + +import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; + +contract GraphTallyCollectTest is GraphTallyTest { + /* + * HELPERS + */ + + struct CollectTestParams { + uint256 tokens; + address allocationId; + address payer; + address indexer; + address collector; + } + + function _getQueryFeeEncodedData( + uint256 _signerPrivateKey, + CollectTestParams memory params + ) private view returns (bytes memory) { + IGraphTallyCollector.ReceiptAggregateVoucher memory rav = _getRAV( + params.allocationId, + params.payer, + params.indexer, + params.collector, + uint128(params.tokens) + ); + bytes32 messageHash = graphTallyCollector.encodeRAV(rav); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPrivateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + IGraphTallyCollector.SignedRAV memory signedRAV = IGraphTallyCollector.SignedRAV(rav, signature); + return abi.encode(signedRAV); + } + + function _getRAV( + address _allocationId, + address _payer, + address _indexer, + address _dataService, + uint128 _tokens + ) private pure returns (IGraphTallyCollector.ReceiptAggregateVoucher memory rav) { + return + IGraphTallyCollector.ReceiptAggregateVoucher({ + collectionId: bytes32(uint256(uint160(_allocationId))), + payer: _payer, + dataService: _dataService, + serviceProvider: _indexer, + timestampNs: 0, + valueAggregate: _tokens, + metadata: abi.encode("") + }); + } + + /* + * TESTS + */ + + function testGraphTally_Collect( + uint256 tokens + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_Multiple( + uint256 tokens, + uint8 steps + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + steps = uint8(bound(steps, 1, 100)); + tokens = bound(tokens, steps, type(uint128).max); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + resetPrank(users.verifier); + uint256 payed = 0; + uint256 tokensPerStep = tokens / steps; + for (uint256 i = 0; i < steps; i++) { + CollectTestParams memory params = CollectTestParams({ + tokens: payed + tokensPerStep, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + payed += tokensPerStep; + } + } + + function testGraphTally_Collect_RevertWhen_NoProvision(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + bytes memory expectedError = abi.encodeWithSelector( + IGraphTallyCollector.GraphTallyCollectorUnauthorizedDataService.selector, + users.verifier + ); + vm.expectRevert(expectedError); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_RevertWhen_ProvisionEmpty( + uint256 tokens + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + // thaw tokens from the provision + resetPrank(users.indexer); + staking.thaw(users.indexer, users.verifier, 100); + + tokens = bound(tokens, 1, type(uint128).max); + + resetPrank(users.gateway); + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + bytes memory expectedError = abi.encodeWithSelector( + IGraphTallyCollector.GraphTallyCollectorUnauthorizedDataService.selector, + users.verifier + ); + vm.expectRevert(expectedError); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_PreventSignerAttack( + uint256 tokens + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + resetPrank(users.gateway); + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + // The sender authorizes another signer + (address anotherSigner, uint256 anotherSignerPrivateKey) = makeAddrAndKey("anotherSigner"); + { + uint256 proofDeadline = block.timestamp + 1; + bytes memory anotherSignerProof = _getSignerProof(proofDeadline, anotherSignerPrivateKey); + _authorizeSigner(anotherSigner, proofDeadline, anotherSignerProof); + } + + // And crafts a RAV using the new signer as the data service + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: anotherSigner + }); + bytes memory data = _getQueryFeeEncodedData(anotherSignerPrivateKey, params); + + // the call should revert because the service provider has no provision with the "data service" + resetPrank(anotherSigner); + bytes memory expectedError = abi.encodeWithSelector( + IGraphTallyCollector.GraphTallyCollectorUnauthorizedDataService.selector, + anotherSigner + ); + vm.expectRevert(expectedError); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_RevertWhen_CallerNotDataService(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + resetPrank(users.gateway); + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.indexer); + bytes memory expectedError = abi.encodeWithSelector( + IGraphTallyCollector.GraphTallyCollectorCallerNotDataService.selector, + users.indexer, + users.verifier + ); + vm.expectRevert(expectedError); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_RevertWhen_PayerMismatch( + uint256 tokens + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + resetPrank(users.gateway); + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + (address anotherPayer, ) = makeAddrAndKey("anotherPayer"); + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: anotherPayer, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + vm.expectRevert(IGraphTallyCollector.GraphTallyCollectorInvalidRAVSigner.selector); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_RevertWhen_InconsistentRAVTokens( + uint256 tokens + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + + // Attempt to collect again + vm.expectRevert( + abi.encodeWithSelector( + IGraphTallyCollector.GraphTallyCollectorInconsistentRAVTokens.selector, + tokens, + tokens + ) + ); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_RevertWhen_SignerNotAuthorized(uint256 tokens) public useGateway { + tokens = bound(tokens, 1, type(uint128).max); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + vm.expectRevert(abi.encodeWithSelector(IGraphTallyCollector.GraphTallyCollectorInvalidRAVSigner.selector)); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_ThawingSigner( + uint256 tokens + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + // Start thawing signer + _thawSigner(signer); + skip(revokeSignerThawingPeriod + 1); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_RevertIf_SignerWasRevoked(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + // Start thawing signer + _thawSigner(signer); + skip(revokeSignerThawingPeriod + 1); + _revokeAuthorizedSigner(signer); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + vm.expectRevert(abi.encodeWithSelector(IGraphTallyCollector.GraphTallyCollectorInvalidRAVSigner.selector)); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_Collect_ThawingSignerCanceled( + uint256 tokens + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + // Start thawing signer + _thawSigner(signer); + skip(revokeSignerThawingPeriod + 1); + _cancelThawSigner(signer); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testGraphTally_CollectPartial( + uint256 tokens, + uint256 tokensToCollect + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + tokensToCollect = bound(tokensToCollect, 1, tokens); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data, tokensToCollect); + } + + function testGraphTally_CollectPartial_RevertWhen_AmountTooHigh( + uint256 tokens, + uint256 tokensToCollect + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max - 1); + + _depositTokens(address(graphTallyCollector), users.indexer, tokens); + + CollectTestParams memory params = CollectTestParams({ + tokens: tokens, + allocationId: _allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); + + resetPrank(users.verifier); + uint256 tokensAlreadyCollected = graphTallyCollector.tokensCollected( + users.verifier, + bytes32(uint256(uint160(_allocationId))), + users.indexer, + users.gateway + ); + tokensToCollect = bound(tokensToCollect, tokens - tokensAlreadyCollected + 1, type(uint128).max); + + vm.expectRevert( + abi.encodeWithSelector( + IGraphTallyCollector.GraphTallyCollectorInvalidTokensToCollectAmount.selector, + tokensToCollect, + tokens - tokensAlreadyCollected + ) + ); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data, tokensToCollect); + } + + function testGraphTally_Collect_SeparateAllocationTracking( + uint256 tokens + ) public useIndexer useProvisionDataService(users.verifier, 100, 0, 0) useGateway useSigner { + tokens = bound(tokens, 1, type(uint64).max); + uint8 numAllocations = 10; + + _depositTokens(address(graphTallyCollector), users.indexer, tokens * numAllocations); + // Array with collectTestParams for each allocation + CollectTestParams[] memory collectTestParams = new CollectTestParams[](numAllocations); + + // Collect tokens for each allocation + resetPrank(users.verifier); + for (uint256 i = 0; i < numAllocations; i++) { + address allocationId = makeAddr(string.concat("allocation", vm.toString(i))); + collectTestParams[i] = CollectTestParams({ + tokens: tokens, + allocationId: allocationId, + payer: users.gateway, + indexer: users.indexer, + collector: users.verifier + }); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, collectTestParams[i]); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + for (uint256 i = 0; i < numAllocations; i++) { + assertEq( + graphTallyCollector.tokensCollected( + collectTestParams[i].collector, + bytes32(uint256(uint160(collectTestParams[i].allocationId))), + collectTestParams[i].indexer, + collectTestParams[i].payer + ), + collectTestParams[i].tokens, + "Incorrect tokens collected for allocation" + ); + + // Try to collect again with the same allocation - should revert + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, collectTestParams[i]); + vm.expectRevert( + abi.encodeWithSelector( + IGraphTallyCollector.GraphTallyCollectorInconsistentRAVTokens.selector, + tokens, + tokens + ) + ); + graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + // Increase tokens for allocation 0 by 1000 ether and collect again + resetPrank(users.gateway); + _depositTokens(address(graphTallyCollector), users.indexer, 1000 ether); + + resetPrank(users.verifier); + collectTestParams[0].tokens = tokens + 1000 ether; + bytes memory allocation0Data = _getQueryFeeEncodedData(signerPrivateKey, collectTestParams[0]); + _collect(IGraphPayments.PaymentTypes.QueryFee, allocation0Data); + } +} diff --git a/packages/horizon/test/payments/graph-tally-collector/signer/authorizeSigner.t.sol b/packages/horizon/test/payments/graph-tally-collector/signer/authorizeSigner.t.sol new file mode 100644 index 000000000..ecdef7004 --- /dev/null +++ b/packages/horizon/test/payments/graph-tally-collector/signer/authorizeSigner.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; + +import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; + +contract GraphTallyAuthorizeSignerTest is GraphTallyTest { + uint256 constant SECP256K1_CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; + + /* + * TESTS + */ + + function testGraphTally_AuthorizeSigner(uint256 signerKey) public useGateway { + signerKey = bound(signerKey, 1, SECP256K1_CURVE_ORDER - 1); + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerKey); + _authorizeSigner(vm.addr(signerKey), proofDeadline, signerProof); + } + + function testGraphTally_AuthorizeSigner_RevertWhen_Invalid() public useGateway { + // Sign proof with payer + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + + // Attempt to authorize delegator with payer's proof + vm.expectRevert(IAuthorizable.AuthorizableInvalidSignerProof.selector); + graphTallyCollector.authorizeSigner(users.delegator, proofDeadline, signerProof); + } + + function testGraphTally_AuthorizeSigner_RevertWhen_AlreadyAuthroized() public useGateway { + // Authorize signer + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + _authorizeSigner(signer, proofDeadline, signerProof); + + // Attempt to authorize signer again + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerAlreadyAuthorized.selector, + users.gateway, + signer, + false + ); + vm.expectRevert(expectedError); + graphTallyCollector.authorizeSigner(signer, proofDeadline, signerProof); + } + + function testGraphTally_AuthorizeSigner_RevertWhen_AlreadyAuthroizedAfterRevoking() public useGateway { + // Authorize signer + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + _authorizeSigner(signer, proofDeadline, signerProof); + // Revoke signer + _thawSigner(signer); + skip(revokeSignerThawingPeriod + 1); + _revokeAuthorizedSigner(signer); + + // Attempt to authorize signer again + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerAlreadyAuthorized.selector, + users.gateway, + signer, + true + ); + vm.expectRevert(expectedError); + graphTallyCollector.authorizeSigner(signer, proofDeadline, signerProof); + } + + function testGraphTally_AuthorizeSigner_RevertWhen_ProofExpired() public useGateway { + // Sign proof with payer + uint256 proofDeadline = block.timestamp - 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + + // Attempt to authorize delegator with expired proof + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableInvalidSignerProofDeadline.selector, + proofDeadline, + block.timestamp + ); + vm.expectRevert(expectedError); + graphTallyCollector.authorizeSigner(users.delegator, proofDeadline, signerProof); + } +} diff --git a/packages/horizon/test/payments/graph-tally-collector/signer/cancelThawSigner.t.sol b/packages/horizon/test/payments/graph-tally-collector/signer/cancelThawSigner.t.sol new file mode 100644 index 000000000..460f47d46 --- /dev/null +++ b/packages/horizon/test/payments/graph-tally-collector/signer/cancelThawSigner.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; + +import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; + +contract GraphTallyCancelThawSignerTest is GraphTallyTest { + /* + * TESTS + */ + + function testGraphTally_CancelThawSigner() public useGateway useSigner { + _thawSigner(signer); + _cancelThawSigner(signer); + } + + function testGraphTally_CancelThawSigner_RevertWhen_NotAuthorized() public useGateway { + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + users.gateway, + signer + ); + vm.expectRevert(expectedError); + graphTallyCollector.thawSigner(signer); + } + + function testGraphTally_CancelThawSigner_RevertWhen_NotThawing() public useGateway useSigner { + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotThawing.selector, + signer + ); + vm.expectRevert(expectedError); + graphTallyCollector.cancelThawSigner(signer); + } +} diff --git a/packages/horizon/test/payments/graph-tally-collector/signer/revokeSigner.t.sol b/packages/horizon/test/payments/graph-tally-collector/signer/revokeSigner.t.sol new file mode 100644 index 000000000..ec32dfce8 --- /dev/null +++ b/packages/horizon/test/payments/graph-tally-collector/signer/revokeSigner.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; + +import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; + +contract GraphTallyRevokeAuthorizedSignerTest is GraphTallyTest { + /* + * TESTS + */ + + function testGraphTally_RevokeAuthorizedSigner() public useGateway useSigner { + _thawSigner(signer); + + // Advance time to thaw signer + skip(revokeSignerThawingPeriod + 1); + + _revokeAuthorizedSigner(signer); + } + + function testGraphTally_RevokeAuthorizedSigner_RevertWhen_NotAuthorized() public useGateway { + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + users.gateway, + signer + ); + vm.expectRevert(expectedError); + graphTallyCollector.revokeAuthorizedSigner(signer); + } + + function testGraphTally_RevokeAuthorizedSigner_RevertWhen_NotThawing() public useGateway useSigner { + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotThawing.selector, + signer + ); + vm.expectRevert(expectedError); + graphTallyCollector.revokeAuthorizedSigner(signer); + } + + function testGraphTally_RevokeAuthorizedSigner_RevertWhen_StillThawing() public useGateway useSigner { + _thawSigner(signer); + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerStillThawing.selector, + block.timestamp, + block.timestamp + revokeSignerThawingPeriod + ); + vm.expectRevert(expectedError); + graphTallyCollector.revokeAuthorizedSigner(signer); + } +} diff --git a/packages/horizon/test/payments/graph-tally-collector/signer/thawSigner.t.sol b/packages/horizon/test/payments/graph-tally-collector/signer/thawSigner.t.sol new file mode 100644 index 000000000..49d5ace10 --- /dev/null +++ b/packages/horizon/test/payments/graph-tally-collector/signer/thawSigner.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; + +import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; + +contract GraphTallyThawSignerTest is GraphTallyTest { + /* + * TESTS + */ + + function testGraphTally_ThawSigner() public useGateway useSigner { + _thawSigner(signer); + } + + function testGraphTally_ThawSigner_RevertWhen_NotAuthorized() public useGateway { + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + users.gateway, + signer + ); + vm.expectRevert(expectedError); + graphTallyCollector.thawSigner(signer); + } + + function testGraphTally_ThawSigner_RevertWhen_AlreadyRevoked() public useGateway useSigner { + _thawSigner(signer); + skip(revokeSignerThawingPeriod + 1); + _revokeAuthorizedSigner(signer); + + bytes memory expectedError = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + users.gateway, + signer + ); + vm.expectRevert(expectedError); + graphTallyCollector.thawSigner(signer); + } + + function testGraphTally_ThawSigner_AlreadyThawing() public useGateway useSigner { + _thawSigner(signer); + uint256 originalThawEnd = graphTallyCollector.getThawEnd(signer); + skip(1); + + graphTallyCollector.thawSigner(signer); + uint256 currentThawEnd = graphTallyCollector.getThawEnd(signer); + vm.assertEq(originalThawEnd, block.timestamp + revokeSignerThawingPeriod - 1); + vm.assertEq(currentThawEnd, block.timestamp + revokeSignerThawingPeriod); + } +} diff --git a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol new file mode 100644 index 000000000..86817c144 --- /dev/null +++ b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol @@ -0,0 +1,2431 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { GraphBaseTest } from "../../GraphBase.t.sol"; +import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol"; +import { IHorizonStakingBase } from "../../../contracts/interfaces/internal/IHorizonStakingBase.sol"; +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol"; +import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; + +import { LinkedList } from "../../../contracts/libraries/LinkedList.sol"; +import { MathUtils } from "../../../contracts/libraries/MathUtils.sol"; +import { PPMMath } from "../../../contracts/libraries/PPMMath.sol"; +import { ExponentialRebates } from "../../../contracts/staking/libraries/ExponentialRebates.sol"; + +abstract contract HorizonStakingSharedTest is GraphBaseTest { + using LinkedList for LinkedList.List; + using PPMMath for uint256; + + event Transfer(address indexed from, address indexed to, uint tokens); + + address internal _allocationId = makeAddr("allocationId"); + bytes32 internal constant _subgraphDeploymentID = keccak256("subgraphDeploymentID"); + uint256 internal constant MAX_ALLOCATION_EPOCHS = 28; + + uint32 internal alphaNumerator = 100; + uint32 internal alphaDenominator = 100; + uint32 internal lambdaNumerator = 60; + uint32 internal lambdaDenominator = 100; + + /* + * MODIFIERS + */ + + modifier useIndexer() { + vm.startPrank(users.indexer); + _; + vm.stopPrank(); + } + + modifier useOperator() { + vm.startPrank(users.indexer); + _setOperator(subgraphDataServiceAddress, users.operator, true); + vm.startPrank(users.operator); + _; + vm.stopPrank(); + } + + modifier useStake(uint256 amount) { + vm.assume(amount > 0); + _stake(amount); + _; + } + + modifier useProvision( + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) virtual { + _useProvision(subgraphDataServiceAddress, tokens, maxVerifierCut, thawingPeriod); + _; + } + + modifier useProvisionDataService( + address dataService, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) { + _useProvision(dataService, tokens, maxVerifierCut, thawingPeriod); + _; + } + + modifier useDelegationFeeCut(IGraphPayments.PaymentTypes paymentType, uint256 cut) { + _setDelegationFeeCut(users.indexer, subgraphDataServiceAddress, paymentType, cut); + _; + } + + function _useProvision(address dataService, uint256 tokens, uint32 maxVerifierCut, uint64 thawingPeriod) internal { + // use assume instead of bound to avoid the bounding falling out of scope + vm.assume(tokens > 0); + vm.assume(tokens <= MAX_STAKING_TOKENS); + vm.assume(maxVerifierCut <= MAX_PPM); + vm.assume(thawingPeriod <= MAX_THAWING_PERIOD); + + _createProvision(users.indexer, dataService, tokens, maxVerifierCut, thawingPeriod); + } + + modifier useAllocation(uint256 tokens) { + vm.assume(tokens <= MAX_STAKING_TOKENS); + _createAllocation(users.indexer, _allocationId, _subgraphDeploymentID, tokens); + _; + } + + modifier useRebateParameters() { + _setStorage_RebateParameters(alphaNumerator, alphaDenominator, lambdaNumerator, lambdaDenominator); + _; + } + + /* + * HELPERS: these are shortcuts to perform common actions that often involve multiple contract calls + */ + function _createProvision( + address serviceProvider, + address verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) internal { + _stakeTo(serviceProvider, tokens); + _provision(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod); + } + + // This allows setting up contract state with legacy allocations + function _createAllocation( + address serviceProvider, + address allocationId, + bytes32 subgraphDeploymentID, + uint256 tokens + ) internal { + _setStorage_MaxAllocationEpochs(MAX_ALLOCATION_EPOCHS); + + IHorizonStakingExtension.Allocation memory _allocation = IHorizonStakingExtension.Allocation({ + indexer: serviceProvider, + subgraphDeploymentID: subgraphDeploymentID, + tokens: tokens, + createdAtEpoch: block.timestamp, + closedAtEpoch: 0, + collectedFees: 0, + __DEPRECATED_effectiveAllocation: 0, + accRewardsPerAllocatedToken: 0, + distributedRebates: 0 + }); + _setStorage_allocation(_allocation, allocationId, tokens); + + // delegation pool initialized + _setStorage_DelegationPool(serviceProvider, 0, uint32(PPMMath.MAX_PPM), uint32(PPMMath.MAX_PPM)); + + token.transfer(address(staking), tokens); + } + + /* + * ACTIONS: these are individual contract calls wrapped in assertion blocks to ensure they work as expected + */ + function _stake(uint256 tokens) internal { + (, address msgSender, ) = vm.readCallers(); + _stakeTo(msgSender, tokens); + } + + function _stakeTo(address serviceProvider, uint256 tokens) internal { + (, address msgSender, ) = vm.readCallers(); + + // before + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + uint256 beforeSenderBalance = token.balanceOf(msgSender); + ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + + // stakeTo + token.approve(address(staking), tokens); + vm.expectEmit(); + emit IHorizonStakingBase.HorizonStakeDeposited(serviceProvider, tokens); + staking.stakeTo(serviceProvider, tokens); + + // after + uint256 afterStakingBalance = token.balanceOf(address(staking)); + uint256 afterSenderBalance = token.balanceOf(msgSender); + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + + // assert + assertEq(afterStakingBalance, beforeStakingBalance + tokens); + assertEq(afterSenderBalance, beforeSenderBalance - tokens); + assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked + tokens); + assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned); + assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLockedUntil, + beforeServiceProvider.__DEPRECATED_tokensLockedUntil + ); + } + + function _unstake(uint256 _tokens) internal { + (, address msgSender, ) = vm.readCallers(); + + uint256 deprecatedThawingPeriod = staking.__DEPRECATED_getThawingPeriod(); + + // before + uint256 beforeSenderBalance = token.balanceOf(msgSender); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(msgSender); + + bool withdrawCalled = beforeServiceProvider.__DEPRECATED_tokensLocked != 0 && + block.number >= beforeServiceProvider.__DEPRECATED_tokensLockedUntil; + + if (deprecatedThawingPeriod != 0 && beforeServiceProvider.__DEPRECATED_tokensLocked > 0) { + deprecatedThawingPeriod = MathUtils.weightedAverageRoundingUp( + MathUtils.diffOrZero( + withdrawCalled ? 0 : beforeServiceProvider.__DEPRECATED_tokensLockedUntil, + block.number + ), + withdrawCalled ? 0 : beforeServiceProvider.__DEPRECATED_tokensLocked, + deprecatedThawingPeriod, + _tokens + ); + } + + // unstake + if (deprecatedThawingPeriod == 0) { + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.HorizonStakeWithdrawn(msgSender, _tokens); + } else { + if (withdrawCalled) { + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.HorizonStakeWithdrawn( + msgSender, + beforeServiceProvider.__DEPRECATED_tokensLocked + ); + } + + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.HorizonStakeLocked( + msgSender, + withdrawCalled ? _tokens : beforeServiceProvider.__DEPRECATED_tokensLocked + _tokens, + block.number + deprecatedThawingPeriod + ); + } + staking.unstake(_tokens); + + // after + uint256 afterSenderBalance = token.balanceOf(msgSender); + uint256 afterStakingBalance = token.balanceOf(address(staking)); + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(msgSender); + + // assert + if (deprecatedThawingPeriod == 0) { + assertEq(afterSenderBalance, _tokens + beforeSenderBalance); + assertEq(afterStakingBalance, beforeStakingBalance - _tokens); + assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked - _tokens); + assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned); + assertEq( + afterServiceProvider.__DEPRECATED_tokensAllocated, + beforeServiceProvider.__DEPRECATED_tokensAllocated + ); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLockedUntil, + beforeServiceProvider.__DEPRECATED_tokensLockedUntil + ); + } else { + assertEq( + afterServiceProvider.tokensStaked, + withdrawCalled + ? beforeServiceProvider.tokensStaked - beforeServiceProvider.__DEPRECATED_tokensLocked + : beforeServiceProvider.tokensStaked + ); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLocked, + _tokens + (withdrawCalled ? 0 : beforeServiceProvider.__DEPRECATED_tokensLocked) + ); + assertEq(afterServiceProvider.__DEPRECATED_tokensLockedUntil, block.number + deprecatedThawingPeriod); + assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned); + assertEq( + afterServiceProvider.__DEPRECATED_tokensAllocated, + beforeServiceProvider.__DEPRECATED_tokensAllocated + ); + uint256 tokensTransferred = (withdrawCalled ? beforeServiceProvider.__DEPRECATED_tokensLocked : 0); + assertEq(afterSenderBalance, beforeSenderBalance + tokensTransferred); + assertEq(afterStakingBalance, beforeStakingBalance - tokensTransferred); + } + } + + function _withdraw() internal { + (, address msgSender, ) = vm.readCallers(); + + // before + ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(msgSender); + uint256 beforeSenderBalance = token.balanceOf(msgSender); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + + // withdraw + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.HorizonStakeWithdrawn(msgSender, beforeServiceProvider.__DEPRECATED_tokensLocked); + staking.withdraw(); + + // after + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(msgSender); + uint256 afterSenderBalance = token.balanceOf(msgSender); + uint256 afterStakingBalance = token.balanceOf(address(staking)); + + // assert + assertEq(afterSenderBalance - beforeSenderBalance, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq(beforeStakingBalance - afterStakingBalance, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq( + afterServiceProvider.tokensStaked, + beforeServiceProvider.tokensStaked - beforeServiceProvider.__DEPRECATED_tokensLocked + ); + assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned); + assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, 0); + assertEq(afterServiceProvider.__DEPRECATED_tokensLockedUntil, 0); + } + + function _provision( + address serviceProvider, + address verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) internal { + __provision(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod, false); + } + + function _provisionLocked( + address serviceProvider, + address verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) internal { + __provision(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod, true); + } + + function __provision( + address serviceProvider, + address verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod, + bool locked + ) private { + // before + ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + + // provision + vm.expectEmit(); + emit IHorizonStakingMain.ProvisionCreated(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod); + if (locked) { + staking.provisionLocked(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod); + } else { + staking.provision(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod); + } + + // after + Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + + // assert + assertEq(afterProvision.tokens, tokens); + assertEq(afterProvision.tokensThawing, 0); + assertEq(afterProvision.sharesThawing, 0); + assertEq(afterProvision.maxVerifierCut, maxVerifierCut); + assertEq(afterProvision.thawingPeriod, thawingPeriod); + assertEq(afterProvision.createdAt, uint64(block.timestamp)); + assertEq(afterProvision.maxVerifierCutPending, maxVerifierCut); + assertEq(afterProvision.thawingPeriodPending, thawingPeriod); + assertEq(afterProvision.thawingNonce, 0); + assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked); + assertEq(afterServiceProvider.tokensProvisioned, tokens + beforeServiceProvider.tokensProvisioned); + assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLockedUntil, + beforeServiceProvider.__DEPRECATED_tokensLockedUntil + ); + } + + function _addToProvision(address serviceProvider, address verifier, uint256 tokens) internal { + // before + Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier); + ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + + // addToProvision + vm.expectEmit(); + emit IHorizonStakingMain.ProvisionIncreased(serviceProvider, verifier, tokens); + staking.addToProvision(serviceProvider, verifier, tokens); + + // after + Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + + // assert + assertEq(afterProvision.tokens, beforeProvision.tokens + tokens); + assertEq(afterProvision.tokensThawing, beforeProvision.tokensThawing); + assertEq(afterProvision.sharesThawing, beforeProvision.sharesThawing); + assertEq(afterProvision.maxVerifierCut, beforeProvision.maxVerifierCut); + assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriod); + assertEq(afterProvision.createdAt, beforeProvision.createdAt); + assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending); + assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending); + assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce); + assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked); + assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned + tokens); + assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLockedUntil, + beforeServiceProvider.__DEPRECATED_tokensLockedUntil + ); + } + + function _thaw(address serviceProvider, address verifier, uint256 tokens) internal returns (bytes32) { + // before + Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier); + LinkedList.List memory beforeThawRequestList = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider + ); + + bytes32 expectedThawRequestId = keccak256( + abi.encodePacked(users.indexer, verifier, users.indexer, beforeThawRequestList.nonce) + ); + uint256 thawingShares = beforeProvision.tokensThawing == 0 + ? tokens + : (beforeProvision.sharesThawing * tokens) / beforeProvision.tokensThawing; + + // thaw + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ThawRequestCreated( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider, + thawingShares, + uint64(block.timestamp + beforeProvision.thawingPeriod), + expectedThawRequestId + ); + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ProvisionThawed(serviceProvider, verifier, tokens); + bytes32 thawRequestId = staking.thaw(serviceProvider, verifier, tokens); + + // after + Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); + ThawRequest memory afterThawRequest = staking.getThawRequest( + IHorizonStakingTypes.ThawRequestType.Provision, + thawRequestId + ); + LinkedList.List memory afterThawRequestList = _getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider + ); + ThawRequest memory afterPreviousTailThawRequest = staking.getThawRequest( + IHorizonStakingTypes.ThawRequestType.Provision, + beforeThawRequestList.tail + ); + + // assert + assertEq(afterProvision.tokens, beforeProvision.tokens); + assertEq(afterProvision.tokensThawing, beforeProvision.tokensThawing + tokens); + assertEq( + afterProvision.sharesThawing, + beforeProvision.tokensThawing == 0 ? thawingShares : beforeProvision.sharesThawing + thawingShares + ); + assertEq(afterProvision.maxVerifierCut, beforeProvision.maxVerifierCut); + assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriod); + assertEq(afterProvision.createdAt, beforeProvision.createdAt); + assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending); + assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending); + assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce); + assertEq(thawRequestId, expectedThawRequestId); + assertEq(afterThawRequest.shares, thawingShares); + assertEq(afterThawRequest.thawingUntil, block.timestamp + beforeProvision.thawingPeriod); + assertEq(afterThawRequest.next, bytes32(0)); + assertEq( + afterThawRequestList.head, + beforeThawRequestList.count == 0 ? thawRequestId : beforeThawRequestList.head + ); + assertEq(afterThawRequestList.tail, thawRequestId); + assertEq(afterThawRequestList.count, beforeThawRequestList.count + 1); + assertEq(afterThawRequestList.nonce, beforeThawRequestList.nonce + 1); + if (beforeThawRequestList.count != 0) { + assertEq(afterPreviousTailThawRequest.next, thawRequestId); + } + + return thawRequestId; + } + + function _deprovision(address serviceProvider, address verifier, uint256 nThawRequests) internal { + // before + Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier); + ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + LinkedList.List memory beforeThawRequestList = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider + ); + + Params_CalcThawRequestData memory params = Params_CalcThawRequestData({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider: serviceProvider, + verifier: verifier, + owner: serviceProvider, + iterations: nThawRequests, + delegation: false + }); + CalcValues_ThawRequestData memory calcValues = calcThawRequestData(params); + + // deprovision + for (uint i = 0; i < calcValues.thawRequestsFulfilledList.length; i++) { + ThawRequest memory thawRequest = calcValues.thawRequestsFulfilledList[i]; + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ThawRequestFulfilled( + params.thawRequestType, + calcValues.thawRequestsFulfilledListIds[i], + calcValues.thawRequestsFulfilledListTokens[i], + thawRequest.shares, + thawRequest.thawingUntil, + beforeProvision.thawingNonce == thawRequest.thawingNonce + ); + } + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ThawRequestsFulfilled( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider, + calcValues.thawRequestsFulfilledList.length, + calcValues.tokensThawed + ); + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.TokensDeprovisioned(serviceProvider, verifier, calcValues.tokensThawed); + staking.deprovision(serviceProvider, verifier, nThawRequests); + + // after + Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + LinkedList.List memory afterThawRequestList = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider + ); + + // assert + assertEq(afterProvision.tokens, beforeProvision.tokens - calcValues.tokensThawed); + assertEq(afterProvision.tokensThawing, calcValues.tokensThawing); + assertEq(afterProvision.sharesThawing, calcValues.sharesThawing); + assertEq(afterProvision.maxVerifierCut, beforeProvision.maxVerifierCut); + assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriod); + assertEq(afterProvision.createdAt, beforeProvision.createdAt); + assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending); + assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending); + assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce); + assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked); + assertEq( + afterServiceProvider.tokensProvisioned, + beforeServiceProvider.tokensProvisioned - calcValues.tokensThawed + ); + assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLockedUntil, + beforeServiceProvider.__DEPRECATED_tokensLockedUntil + ); + for (uint i = 0; i < calcValues.thawRequestsFulfilledListIds.length; i++) { + ThawRequest memory thawRequest = staking.getThawRequest( + IHorizonStakingTypes.ThawRequestType.Provision, + calcValues.thawRequestsFulfilledListIds[i] + ); + assertEq(thawRequest.shares, 0); + assertEq(thawRequest.thawingUntil, 0); + assertEq(thawRequest.next, bytes32(0)); + } + if (calcValues.thawRequestsFulfilledList.length == 0) { + assertEq(afterThawRequestList.head, beforeThawRequestList.head); + } else { + assertEq( + afterThawRequestList.head, + calcValues.thawRequestsFulfilledList.length == beforeThawRequestList.count + ? bytes32(0) + : calcValues.thawRequestsFulfilledList[calcValues.thawRequestsFulfilledList.length - 1].next + ); + } + assertEq( + afterThawRequestList.tail, + calcValues.thawRequestsFulfilledList.length == beforeThawRequestList.count + ? bytes32(0) + : beforeThawRequestList.tail + ); + assertEq(afterThawRequestList.count, beforeThawRequestList.count - calcValues.thawRequestsFulfilledList.length); + assertEq(afterThawRequestList.nonce, beforeThawRequestList.nonce); + } + + struct BeforeValues_Reprovision { + Provision provision; + Provision provisionNewVerifier; + ServiceProviderInternal serviceProvider; + LinkedList.List thawRequestList; + } + + function _reprovision( + address serviceProvider, + address verifier, + address newVerifier, + uint256 nThawRequests + ) internal { + // before + BeforeValues_Reprovision memory beforeValues = BeforeValues_Reprovision({ + provision: staking.getProvision(serviceProvider, verifier), + provisionNewVerifier: staking.getProvision(serviceProvider, newVerifier), + serviceProvider: _getStorage_ServiceProviderInternal(serviceProvider), + thawRequestList: staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider + ) + }); + + // calc + Params_CalcThawRequestData memory params = Params_CalcThawRequestData({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider: serviceProvider, + verifier: verifier, + owner: serviceProvider, + iterations: nThawRequests, + delegation: false + }); + CalcValues_ThawRequestData memory calcValues = calcThawRequestData(params); + + // reprovision + for (uint i = 0; i < calcValues.thawRequestsFulfilledList.length; i++) { + ThawRequest memory thawRequest = calcValues.thawRequestsFulfilledList[i]; + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ThawRequestFulfilled( + params.thawRequestType, + calcValues.thawRequestsFulfilledListIds[i], + calcValues.thawRequestsFulfilledListTokens[i], + thawRequest.shares, + thawRequest.thawingUntil, + beforeValues.provision.thawingNonce == thawRequest.thawingNonce + ); + } + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ThawRequestsFulfilled( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider, + calcValues.thawRequestsFulfilledList.length, + calcValues.tokensThawed + ); + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.TokensDeprovisioned(serviceProvider, verifier, calcValues.tokensThawed); + vm.expectEmit(); + emit IHorizonStakingMain.ProvisionIncreased(serviceProvider, newVerifier, calcValues.tokensThawed); + staking.reprovision(serviceProvider, verifier, newVerifier, nThawRequests); + + // after + Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); + Provision memory afterProvisionNewVerifier = staking.getProvision(serviceProvider, newVerifier); + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + LinkedList.List memory afterThawRequestList = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider, + verifier, + serviceProvider + ); + + // assert: provision old verifier + assertEq(afterProvision.tokens, beforeValues.provision.tokens - calcValues.tokensThawed); + assertEq(afterProvision.tokensThawing, calcValues.tokensThawing); + assertEq(afterProvision.sharesThawing, calcValues.sharesThawing); + assertEq(afterProvision.maxVerifierCut, beforeValues.provision.maxVerifierCut); + assertEq(afterProvision.thawingPeriod, beforeValues.provision.thawingPeriod); + assertEq(afterProvision.createdAt, beforeValues.provision.createdAt); + assertEq(afterProvision.maxVerifierCutPending, beforeValues.provision.maxVerifierCutPending); + assertEq(afterProvision.thawingPeriodPending, beforeValues.provision.thawingPeriodPending); + assertEq(afterProvision.thawingNonce, beforeValues.provision.thawingNonce); + + // assert: provision new verifier + assertEq(afterProvisionNewVerifier.tokens, beforeValues.provisionNewVerifier.tokens + calcValues.tokensThawed); + assertEq(afterProvisionNewVerifier.tokensThawing, beforeValues.provisionNewVerifier.tokensThawing); + assertEq(afterProvisionNewVerifier.sharesThawing, beforeValues.provisionNewVerifier.sharesThawing); + assertEq(afterProvisionNewVerifier.maxVerifierCut, beforeValues.provisionNewVerifier.maxVerifierCut); + assertEq(afterProvisionNewVerifier.thawingPeriod, beforeValues.provisionNewVerifier.thawingPeriod); + assertEq(afterProvisionNewVerifier.createdAt, beforeValues.provisionNewVerifier.createdAt); + assertEq( + afterProvisionNewVerifier.maxVerifierCutPending, + beforeValues.provisionNewVerifier.maxVerifierCutPending + ); + assertEq( + afterProvisionNewVerifier.thawingPeriodPending, + beforeValues.provisionNewVerifier.thawingPeriodPending + ); + assertEq(afterProvisionNewVerifier.thawingNonce, beforeValues.provisionNewVerifier.thawingNonce); + + // assert: service provider + assertEq(afterServiceProvider.tokensStaked, beforeValues.serviceProvider.tokensStaked); + assertEq( + afterServiceProvider.tokensProvisioned, + beforeValues.serviceProvider.tokensProvisioned + calcValues.tokensThawed - calcValues.tokensThawed + ); + assertEq( + afterServiceProvider.__DEPRECATED_tokensAllocated, + beforeValues.serviceProvider.__DEPRECATED_tokensAllocated + ); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLocked, + beforeValues.serviceProvider.__DEPRECATED_tokensLocked + ); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLockedUntil, + beforeValues.serviceProvider.__DEPRECATED_tokensLockedUntil + ); + + // assert: thaw request list old verifier + for (uint i = 0; i < calcValues.thawRequestsFulfilledListIds.length; i++) { + ThawRequest memory thawRequest = staking.getThawRequest( + IHorizonStakingTypes.ThawRequestType.Provision, + calcValues.thawRequestsFulfilledListIds[i] + ); + assertEq(thawRequest.shares, 0); + assertEq(thawRequest.thawingUntil, 0); + assertEq(thawRequest.next, bytes32(0)); + } + if (calcValues.thawRequestsFulfilledList.length == 0) { + assertEq(afterThawRequestList.head, beforeValues.thawRequestList.head); + } else { + assertEq( + afterThawRequestList.head, + calcValues.thawRequestsFulfilledList.length == beforeValues.thawRequestList.count + ? bytes32(0) + : calcValues.thawRequestsFulfilledList[calcValues.thawRequestsFulfilledList.length - 1].next + ); + } + assertEq( + afterThawRequestList.tail, + calcValues.thawRequestsFulfilledList.length == beforeValues.thawRequestList.count + ? bytes32(0) + : beforeValues.thawRequestList.tail + ); + assertEq( + afterThawRequestList.count, + beforeValues.thawRequestList.count - calcValues.thawRequestsFulfilledList.length + ); + assertEq(afterThawRequestList.nonce, beforeValues.thawRequestList.nonce); + } + + function _setProvisionParameters( + address serviceProvider, + address verifier, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) internal { + // before + Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier); + + // setProvisionParameters + if (beforeProvision.maxVerifierCut != maxVerifierCut || beforeProvision.thawingPeriod != thawingPeriod) { + vm.expectEmit(); + emit IHorizonStakingMain.ProvisionParametersStaged( + serviceProvider, + verifier, + maxVerifierCut, + thawingPeriod + ); + } + staking.setProvisionParameters(serviceProvider, verifier, maxVerifierCut, thawingPeriod); + + // after + Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); + + // assert + assertEq(afterProvision.tokens, beforeProvision.tokens); + assertEq(afterProvision.tokensThawing, beforeProvision.tokensThawing); + assertEq(afterProvision.sharesThawing, beforeProvision.sharesThawing); + assertEq(afterProvision.maxVerifierCut, beforeProvision.maxVerifierCut); + assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriod); + assertEq(afterProvision.createdAt, beforeProvision.createdAt); + assertEq(afterProvision.maxVerifierCutPending, maxVerifierCut); + assertEq(afterProvision.thawingPeriodPending, thawingPeriod); + assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce); + } + + function _acceptProvisionParameters(address serviceProvider) internal { + (, address msgSender, ) = vm.readCallers(); + + // before + Provision memory beforeProvision = staking.getProvision(serviceProvider, msgSender); + + // acceptProvisionParameters + if ( + beforeProvision.maxVerifierCutPending != beforeProvision.maxVerifierCut || + beforeProvision.thawingPeriodPending != beforeProvision.thawingPeriod + ) { + vm.expectEmit(); + emit IHorizonStakingMain.ProvisionParametersSet( + serviceProvider, + msgSender, + beforeProvision.maxVerifierCutPending, + beforeProvision.thawingPeriodPending + ); + } + staking.acceptProvisionParameters(serviceProvider); + + // after + Provision memory afterProvision = staking.getProvision(serviceProvider, msgSender); + + // assert + assertEq(afterProvision.tokens, beforeProvision.tokens); + assertEq(afterProvision.tokensThawing, beforeProvision.tokensThawing); + assertEq(afterProvision.sharesThawing, beforeProvision.sharesThawing); + assertEq(afterProvision.maxVerifierCut, beforeProvision.maxVerifierCutPending); + assertEq(afterProvision.maxVerifierCut, afterProvision.maxVerifierCutPending); + assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriodPending); + assertEq(afterProvision.thawingPeriod, afterProvision.thawingPeriodPending); + assertEq(afterProvision.createdAt, beforeProvision.createdAt); + assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce); + } + + function _setOperator(address verifier, address operator, bool allow) internal { + __setOperator(verifier, operator, allow, false); + } + + function _setOperatorLocked(address verifier, address operator, bool allow) internal { + __setOperator(verifier, operator, allow, true); + } + + function __setOperator(address verifier, address operator, bool allow, bool locked) private { + (, address msgSender, ) = vm.readCallers(); + + // staking contract knows the address of the legacy subgraph service + // but we cannot read it as it's an immutable, we have to use the global var :/ + bool legacy = verifier == subgraphDataServiceLegacyAddress; + + // before + bool beforeOperatorAllowed = _getStorage_OperatorAuth(msgSender, verifier, operator, legacy); + bool beforeOperatorAllowedGetter = staking.isAuthorized(msgSender, verifier, operator); + assertEq(beforeOperatorAllowed, beforeOperatorAllowedGetter); + + // setOperator + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.OperatorSet(msgSender, verifier, operator, allow); + if (locked) { + staking.setOperatorLocked(verifier, operator, allow); + } else { + staking.setOperator(verifier, operator, allow); + } + + // after + bool afterOperatorAllowed = _getStorage_OperatorAuth(msgSender, verifier, operator, legacy); + bool afterOperatorAllowedGetter = staking.isAuthorized(msgSender, verifier, operator); + assertEq(afterOperatorAllowed, afterOperatorAllowedGetter, "afterOperatorAllowedGetter FAIL"); + + // assert + assertEq(afterOperatorAllowed, allow); + } + + function _delegate(address serviceProvider, address verifier, uint256 tokens, uint256 minSharesOut) internal { + __delegate(serviceProvider, verifier, tokens, minSharesOut, false); + } + + function _delegate(address serviceProvider, uint256 tokens) internal { + __delegate(serviceProvider, subgraphDataServiceLegacyAddress, tokens, 0, true); + } + + function __delegate( + address serviceProvider, + address verifier, + uint256 tokens, + uint256 minSharesOut, + bool legacy + ) private { + (, address delegator, ) = vm.readCallers(); + + // before + DelegationPoolInternalTest memory beforePool = _getStorage_DelegationPoolInternal( + serviceProvider, + verifier, + legacy + ); + DelegationInternal memory beforeDelegation = _getStorage_Delegation( + serviceProvider, + verifier, + delegator, + legacy + ); + uint256 beforeDelegatorBalance = token.balanceOf(delegator); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + + uint256 calcShares = (beforePool.tokens == 0 || beforePool.tokens == beforePool.tokensThawing) + ? tokens + : ((tokens * beforePool.shares) / (beforePool.tokens - beforePool.tokensThawing)); + + // delegate + token.approve(address(staking), tokens); + vm.expectEmit(); + emit IHorizonStakingMain.TokensDelegated(serviceProvider, verifier, delegator, tokens, calcShares); + if (legacy) { + staking.delegate(serviceProvider, tokens); + } else { + staking.delegate(serviceProvider, verifier, tokens, minSharesOut); + } + + // after + DelegationPoolInternalTest memory afterPool = _getStorage_DelegationPoolInternal( + serviceProvider, + verifier, + legacy + ); + DelegationInternal memory afterDelegation = _getStorage_Delegation( + serviceProvider, + verifier, + delegator, + legacy + ); + uint256 afterDelegatorBalance = token.balanceOf(delegator); + uint256 afterStakingBalance = token.balanceOf(address(staking)); + + uint256 deltaShares = afterDelegation.shares - beforeDelegation.shares; + + // assertions + assertEq(beforePool.tokens + tokens, afterPool.tokens, "afterPool.tokens FAIL"); + assertEq(beforePool.shares + calcShares, afterPool.shares, "afterPool.shares FAIL"); + assertEq(beforePool.tokensThawing, afterPool.tokensThawing); + assertEq(beforePool.sharesThawing, afterPool.sharesThawing); + assertEq(beforePool.thawingNonce, afterPool.thawingNonce); + assertEq(beforeDelegation.shares + calcShares, afterDelegation.shares); + assertEq(beforeDelegation.__DEPRECATED_tokensLocked, afterDelegation.__DEPRECATED_tokensLocked); + assertEq(beforeDelegation.__DEPRECATED_tokensLockedUntil, afterDelegation.__DEPRECATED_tokensLockedUntil); + assertGe(deltaShares, minSharesOut); + assertEq(calcShares, deltaShares); + assertEq(beforeDelegatorBalance - tokens, afterDelegatorBalance); + assertEq(beforeStakingBalance + tokens, afterStakingBalance); + } + + function _undelegate(address serviceProvider, address verifier, uint256 shares) internal { + (, address caller, ) = vm.readCallers(); + __undelegate(IHorizonStakingTypes.ThawRequestType.Delegation, serviceProvider, verifier, shares, false, caller); + } + + function _undelegate(address serviceProvider, uint256 shares) internal { + (, address caller, ) = vm.readCallers(); + __undelegate( + IHorizonStakingTypes.ThawRequestType.Delegation, + serviceProvider, + subgraphDataServiceLegacyAddress, + shares, + true, + caller + ); + } + + struct BeforeValues_Undelegate { + DelegationPoolInternalTest pool; + DelegationInternal delegation; + LinkedList.List thawRequestList; + uint256 delegatedTokens; + } + struct CalcValues_Undelegate { + uint256 tokens; + uint256 thawingShares; + uint64 thawingUntil; + bytes32 thawRequestId; + } + + function __undelegate( + IHorizonStakingTypes.ThawRequestType thawRequestType, + address serviceProvider, + address verifier, + uint256 shares, + bool legacy, + address beneficiary + ) private { + (, address delegator, ) = vm.readCallers(); + + // before + BeforeValues_Undelegate memory beforeValues; + beforeValues.pool = _getStorage_DelegationPoolInternal(serviceProvider, verifier, legacy); + beforeValues.delegation = _getStorage_Delegation(serviceProvider, verifier, delegator, legacy); + beforeValues.thawRequestList = staking.getThawRequestList( + thawRequestType, + serviceProvider, + verifier, + delegator + ); + beforeValues.delegatedTokens = staking.getDelegatedTokensAvailable(serviceProvider, verifier); + + // calc + CalcValues_Undelegate memory calcValues; + calcValues.tokens = + ((beforeValues.pool.tokens - beforeValues.pool.tokensThawing) * shares) / + beforeValues.pool.shares; + calcValues.thawingShares = beforeValues.pool.tokensThawing == 0 + ? calcValues.tokens + : (beforeValues.pool.sharesThawing * calcValues.tokens) / beforeValues.pool.tokensThawing; + calcValues.thawingUntil = + staking.getProvision(serviceProvider, verifier).thawingPeriod + + uint64(block.timestamp); + calcValues.thawRequestId = keccak256( + abi.encodePacked(serviceProvider, verifier, beneficiary, beforeValues.thawRequestList.nonce) + ); + + // undelegate + vm.expectEmit(); + emit IHorizonStakingMain.ThawRequestCreated( + thawRequestType, + serviceProvider, + verifier, + beneficiary, + calcValues.thawingShares, + calcValues.thawingUntil, + calcValues.thawRequestId + ); + vm.expectEmit(); + emit IHorizonStakingMain.TokensUndelegated(serviceProvider, verifier, delegator, calcValues.tokens); + if (legacy) { + staking.undelegate(serviceProvider, shares); + } else if (thawRequestType == IHorizonStakingTypes.ThawRequestType.Delegation) { + staking.undelegate(serviceProvider, verifier, shares); + } else { + revert("Invalid thaw request type"); + } + + // after + DelegationPoolInternalTest memory afterPool = _getStorage_DelegationPoolInternal( + users.indexer, + verifier, + legacy + ); + DelegationInternal memory afterDelegation = _getStorage_Delegation( + serviceProvider, + verifier, + beneficiary, + legacy + ); + LinkedList.List memory afterThawRequestList = staking.getThawRequestList( + thawRequestType, + serviceProvider, + verifier, + beneficiary + ); + ThawRequest memory afterThawRequest = staking.getThawRequest(thawRequestType, calcValues.thawRequestId); + uint256 afterDelegatedTokens = staking.getDelegatedTokensAvailable(serviceProvider, verifier); + + // assertions + assertEq(beforeValues.pool.shares, afterPool.shares + shares); + assertEq(beforeValues.pool.tokens, afterPool.tokens); + assertEq(beforeValues.pool.tokensThawing + calcValues.tokens, afterPool.tokensThawing); + assertEq( + beforeValues.pool.tokensThawing == 0 + ? calcValues.thawingShares + : beforeValues.pool.sharesThawing + calcValues.thawingShares, + afterPool.sharesThawing + ); + assertEq(beforeValues.pool.thawingNonce, afterPool.thawingNonce); + assertEq(beforeValues.delegation.shares - shares, afterDelegation.shares); + assertEq(afterThawRequest.shares, calcValues.thawingShares); + assertEq(afterThawRequest.thawingUntil, calcValues.thawingUntil); + assertEq(afterThawRequest.next, bytes32(0)); + assertEq(calcValues.thawRequestId, afterThawRequestList.tail); + assertEq(beforeValues.thawRequestList.nonce + 1, afterThawRequestList.nonce); + assertEq(beforeValues.thawRequestList.count + 1, afterThawRequestList.count); + assertEq(afterDelegatedTokens + calcValues.tokens, beforeValues.delegatedTokens); + } + + function _withdrawDelegated(address serviceProvider, address verifier, uint256 nThawRequests) internal { + Params_WithdrawDelegated memory params = Params_WithdrawDelegated({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.Delegation, + serviceProvider: serviceProvider, + verifier: verifier, + newServiceProvider: address(0), + newVerifier: address(0), + minSharesForNewProvider: 0, + nThawRequests: nThawRequests, + legacy: verifier == subgraphDataServiceLegacyAddress + }); + __withdrawDelegated(params); + } + + function _redelegate( + address serviceProvider, + address verifier, + address newServiceProvider, + address newVerifier, + uint256 minSharesForNewProvider, + uint256 nThawRequests + ) internal { + Params_WithdrawDelegated memory params = Params_WithdrawDelegated({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.Delegation, + serviceProvider: serviceProvider, + verifier: verifier, + newServiceProvider: newServiceProvider, + newVerifier: newVerifier, + minSharesForNewProvider: minSharesForNewProvider, + nThawRequests: nThawRequests, + legacy: false + }); + __withdrawDelegated(params); + } + + struct BeforeValues_WithdrawDelegated { + DelegationPoolInternalTest pool; + DelegationPoolInternalTest newPool; + DelegationInternal newDelegation; + LinkedList.List thawRequestList; + uint256 senderBalance; + uint256 stakingBalance; + } + struct AfterValues_WithdrawDelegated { + DelegationPoolInternalTest pool; + DelegationPoolInternalTest newPool; + DelegationInternal newDelegation; + LinkedList.List thawRequestList; + uint256 senderBalance; + uint256 stakingBalance; + } + + struct Params_WithdrawDelegated { + IHorizonStakingTypes.ThawRequestType thawRequestType; + address serviceProvider; + address verifier; + address newServiceProvider; + address newVerifier; + uint256 minSharesForNewProvider; + uint256 nThawRequests; + bool legacy; + } + + function __withdrawDelegated(Params_WithdrawDelegated memory params) private { + (, address msgSender, ) = vm.readCallers(); + + bool reDelegate = params.newServiceProvider != address(0) && params.newVerifier != address(0); + + // before + BeforeValues_WithdrawDelegated memory beforeValues; + beforeValues.pool = _getStorage_DelegationPoolInternal(params.serviceProvider, params.verifier, params.legacy); + beforeValues.newPool = _getStorage_DelegationPoolInternal( + params.newServiceProvider, + params.newVerifier, + params.legacy + ); + beforeValues.newDelegation = _getStorage_Delegation( + params.newServiceProvider, + params.newVerifier, + msgSender, + params.legacy + ); + beforeValues.thawRequestList = staking.getThawRequestList( + params.thawRequestType, + params.serviceProvider, + params.verifier, + msgSender + ); + beforeValues.senderBalance = token.balanceOf(msgSender); + beforeValues.stakingBalance = token.balanceOf(address(staking)); + + Params_CalcThawRequestData memory paramsCalc = Params_CalcThawRequestData({ + thawRequestType: params.thawRequestType, + serviceProvider: params.serviceProvider, + verifier: params.verifier, + owner: msgSender, + iterations: params.nThawRequests, + delegation: true + }); + CalcValues_ThawRequestData memory calcValues = calcThawRequestData(paramsCalc); + + // withdrawDelegated + for (uint i = 0; i < calcValues.thawRequestsFulfilledList.length; i++) { + ThawRequest memory thawRequest = calcValues.thawRequestsFulfilledList[i]; + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ThawRequestFulfilled( + params.thawRequestType, + calcValues.thawRequestsFulfilledListIds[i], + calcValues.thawRequestsFulfilledListTokens[i], + thawRequest.shares, + thawRequest.thawingUntil, + beforeValues.pool.thawingNonce == thawRequest.thawingNonce + ); + } + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ThawRequestsFulfilled( + params.thawRequestType, + params.serviceProvider, + params.verifier, + msgSender, + calcValues.thawRequestsFulfilledList.length, + calcValues.tokensThawed + ); + if (calcValues.tokensThawed != 0) { + vm.expectEmit(); + if (reDelegate) { + emit IHorizonStakingMain.TokensDelegated( + params.newServiceProvider, + params.newVerifier, + msgSender, + calcValues.tokensThawed, + calcValues.sharesThawed + ); + } else { + emit Transfer(address(staking), msgSender, calcValues.tokensThawed); + + vm.expectEmit(); + emit IHorizonStakingMain.DelegatedTokensWithdrawn( + params.serviceProvider, + params.verifier, + msgSender, + calcValues.tokensThawed + ); + } + } + + if (reDelegate) { + staking.redelegate( + params.serviceProvider, + params.verifier, + params.newServiceProvider, + params.newVerifier, + params.minSharesForNewProvider, + params.nThawRequests + ); + } else if (params.thawRequestType == IHorizonStakingTypes.ThawRequestType.Delegation) { + staking.withdrawDelegated(params.serviceProvider, params.verifier, params.nThawRequests); + } else { + revert("Invalid thaw request type"); + } + + // after + AfterValues_WithdrawDelegated memory afterValues; + afterValues.pool = _getStorage_DelegationPoolInternal(params.serviceProvider, params.verifier, params.legacy); + afterValues.newPool = _getStorage_DelegationPoolInternal( + params.newServiceProvider, + params.newVerifier, + params.legacy + ); + afterValues.newDelegation = _getStorage_Delegation( + params.newServiceProvider, + params.newVerifier, + msgSender, + params.legacy + ); + afterValues.thawRequestList = staking.getThawRequestList( + params.thawRequestType, + params.serviceProvider, + params.verifier, + msgSender + ); + afterValues.senderBalance = token.balanceOf(msgSender); + afterValues.stakingBalance = token.balanceOf(address(staking)); + + // assert + assertEq(afterValues.pool.tokens, beforeValues.pool.tokens - calcValues.tokensThawed); + assertEq(afterValues.pool.shares, beforeValues.pool.shares); + assertEq(afterValues.pool.tokensThawing, calcValues.tokensThawing); + assertEq(afterValues.pool.sharesThawing, calcValues.sharesThawing); + assertEq(afterValues.pool.thawingNonce, beforeValues.pool.thawingNonce); + + for (uint i = 0; i < calcValues.thawRequestsFulfilledListIds.length; i++) { + ThawRequest memory thawRequest = staking.getThawRequest( + params.thawRequestType, + calcValues.thawRequestsFulfilledListIds[i] + ); + assertEq(thawRequest.shares, 0); + assertEq(thawRequest.thawingUntil, 0); + assertEq(thawRequest.next, bytes32(0)); + } + if (calcValues.thawRequestsFulfilledList.length == 0) { + assertEq(afterValues.thawRequestList.head, beforeValues.thawRequestList.head); + } else { + assertEq( + afterValues.thawRequestList.head, + calcValues.thawRequestsFulfilledList.length == beforeValues.thawRequestList.count + ? bytes32(0) + : calcValues.thawRequestsFulfilledList[calcValues.thawRequestsFulfilledList.length - 1].next + ); + } + assertEq( + afterValues.thawRequestList.tail, + calcValues.thawRequestsFulfilledList.length == beforeValues.thawRequestList.count + ? bytes32(0) + : beforeValues.thawRequestList.tail + ); + assertEq( + afterValues.thawRequestList.count, + beforeValues.thawRequestList.count - calcValues.thawRequestsFulfilledList.length + ); + assertEq(afterValues.thawRequestList.nonce, beforeValues.thawRequestList.nonce); + + if (reDelegate) { + uint256 calcShares = (afterValues.newPool.tokens == 0 || + afterValues.newPool.tokens == afterValues.newPool.tokensThawing) + ? calcValues.tokensThawed + : ((calcValues.tokensThawed * afterValues.newPool.shares) / + (afterValues.newPool.tokens - afterValues.newPool.tokensThawing)); + uint256 deltaShares = afterValues.newDelegation.shares - beforeValues.newDelegation.shares; + + assertEq(afterValues.newPool.tokens, beforeValues.newPool.tokens + calcValues.tokensThawed); + assertEq(afterValues.newPool.shares, beforeValues.newPool.shares + calcShares); + assertEq(afterValues.newPool.tokensThawing, beforeValues.newPool.tokensThawing); + assertEq(afterValues.newPool.sharesThawing, beforeValues.newPool.sharesThawing); + assertEq(afterValues.newDelegation.shares, beforeValues.newDelegation.shares + calcShares); + assertEq( + afterValues.newDelegation.__DEPRECATED_tokensLocked, + beforeValues.newDelegation.__DEPRECATED_tokensLocked + ); + assertEq( + afterValues.newDelegation.__DEPRECATED_tokensLockedUntil, + beforeValues.newDelegation.__DEPRECATED_tokensLockedUntil + ); + assertGe(deltaShares, params.minSharesForNewProvider); + assertEq(calcShares, deltaShares); + assertEq(afterValues.senderBalance - beforeValues.senderBalance, 0); + assertEq(beforeValues.stakingBalance - afterValues.stakingBalance, 0); + } else { + assertEq(beforeValues.stakingBalance - afterValues.stakingBalance, calcValues.tokensThawed); + assertEq(afterValues.senderBalance - beforeValues.senderBalance, calcValues.tokensThawed); + } + } + + function _addToDelegationPool(address serviceProvider, address verifier, uint256 tokens) internal { + (, address msgSender, ) = vm.readCallers(); + + // staking contract knows the address of the legacy subgraph service + // but we cannot read it as it's an immutable, we have to use the global var :/ + bool legacy = verifier == subgraphDataServiceLegacyAddress; + + // before + DelegationPoolInternalTest memory beforePool = _getStorage_DelegationPoolInternal( + serviceProvider, + verifier, + legacy + ); + uint256 beforeSenderBalance = token.balanceOf(msgSender); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + + // addToDelegationPool + vm.expectEmit(); + emit Transfer(msgSender, address(staking), tokens); + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.TokensToDelegationPoolAdded(serviceProvider, verifier, tokens); + staking.addToDelegationPool(serviceProvider, verifier, tokens); + + // after + DelegationPoolInternalTest memory afterPool = _getStorage_DelegationPoolInternal( + serviceProvider, + verifier, + legacy + ); + uint256 afterSenderBalance = token.balanceOf(msgSender); + uint256 afterStakingBalance = token.balanceOf(address(staking)); + + // assert + assertEq(beforeSenderBalance - tokens, afterSenderBalance); + assertEq(beforeStakingBalance + tokens, afterStakingBalance); + assertEq(beforePool.tokens + tokens, afterPool.tokens); + assertEq(beforePool.shares, afterPool.shares); + assertEq(beforePool.tokensThawing, afterPool.tokensThawing); + assertEq(beforePool.sharesThawing, afterPool.sharesThawing); + assertEq(beforePool.thawingNonce, afterPool.thawingNonce); + } + + function _setDelegationFeeCut( + address serviceProvider, + address verifier, + IGraphPayments.PaymentTypes paymentType, + uint256 feeCut + ) internal { + // setDelegationFeeCut + vm.expectEmit(); + emit IHorizonStakingMain.DelegationFeeCutSet(serviceProvider, verifier, paymentType, feeCut); + staking.setDelegationFeeCut(serviceProvider, verifier, paymentType, feeCut); + + // after + uint256 afterDelegationFeeCut = staking.getDelegationFeeCut(serviceProvider, verifier, paymentType); + + // assert + assertEq(afterDelegationFeeCut, feeCut); + } + + function _setAllowedLockedVerifier(address verifier, bool allowed) internal { + // setAllowedLockedVerifier + vm.expectEmit(); + emit IHorizonStakingMain.AllowedLockedVerifierSet(verifier, allowed); + staking.setAllowedLockedVerifier(verifier, allowed); + + // after + bool afterAllowed = staking.isAllowedLockedVerifier(verifier); + + // assert + assertEq(afterAllowed, allowed); + } + + function _setDelegationSlashingEnabled() internal { + // setDelegationSlashingEnabled + vm.expectEmit(); + emit IHorizonStakingMain.DelegationSlashingEnabled(); + staking.setDelegationSlashingEnabled(); + + // after + bool afterEnabled = staking.isDelegationSlashingEnabled(); + + // assert + assertEq(afterEnabled, true); + } + + function _clearThawingPeriod() internal { + // clearThawingPeriod + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ThawingPeriodCleared(); + staking.clearThawingPeriod(); + + // after + uint64 afterThawingPeriod = staking.__DEPRECATED_getThawingPeriod(); + + // assert + assertEq(afterThawingPeriod, 0); + } + + function _setMaxThawingPeriod(uint64 maxThawingPeriod) internal { + // setMaxThawingPeriod + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.MaxThawingPeriodSet(maxThawingPeriod); + staking.setMaxThawingPeriod(maxThawingPeriod); + + // after + uint64 afterMaxThawingPeriod = staking.getMaxThawingPeriod(); + + // assert + assertEq(afterMaxThawingPeriod, maxThawingPeriod); + } + + struct BeforeValues_Slash { + Provision provision; + DelegationPoolInternalTest pool; + ServiceProviderInternal serviceProvider; + uint256 stakingBalance; + uint256 verifierBalance; + } + struct CalcValues_Slash { + uint256 tokensToSlash; + uint256 providerTokensSlashed; + uint256 delegationTokensSlashed; + } + + function _slash(address serviceProvider, address verifier, uint256 tokens, uint256 verifierCutAmount) internal { + bool isDelegationSlashingEnabled = staking.isDelegationSlashingEnabled(); + + // staking contract knows the address of the legacy subgraph service + // but we cannot read it as it's an immutable, we have to use the global var :/ + bool legacy = verifier == subgraphDataServiceLegacyAddress; + + // before + BeforeValues_Slash memory before; + before.provision = staking.getProvision(serviceProvider, verifier); + before.pool = _getStorage_DelegationPoolInternal(serviceProvider, verifier, legacy); + before.serviceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + before.stakingBalance = token.balanceOf(address(staking)); + before.verifierBalance = token.balanceOf(verifier); + + // Calculate expected tokens after slashing + CalcValues_Slash memory calcValues; + calcValues.tokensToSlash = MathUtils.min(tokens, before.provision.tokens + before.pool.tokens); + calcValues.providerTokensSlashed = MathUtils.min(before.provision.tokens, calcValues.tokensToSlash); + calcValues.delegationTokensSlashed = calcValues.tokensToSlash - calcValues.providerTokensSlashed; + + if (calcValues.tokensToSlash > 0) { + if (verifierCutAmount > 0) { + vm.expectEmit(address(token)); + emit Transfer(address(staking), verifier, verifierCutAmount); + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.VerifierTokensSent(serviceProvider, verifier, verifier, verifierCutAmount); + } + if (calcValues.providerTokensSlashed - verifierCutAmount > 0) { + vm.expectEmit(address(token)); + emit Transfer(address(staking), address(0), calcValues.providerTokensSlashed - verifierCutAmount); + } + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.ProvisionSlashed(serviceProvider, verifier, calcValues.providerTokensSlashed); + } + + if (calcValues.delegationTokensSlashed > 0) { + if (isDelegationSlashingEnabled) { + vm.expectEmit(address(token)); + emit Transfer(address(staking), address(0), calcValues.delegationTokensSlashed); + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.DelegationSlashed( + serviceProvider, + verifier, + calcValues.delegationTokensSlashed + ); + } else { + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.DelegationSlashingSkipped( + serviceProvider, + verifier, + calcValues.delegationTokensSlashed + ); + } + } + staking.slash(serviceProvider, tokens, verifierCutAmount, verifier); + + // after + Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); + DelegationPoolInternalTest memory afterPool = _getStorage_DelegationPoolInternal( + serviceProvider, + verifier, + legacy + ); + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); + uint256 afterStakingBalance = token.balanceOf(address(staking)); + uint256 afterVerifierBalance = token.balanceOf(verifier); + + { + uint256 tokensSlashed = calcValues.providerTokensSlashed + + (isDelegationSlashingEnabled ? calcValues.delegationTokensSlashed : 0); + uint256 provisionThawingTokens = (before.provision.tokensThawing * + (1e18 - + ((calcValues.providerTokensSlashed * 1e18 + before.provision.tokens - 1) / + before.provision.tokens))) / (1e18); + + // assert + assertEq(afterProvision.tokens + calcValues.providerTokensSlashed, before.provision.tokens); + assertEq(afterProvision.tokensThawing, provisionThawingTokens); + assertEq( + afterProvision.sharesThawing, + afterProvision.tokensThawing == 0 ? 0 : before.provision.sharesThawing + ); + assertEq(afterProvision.maxVerifierCut, before.provision.maxVerifierCut); + assertEq(afterProvision.maxVerifierCutPending, before.provision.maxVerifierCutPending); + assertEq(afterProvision.thawingPeriod, before.provision.thawingPeriod); + assertEq(afterProvision.thawingPeriodPending, before.provision.thawingPeriodPending); + assertEq( + afterProvision.thawingNonce, + (before.provision.sharesThawing != 0 && afterProvision.sharesThawing == 0) + ? before.provision.thawingNonce + 1 + : before.provision.thawingNonce + ); + if (isDelegationSlashingEnabled) { + uint256 poolThawingTokens = (before.pool.tokensThawing * + (1e18 - + ((calcValues.delegationTokensSlashed * 1e18 + before.pool.tokens - 1) / before.pool.tokens))) / + (1e18); + assertEq(afterPool.tokens + calcValues.delegationTokensSlashed, before.pool.tokens); + assertEq(afterPool.shares, before.pool.shares); + assertEq(afterPool.tokensThawing, poolThawingTokens); + assertEq(afterPool.sharesThawing, afterPool.tokensThawing == 0 ? 0 : before.pool.sharesThawing); + assertEq( + afterPool.thawingNonce, + (before.pool.sharesThawing != 0 && afterPool.sharesThawing == 0) + ? before.pool.thawingNonce + 1 + : before.pool.thawingNonce + ); + } + + assertEq(before.stakingBalance - tokensSlashed, afterStakingBalance); + assertEq(before.verifierBalance + verifierCutAmount, afterVerifierBalance); + + assertEq( + afterServiceProvider.tokensStaked + calcValues.providerTokensSlashed, + before.serviceProvider.tokensStaked + ); + assertEq( + afterServiceProvider.tokensProvisioned + calcValues.providerTokensSlashed, + before.serviceProvider.tokensProvisioned + ); + } + } + + // use struct to avoid 'stack too deep' error + struct CalcValues_CloseAllocation { + uint256 rewards; + uint256 delegatorRewards; + uint256 indexerRewards; + } + struct BeforeValues_CloseAllocation { + IHorizonStakingExtension.Allocation allocation; + DelegationPoolInternalTest pool; + ServiceProviderInternal serviceProvider; + uint256 subgraphAllocations; + uint256 stakingBalance; + uint256 indexerBalance; + uint256 beneficiaryBalance; + } + + // Current rewards manager is mocked and assumed to mint fixed rewards + function _closeAllocation(address allocationId, bytes32 poi) internal { + (, address msgSender, ) = vm.readCallers(); + + // before + BeforeValues_CloseAllocation memory beforeValues; + beforeValues.allocation = staking.getAllocation(allocationId); + beforeValues.pool = _getStorage_DelegationPoolInternal( + beforeValues.allocation.indexer, + subgraphDataServiceLegacyAddress, + true + ); + beforeValues.serviceProvider = _getStorage_ServiceProviderInternal(beforeValues.allocation.indexer); + beforeValues.subgraphAllocations = _getStorage_SubgraphAllocations( + beforeValues.allocation.subgraphDeploymentID + ); + beforeValues.stakingBalance = token.balanceOf(address(staking)); + beforeValues.indexerBalance = token.balanceOf(beforeValues.allocation.indexer); + beforeValues.beneficiaryBalance = token.balanceOf( + _getStorage_RewardsDestination(beforeValues.allocation.indexer) + ); + + bool isAuth = staking.isAuthorized( + beforeValues.allocation.indexer, + subgraphDataServiceLegacyAddress, + msgSender + ); + address rewardsDestination = _getStorage_RewardsDestination(beforeValues.allocation.indexer); + + CalcValues_CloseAllocation memory calcValues = CalcValues_CloseAllocation({ + rewards: ALLOCATIONS_REWARD_CUT, + delegatorRewards: ALLOCATIONS_REWARD_CUT - + uint256(beforeValues.pool.__DEPRECATED_indexingRewardCut).mulPPM(ALLOCATIONS_REWARD_CUT), + indexerRewards: 0 + }); + calcValues.indexerRewards = + ALLOCATIONS_REWARD_CUT - + (beforeValues.pool.tokens > 0 ? calcValues.delegatorRewards : 0); + + // closeAllocation + vm.expectEmit(address(staking)); + emit IHorizonStakingExtension.AllocationClosed( + beforeValues.allocation.indexer, + beforeValues.allocation.subgraphDeploymentID, + epochManager.currentEpoch(), + beforeValues.allocation.tokens, + allocationId, + msgSender, + poi, + !isAuth + ); + staking.closeAllocation(allocationId, poi); + + // after + IHorizonStakingExtension.Allocation memory afterAllocation = staking.getAllocation(allocationId); + DelegationPoolInternalTest memory afterPool = _getStorage_DelegationPoolInternal( + beforeValues.allocation.indexer, + subgraphDataServiceLegacyAddress, + true + ); + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal( + beforeValues.allocation.indexer + ); + uint256 afterSubgraphAllocations = _getStorage_SubgraphAllocations( + beforeValues.allocation.subgraphDeploymentID + ); + uint256 afterStakingBalance = token.balanceOf(address(staking)); + uint256 afterIndexerBalance = token.balanceOf(beforeValues.allocation.indexer); + uint256 afterBeneficiaryBalance = token.balanceOf(rewardsDestination); + + if (beforeValues.allocation.tokens > 0) { + if (isAuth && poi != 0) { + if (rewardsDestination != address(0)) { + assertEq( + beforeValues.stakingBalance + calcValues.rewards - calcValues.indexerRewards, + afterStakingBalance + ); + assertEq(beforeValues.indexerBalance, afterIndexerBalance); + assertEq(beforeValues.beneficiaryBalance + calcValues.indexerRewards, afterBeneficiaryBalance); + } else { + assertEq(beforeValues.stakingBalance + calcValues.rewards, afterStakingBalance); + assertEq(beforeValues.indexerBalance, afterIndexerBalance); + assertEq(beforeValues.beneficiaryBalance, afterBeneficiaryBalance); + } + } else { + assertEq(beforeValues.stakingBalance, afterStakingBalance); + assertEq(beforeValues.indexerBalance, afterIndexerBalance); + assertEq(beforeValues.beneficiaryBalance, afterBeneficiaryBalance); + } + } else { + assertEq(beforeValues.stakingBalance, afterStakingBalance); + assertEq(beforeValues.indexerBalance, afterIndexerBalance); + assertEq(beforeValues.beneficiaryBalance, afterBeneficiaryBalance); + } + + assertEq(afterAllocation.indexer, beforeValues.allocation.indexer); + assertEq(afterAllocation.subgraphDeploymentID, beforeValues.allocation.subgraphDeploymentID); + assertEq(afterAllocation.tokens, beforeValues.allocation.tokens); + assertEq(afterAllocation.createdAtEpoch, beforeValues.allocation.createdAtEpoch); + assertEq(afterAllocation.closedAtEpoch, epochManager.currentEpoch()); + assertEq(afterAllocation.collectedFees, beforeValues.allocation.collectedFees); + assertEq( + afterAllocation.__DEPRECATED_effectiveAllocation, + beforeValues.allocation.__DEPRECATED_effectiveAllocation + ); + assertEq(afterAllocation.accRewardsPerAllocatedToken, beforeValues.allocation.accRewardsPerAllocatedToken); + assertEq(afterAllocation.distributedRebates, beforeValues.allocation.distributedRebates); + + if (beforeValues.allocation.tokens > 0 && isAuth && poi != 0 && rewardsDestination == address(0)) { + assertEq( + afterServiceProvider.tokensStaked, + beforeValues.serviceProvider.tokensStaked + calcValues.indexerRewards + ); + } else { + assertEq(afterServiceProvider.tokensStaked, beforeValues.serviceProvider.tokensStaked); + } + assertEq(afterServiceProvider.tokensProvisioned, beforeValues.serviceProvider.tokensProvisioned); + assertEq( + afterServiceProvider.__DEPRECATED_tokensAllocated + beforeValues.allocation.tokens, + beforeValues.serviceProvider.__DEPRECATED_tokensAllocated + ); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLocked, + beforeValues.serviceProvider.__DEPRECATED_tokensLocked + ); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLockedUntil, + beforeValues.serviceProvider.__DEPRECATED_tokensLockedUntil + ); + + assertEq(afterSubgraphAllocations + beforeValues.allocation.tokens, beforeValues.subgraphAllocations); + + if (beforeValues.allocation.tokens > 0 && isAuth && poi != 0 && beforeValues.pool.tokens > 0) { + assertEq(afterPool.tokens, beforeValues.pool.tokens + calcValues.delegatorRewards); + } else { + assertEq(afterPool.tokens, beforeValues.pool.tokens); + } + } + + // use struct to avoid 'stack too deep' error + struct BeforeValues_Collect { + IHorizonStakingExtension.Allocation allocation; + DelegationPoolInternalTest pool; + ServiceProviderInternal serviceProvider; + uint256 stakingBalance; + uint256 senderBalance; + uint256 curationBalance; + uint256 beneficiaryBalance; + } + struct CalcValues_Collect { + uint256 protocolTaxTokens; + uint256 queryFees; + uint256 curationCutTokens; + uint256 newRebates; + uint256 payment; + uint256 delegationFeeCut; + } + struct AfterValues_Collect { + IHorizonStakingExtension.Allocation allocation; + DelegationPoolInternalTest pool; + ServiceProviderInternal serviceProvider; + uint256 stakingBalance; + uint256 senderBalance; + uint256 curationBalance; + uint256 beneficiaryBalance; + } + + function _collect(uint256 tokens, address allocationId) internal { + (, address msgSender, ) = vm.readCallers(); + + // before + BeforeValues_Collect memory beforeValues; + beforeValues.allocation = staking.getAllocation(allocationId); + beforeValues.pool = _getStorage_DelegationPoolInternal( + beforeValues.allocation.indexer, + subgraphDataServiceLegacyAddress, + true + ); + beforeValues.serviceProvider = _getStorage_ServiceProviderInternal(beforeValues.allocation.indexer); + + (uint32 curationPercentage, uint32 protocolPercentage) = _getStorage_ProtocolTaxAndCuration(); + address rewardsDestination = _getStorage_RewardsDestination(beforeValues.allocation.indexer); + + beforeValues.stakingBalance = token.balanceOf(address(staking)); + beforeValues.senderBalance = token.balanceOf(msgSender); + beforeValues.curationBalance = token.balanceOf(address(curation)); + beforeValues.beneficiaryBalance = token.balanceOf(rewardsDestination); + + // calc some stuff + CalcValues_Collect memory calcValues; + calcValues.protocolTaxTokens = tokens.mulPPMRoundUp(protocolPercentage); + calcValues.queryFees = tokens - calcValues.protocolTaxTokens; + calcValues.curationCutTokens = 0; + if (curation.isCurated(beforeValues.allocation.subgraphDeploymentID)) { + calcValues.curationCutTokens = calcValues.queryFees.mulPPMRoundUp(curationPercentage); + calcValues.queryFees -= calcValues.curationCutTokens; + } + calcValues.newRebates = ExponentialRebates.exponentialRebates( + calcValues.queryFees + beforeValues.allocation.collectedFees, + beforeValues.allocation.tokens, + alphaNumerator, + alphaDenominator, + lambdaNumerator, + lambdaDenominator + ); + calcValues.payment = calcValues.newRebates > calcValues.queryFees + ? calcValues.queryFees + : calcValues.newRebates; + calcValues.delegationFeeCut = 0; + if (beforeValues.pool.tokens > 0) { + calcValues.delegationFeeCut = + calcValues.payment - + calcValues.payment.mulPPM(beforeValues.pool.__DEPRECATED_queryFeeCut); + calcValues.payment -= calcValues.delegationFeeCut; + } + + // staking.collect() + if (tokens > 0) { + vm.expectEmit(address(staking)); + emit IHorizonStakingExtension.RebateCollected( + msgSender, + beforeValues.allocation.indexer, + beforeValues.allocation.subgraphDeploymentID, + allocationId, + epochManager.currentEpoch(), + tokens, + calcValues.protocolTaxTokens, + calcValues.curationCutTokens, + calcValues.queryFees, + calcValues.payment, + calcValues.delegationFeeCut + ); + } + staking.collect(tokens, allocationId); + + // after + AfterValues_Collect memory afterValues; + afterValues.allocation = staking.getAllocation(allocationId); + afterValues.pool = _getStorage_DelegationPoolInternal( + beforeValues.allocation.indexer, + subgraphDataServiceLegacyAddress, + true + ); + afterValues.serviceProvider = _getStorage_ServiceProviderInternal(beforeValues.allocation.indexer); + afterValues.stakingBalance = token.balanceOf(address(staking)); + afterValues.senderBalance = token.balanceOf(msgSender); + afterValues.curationBalance = token.balanceOf(address(curation)); + afterValues.beneficiaryBalance = token.balanceOf(rewardsDestination); + + // assert + assertEq(afterValues.senderBalance + tokens, beforeValues.senderBalance); + assertEq(afterValues.curationBalance, beforeValues.curationBalance + calcValues.curationCutTokens); + if (rewardsDestination != address(0)) { + assertEq(afterValues.beneficiaryBalance, beforeValues.beneficiaryBalance + calcValues.payment); + assertEq(afterValues.stakingBalance, beforeValues.stakingBalance + calcValues.delegationFeeCut); + } else { + assertEq(afterValues.beneficiaryBalance, beforeValues.beneficiaryBalance); + assertEq( + afterValues.stakingBalance, + beforeValues.stakingBalance + calcValues.delegationFeeCut + calcValues.payment + ); + } + + assertEq( + afterValues.allocation.collectedFees, + beforeValues.allocation.collectedFees + tokens - calcValues.protocolTaxTokens - calcValues.curationCutTokens + ); + assertEq(afterValues.allocation.indexer, beforeValues.allocation.indexer); + assertEq(afterValues.allocation.subgraphDeploymentID, beforeValues.allocation.subgraphDeploymentID); + assertEq(afterValues.allocation.tokens, beforeValues.allocation.tokens); + assertEq(afterValues.allocation.createdAtEpoch, beforeValues.allocation.createdAtEpoch); + assertEq(afterValues.allocation.closedAtEpoch, beforeValues.allocation.closedAtEpoch); + assertEq( + afterValues.allocation.accRewardsPerAllocatedToken, + beforeValues.allocation.accRewardsPerAllocatedToken + ); + assertEq( + afterValues.allocation.distributedRebates, + beforeValues.allocation.distributedRebates + calcValues.newRebates + ); + + assertEq(afterValues.pool.tokens, beforeValues.pool.tokens + calcValues.delegationFeeCut); + assertEq(afterValues.pool.shares, beforeValues.pool.shares); + assertEq(afterValues.pool.tokensThawing, beforeValues.pool.tokensThawing); + assertEq(afterValues.pool.sharesThawing, beforeValues.pool.sharesThawing); + assertEq(afterValues.pool.thawingNonce, beforeValues.pool.thawingNonce); + + assertEq(afterValues.serviceProvider.tokensProvisioned, beforeValues.serviceProvider.tokensProvisioned); + if (rewardsDestination != address(0)) { + assertEq(afterValues.serviceProvider.tokensStaked, beforeValues.serviceProvider.tokensStaked); + } else { + assertEq( + afterValues.serviceProvider.tokensStaked, + beforeValues.serviceProvider.tokensStaked + calcValues.payment + ); + } + } + + /* + * STORAGE HELPERS + */ + function _getStorage_ServiceProviderInternal( + address serviceProvider + ) internal view returns (ServiceProviderInternal memory) { + uint256 slotNumber = 14; + uint256 baseSlotUint = uint256(keccak256(abi.encode(serviceProvider, slotNumber))); + + ServiceProviderInternal memory serviceProviderInternal = ServiceProviderInternal({ + tokensStaked: uint256(vm.load(address(staking), bytes32(baseSlotUint))), + __DEPRECATED_tokensAllocated: uint256(vm.load(address(staking), bytes32(baseSlotUint + 1))), + __DEPRECATED_tokensLocked: uint256(vm.load(address(staking), bytes32(baseSlotUint + 2))), + __DEPRECATED_tokensLockedUntil: uint256(vm.load(address(staking), bytes32(baseSlotUint + 3))), + tokensProvisioned: uint256(vm.load(address(staking), bytes32(baseSlotUint + 4))) + }); + + return serviceProviderInternal; + } + + function _getStorage_OperatorAuth( + address serviceProvider, + address verifier, + address operator, + bool legacy + ) internal view returns (bool) { + uint256 slotNumber = legacy ? 21 : 31; + uint256 slot; + + if (legacy) { + slot = uint256(keccak256(abi.encode(operator, keccak256(abi.encode(serviceProvider, slotNumber))))); + } else { + slot = uint256( + keccak256( + abi.encode( + operator, + keccak256(abi.encode(verifier, keccak256(abi.encode(serviceProvider, slotNumber)))) + ) + ) + ); + } + return vm.load(address(staking), bytes32(slot)) == bytes32(uint256(1)); + } + + function _setStorage_DeprecatedThawingPeriod(uint32 _thawingPeriod) internal { + uint256 slot = 13; + + // Read the current value of the slot + uint256 currentSlotValue = uint256(vm.load(address(staking), bytes32(slot))); + + // Create a mask to clear the bits for __DEPRECATED_thawingPeriod (bits 0-31) + uint256 mask = ~(uint256(0xFFFFFFFF)); // Mask to clear the first 32 bits + + // Clear the bits for __DEPRECATED_thawingPeriod and set the new value + uint256 newSlotValue = (currentSlotValue & mask) | uint256(_thawingPeriod); + + // Store the updated value back into the slot + vm.store(address(staking), bytes32(slot), bytes32(newSlotValue)); + } + + function _setStorage_ServiceProvider( + address _indexer, + uint256 _tokensStaked, + uint256 _tokensAllocated, + uint256 _tokensLocked, + uint256 _tokensLockedUntil, + uint256 _tokensProvisioned + ) internal { + uint256 serviceProviderSlot = 14; + bytes32 serviceProviderBaseSlot = keccak256(abi.encode(_indexer, serviceProviderSlot)); + vm.store(address(staking), bytes32(uint256(serviceProviderBaseSlot)), bytes32(_tokensStaked)); + vm.store(address(staking), bytes32(uint256(serviceProviderBaseSlot) + 1), bytes32(_tokensAllocated)); + vm.store(address(staking), bytes32(uint256(serviceProviderBaseSlot) + 2), bytes32(_tokensLocked)); + vm.store(address(staking), bytes32(uint256(serviceProviderBaseSlot) + 3), bytes32(_tokensLockedUntil)); + vm.store(address(staking), bytes32(uint256(serviceProviderBaseSlot) + 4), bytes32(_tokensProvisioned)); + } + + // DelegationPoolInternal contains a mapping, solidity doesn't allow constructing structs with + // nested mappings on memory: "Struct containing a (nested) mapping cannot be constructed" + // So we use a custom struct here and remove the nested mapping which we don't need anyways + struct DelegationPoolInternalTest { + // (Deprecated) Time, in blocks, an indexer must wait before updating delegation parameters + uint32 __DEPRECATED_cooldownBlocks; + // (Deprecated) Percentage of indexing rewards for the service provider, in PPM + uint32 __DEPRECATED_indexingRewardCut; + // (Deprecated) Percentage of query fees for the service provider, in PPM + uint32 __DEPRECATED_queryFeeCut; + // (Deprecated) Block when the delegation parameters were last updated + uint256 __DEPRECATED_updatedAtBlock; + // Total tokens as pool reserves + uint256 tokens; + // Total shares minted in the pool + uint256 shares; + // Delegation details by delegator + uint256 _gap_delegators_mapping; + // Tokens thawing in the pool + uint256 tokensThawing; + // Shares representing the thawing tokens + uint256 sharesThawing; + // Thawing nonce + uint256 thawingNonce; + } + + function _getStorage_DelegationPoolInternal( + address serviceProvider, + address verifier, + bool legacy + ) internal view returns (DelegationPoolInternalTest memory) { + uint256 slotNumber = legacy ? 20 : 33; + uint256 baseSlot; + if (legacy) { + baseSlot = uint256(keccak256(abi.encode(serviceProvider, slotNumber))); + } else { + baseSlot = uint256(keccak256(abi.encode(verifier, keccak256(abi.encode(serviceProvider, slotNumber))))); + } + + uint256 packedData = uint256(vm.load(address(staking), bytes32(baseSlot))); + + DelegationPoolInternalTest memory delegationPoolInternal = DelegationPoolInternalTest({ + __DEPRECATED_cooldownBlocks: uint32(packedData & 0xFFFFFFFF), + __DEPRECATED_indexingRewardCut: uint32((packedData >> 32) & 0xFFFFFFFF), + __DEPRECATED_queryFeeCut: uint32((packedData >> 64) & 0xFFFFFFFF), + __DEPRECATED_updatedAtBlock: uint256(vm.load(address(staking), bytes32(baseSlot + 1))), + tokens: uint256(vm.load(address(staking), bytes32(baseSlot + 2))), + shares: uint256(vm.load(address(staking), bytes32(baseSlot + 3))), + _gap_delegators_mapping: uint256(vm.load(address(staking), bytes32(baseSlot + 4))), + tokensThawing: uint256(vm.load(address(staking), bytes32(baseSlot + 5))), + sharesThawing: uint256(vm.load(address(staking), bytes32(baseSlot + 6))), + thawingNonce: uint256(vm.load(address(staking), bytes32(baseSlot + 7))) + }); + + return delegationPoolInternal; + } + + function _getStorage_Delegation( + address serviceProvider, + address verifier, + address delegator, + bool legacy + ) internal view returns (DelegationInternal memory) { + uint256 slotNumber = legacy ? 20 : 33; + uint256 baseSlot; + + // DelegationPool + if (legacy) { + baseSlot = uint256(keccak256(abi.encode(serviceProvider, slotNumber))); + } else { + baseSlot = uint256(keccak256(abi.encode(verifier, keccak256(abi.encode(serviceProvider, slotNumber))))); + } + + // delegators slot in DelegationPool + baseSlot += 4; + + // Delegation + baseSlot = uint256(keccak256(abi.encode(delegator, baseSlot))); + + DelegationInternal memory delegation = DelegationInternal({ + shares: uint256(vm.load(address(staking), bytes32(baseSlot))), + __DEPRECATED_tokensLocked: uint256(vm.load(address(staking), bytes32(baseSlot + 1))), + __DEPRECATED_tokensLockedUntil: uint256(vm.load(address(staking), bytes32(baseSlot + 2))) + }); + + return delegation; + } + + function _setStorage_allocation( + IHorizonStakingExtension.Allocation memory allocation, + address allocationId, + uint256 tokens + ) internal { + // __DEPRECATED_allocations + uint256 allocationsSlot = 15; + bytes32 allocationBaseSlot = keccak256(abi.encode(allocationId, allocationsSlot)); + vm.store(address(staking), allocationBaseSlot, bytes32(uint256(uint160(allocation.indexer)))); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 1), allocation.subgraphDeploymentID); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 2), bytes32(tokens)); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 3), bytes32(allocation.createdAtEpoch)); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 4), bytes32(allocation.closedAtEpoch)); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 5), bytes32(allocation.collectedFees)); + vm.store( + address(staking), + bytes32(uint256(allocationBaseSlot) + 6), + bytes32(allocation.__DEPRECATED_effectiveAllocation) + ); + vm.store( + address(staking), + bytes32(uint256(allocationBaseSlot) + 7), + bytes32(allocation.accRewardsPerAllocatedToken) + ); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 8), bytes32(allocation.distributedRebates)); + + // _serviceProviders + uint256 serviceProviderSlot = 14; + bytes32 serviceProviderBaseSlot = keccak256(abi.encode(allocation.indexer, serviceProviderSlot)); + uint256 currentTokensStaked = uint256(vm.load(address(staking), serviceProviderBaseSlot)); + uint256 currentTokensProvisioned = uint256( + vm.load(address(staking), bytes32(uint256(serviceProviderBaseSlot) + 1)) + ); + vm.store( + address(staking), + bytes32(uint256(serviceProviderBaseSlot) + 0), + bytes32(currentTokensStaked + tokens) + ); + vm.store( + address(staking), + bytes32(uint256(serviceProviderBaseSlot) + 1), + bytes32(currentTokensProvisioned + tokens) + ); + + // __DEPRECATED_subgraphAllocations + uint256 subgraphsAllocationsSlot = 16; + bytes32 subgraphAllocationsBaseSlot = keccak256( + abi.encode(allocation.subgraphDeploymentID, subgraphsAllocationsSlot) + ); + uint256 currentAllocatedTokens = uint256(vm.load(address(staking), subgraphAllocationsBaseSlot)); + vm.store(address(staking), subgraphAllocationsBaseSlot, bytes32(currentAllocatedTokens + tokens)); + } + + function _getStorage_SubgraphAllocations(bytes32 subgraphDeploymentID) internal view returns (uint256) { + uint256 subgraphsAllocationsSlot = 16; + bytes32 subgraphAllocationsBaseSlot = keccak256(abi.encode(subgraphDeploymentID, subgraphsAllocationsSlot)); + return uint256(vm.load(address(staking), subgraphAllocationsBaseSlot)); + } + + function _setStorage_RewardsDestination(address serviceProvider, address destination) internal { + uint256 rewardsDestinationSlot = 23; + bytes32 rewardsDestinationSlotBaseSlot = keccak256(abi.encode(serviceProvider, rewardsDestinationSlot)); + vm.store(address(staking), rewardsDestinationSlotBaseSlot, bytes32(uint256(uint160(destination)))); + } + + function _getStorage_RewardsDestination(address serviceProvider) internal view returns (address) { + uint256 rewardsDestinationSlot = 23; + bytes32 rewardsDestinationSlotBaseSlot = keccak256(abi.encode(serviceProvider, rewardsDestinationSlot)); + return address(uint160(uint256(vm.load(address(staking), rewardsDestinationSlotBaseSlot)))); + } + + function _setStorage_MaxAllocationEpochs(uint256 maxAllocationEpochs) internal { + uint256 slot = 13; + + // Read the current value of the storage slot + uint256 currentSlotValue = uint256(vm.load(address(staking), bytes32(slot))); + + // Mask to clear the specific bits for __DEPRECATED_maxAllocationEpochs (bits 128-159) + uint256 mask = ~(uint256(0xFFFFFFFF) << 128); + + // Clear the bits and set the new maxAllocationEpochs value + uint256 newSlotValue = (currentSlotValue & mask) | (uint256(maxAllocationEpochs) << 128); + + // Store the updated value back into the slot + vm.store(address(staking), bytes32(slot), bytes32(newSlotValue)); + + uint256 readMaxAllocationEpochs = _getStorage_MaxAllocationEpochs(); + assertEq(readMaxAllocationEpochs, maxAllocationEpochs); + } + + function _getStorage_MaxAllocationEpochs() internal view returns (uint256) { + uint256 slot = 13; + + // Read the current value of the storage slot + uint256 currentSlotValue = uint256(vm.load(address(staking), bytes32(slot))); + + // Mask to isolate bits 128-159 + uint256 mask = uint256(0xFFFFFFFF) << 128; + + // Extract the maxAllocationEpochs by masking and shifting + uint256 maxAllocationEpochs = (currentSlotValue & mask) >> 128; + + return maxAllocationEpochs; + } + + function _setStorage_DelegationPool( + address serviceProvider, + uint256 tokens, + uint32 indexingRewardCut, + uint32 queryFeeCut + ) internal { + bytes32 baseSlot = keccak256(abi.encode(serviceProvider, uint256(20))); + bytes32 feeCutValues = bytes32( + (uint256(indexingRewardCut) << uint256(32)) | (uint256(queryFeeCut) << uint256(64)) + ); + bytes32 tokensSlot = bytes32(uint256(baseSlot) + 2); + vm.store(address(staking), baseSlot, feeCutValues); + vm.store(address(staking), tokensSlot, bytes32(tokens)); + } + + function _setStorage_RebateParameters( + uint32 alphaNumerator_, + uint32 alphaDenominator_, + uint32 lambdaNumerator_, + uint32 lambdaDenominator_ + ) internal { + // Store alpha numerator and denominator in slot 13 + uint256 alphaSlot = 13; + + uint256 newAlphaSlotValue; + { + uint256 alphaNumeratorOffset = 160; // Offset for __DEPRECATED_alphaNumerator (20th byte) + uint256 alphaDenominatorOffset = 192; // Offset for __DEPRECATED_alphaDenominator (24th byte) + + // Read current value of the slot + uint256 currentAlphaSlotValue = uint256(vm.load(address(staking), bytes32(alphaSlot))); + + // Create a mask to clear the bits for alphaNumerator and alphaDenominator + uint256 alphaMask = ~(uint256(0xFFFFFFFF) << alphaNumeratorOffset) & + ~(uint256(0xFFFFFFFF) << alphaDenominatorOffset); + + // Clear and set new values + newAlphaSlotValue = + (currentAlphaSlotValue & alphaMask) | + (uint256(alphaNumerator_) << alphaNumeratorOffset) | + (uint256(alphaDenominator_) << alphaDenominatorOffset); + } + + // Store the updated value back into the slot + vm.store(address(staking), bytes32(alphaSlot), bytes32(newAlphaSlotValue)); + + // Store lambda numerator and denominator in slot 25 + uint256 lambdaSlot = 25; + + uint256 newLambdaSlotValue; + { + uint256 lambdaNumeratorOffset = 160; // Offset for lambdaNumerator (20th byte) + uint256 lambdaDenominatorOffset = 192; // Offset for lambdaDenominator (24th byte) + + // Read current value of the slot + uint256 currentLambdaSlotValue = uint256(vm.load(address(staking), bytes32(lambdaSlot))); + + // Create a mask to clear the bits for lambdaNumerator and lambdaDenominator + uint256 lambdaMask = ~(uint256(0xFFFFFFFF) << lambdaNumeratorOffset) & + ~(uint256(0xFFFFFFFF) << lambdaDenominatorOffset); + + // Clear and set new values + newLambdaSlotValue = + (currentLambdaSlotValue & lambdaMask) | + (uint256(lambdaNumerator_) << lambdaNumeratorOffset) | + (uint256(lambdaDenominator_) << lambdaDenominatorOffset); + } + + // Store the updated value back into the slot + vm.store(address(staking), bytes32(lambdaSlot), bytes32(newLambdaSlotValue)); + + // Verify the storage + ( + uint32 readAlphaNumerator, + uint32 readAlphaDenominator, + uint32 readLambdaNumerator, + uint32 readLambdaDenominator + ) = _getStorage_RebateParameters(); + assertEq(readAlphaNumerator, alphaNumerator_); + assertEq(readAlphaDenominator, alphaDenominator_); + assertEq(readLambdaNumerator, lambdaNumerator_); + assertEq(readLambdaDenominator, lambdaDenominator_); + } + + function _getStorage_RebateParameters() internal view returns (uint32, uint32, uint32, uint32) { + // Read alpha numerator and denominator + uint256 alphaSlot = 13; + uint256 alphaValues = uint256(vm.load(address(staking), bytes32(alphaSlot))); + uint32 alphaNumerator_ = uint32(alphaValues >> 160); + uint32 alphaDenominator_ = uint32(alphaValues >> 192); + + // Read lambda numerator and denominator + uint256 lambdaSlot = 25; + uint256 lambdaValues = uint256(vm.load(address(staking), bytes32(lambdaSlot))); + uint32 lambdaNumerator_ = uint32(lambdaValues >> 160); + uint32 lambdaDenominator_ = uint32(lambdaValues >> 192); + + return (alphaNumerator_, alphaDenominator_, lambdaNumerator_, lambdaDenominator_); + } + + // function _setStorage_ProtocolTaxAndCuration(uint32 curationPercentage, uint32 taxPercentage) private { + // bytes32 slot = bytes32(uint256(13)); + // uint256 curationOffset = 4; + // uint256 protocolTaxOffset = 8; + // bytes32 originalValue = vm.load(address(staking), slot); + + // bytes32 newProtocolTaxValue = bytes32( + // ((uint256(originalValue) & + // ~((0xFFFFFFFF << (8 * curationOffset)) | (0xFFFFFFFF << (8 * protocolTaxOffset)))) | + // (uint256(curationPercentage) << (8 * curationOffset))) | + // (uint256(taxPercentage) << (8 * protocolTaxOffset)) + // ); + // vm.store(address(staking), slot, newProtocolTaxValue); + + // (uint32 readCurationPercentage, uint32 readTaxPercentage) = _getStorage_ProtocolTaxAndCuration(); + // assertEq(readCurationPercentage, curationPercentage); + // } + + function _setStorage_ProtocolTaxAndCuration(uint32 curationPercentage, uint32 taxPercentage) internal { + bytes32 slot = bytes32(uint256(13)); + + // Offsets for the percentages + uint256 curationOffset = 32; // __DEPRECATED_curationPercentage (2nd uint32, bits 32-63) + uint256 protocolTaxOffset = 64; // __DEPRECATED_protocolPercentage (3rd uint32, bits 64-95) + + // Read the current slot value + uint256 originalValue = uint256(vm.load(address(staking), slot)); + + // Create masks to clear the specific bits for the two percentages + uint256 mask = ~(uint256(0xFFFFFFFF) << curationOffset) & ~(uint256(0xFFFFFFFF) << protocolTaxOffset); // Mask for curationPercentage // Mask for protocolTax + + // Clear the existing bits and set the new values + uint256 newSlotValue = (originalValue & mask) | + (uint256(curationPercentage) << curationOffset) | + (uint256(taxPercentage) << protocolTaxOffset); + + // Store the updated slot value + vm.store(address(staking), slot, bytes32(newSlotValue)); + + // Verify the values were set correctly + (uint32 readCurationPercentage, uint32 readTaxPercentage) = _getStorage_ProtocolTaxAndCuration(); + assertEq(readCurationPercentage, curationPercentage); + assertEq(readTaxPercentage, taxPercentage); + } + + function _getStorage_ProtocolTaxAndCuration() internal view returns (uint32, uint32) { + bytes32 slot = bytes32(uint256(13)); + bytes32 value = vm.load(address(staking), slot); + uint32 curationPercentage = uint32(uint256(value) >> 32); + uint32 taxPercentage = uint32(uint256(value) >> 64); + return (curationPercentage, taxPercentage); + } + + /* + * MISC: private functions to help with testing + */ + // use struct to avoid 'stack too deep' error + struct CalcValues_ThawRequestData { + uint256 tokensThawed; + uint256 tokensThawing; + uint256 sharesThawed; + uint256 sharesThawing; + ThawRequest[] thawRequestsFulfilledList; + bytes32[] thawRequestsFulfilledListIds; + uint256[] thawRequestsFulfilledListTokens; + } + + struct ThawingData { + uint256 tokensThawed; + uint256 tokensThawing; + uint256 sharesThawing; + uint256 thawRequestsFulfilled; + } + + struct Params_CalcThawRequestData { + IHorizonStakingTypes.ThawRequestType thawRequestType; + address serviceProvider; + address verifier; + address owner; + uint256 iterations; + bool delegation; + } + + function calcThawRequestData( + Params_CalcThawRequestData memory params + ) private view returns (CalcValues_ThawRequestData memory) { + LinkedList.List memory thawRequestList = _getThawRequestList( + params.thawRequestType, + params.serviceProvider, + params.verifier, + params.owner + ); + if (thawRequestList.count == 0) { + return CalcValues_ThawRequestData(0, 0, 0, 0, new ThawRequest[](0), new bytes32[](0), new uint256[](0)); + } + + Provision memory prov = staking.getProvision(params.serviceProvider, params.verifier); + DelegationPool memory pool = staking.getDelegationPool(params.serviceProvider, params.verifier); + + uint256 tokensThawed = 0; + uint256 sharesThawed = 0; + uint256 tokensThawing = params.delegation ? pool.tokensThawing : prov.tokensThawing; + uint256 sharesThawing = params.delegation ? pool.sharesThawing : prov.sharesThawing; + uint256 thawRequestsFulfilled = 0; + + bytes32 thawRequestId = thawRequestList.head; + while (thawRequestId != bytes32(0) && (params.iterations == 0 || thawRequestsFulfilled < params.iterations)) { + ThawRequest memory thawRequest = _getThawRequest(params.thawRequestType, thawRequestId); + bool isThawRequestValid = thawRequest.thawingNonce == + (params.delegation ? pool.thawingNonce : prov.thawingNonce); + if (thawRequest.thawingUntil <= block.timestamp) { + thawRequestsFulfilled++; + if (isThawRequestValid) { + uint256 tokens = params.delegation + ? (thawRequest.shares * pool.tokensThawing) / pool.sharesThawing + : (thawRequest.shares * prov.tokensThawing) / prov.sharesThawing; + tokensThawed += tokens; + tokensThawing -= tokens; + sharesThawed += thawRequest.shares; + sharesThawing -= thawRequest.shares; + } + } else { + break; + } + thawRequestId = thawRequest.next; + } + + // we need to do a second pass because solidity doesnt allow dynamic arrays on memory + CalcValues_ThawRequestData memory thawRequestData; + thawRequestData.tokensThawed = tokensThawed; + thawRequestData.tokensThawing = tokensThawing; + thawRequestData.sharesThawed = sharesThawed; + thawRequestData.sharesThawing = sharesThawing; + thawRequestData.thawRequestsFulfilledList = new ThawRequest[](thawRequestsFulfilled); + thawRequestData.thawRequestsFulfilledListIds = new bytes32[](thawRequestsFulfilled); + thawRequestData.thawRequestsFulfilledListTokens = new uint256[](thawRequestsFulfilled); + uint256 i = 0; + thawRequestId = thawRequestList.head; + while (thawRequestId != bytes32(0) && (params.iterations == 0 || i < params.iterations)) { + ThawRequest memory thawRequest = _getThawRequest(params.thawRequestType, thawRequestId); + bool isThawRequestValid = thawRequest.thawingNonce == + (params.delegation ? pool.thawingNonce : prov.thawingNonce); + + if (thawRequest.thawingUntil <= block.timestamp) { + if (isThawRequestValid) { + thawRequestData.thawRequestsFulfilledListTokens[i] = params.delegation + ? (thawRequest.shares * pool.tokensThawing) / pool.sharesThawing + : (thawRequest.shares * prov.tokensThawing) / prov.sharesThawing; + } + thawRequestData.thawRequestsFulfilledListIds[i] = thawRequestId; + thawRequestData.thawRequestsFulfilledList[i] = _getThawRequest(params.thawRequestType, thawRequestId); + thawRequestId = thawRequestData.thawRequestsFulfilledList[i].next; + i++; + } else { + break; + } + thawRequestId = thawRequest.next; + } + + assertEq(thawRequestsFulfilled, thawRequestData.thawRequestsFulfilledList.length); + assertEq(thawRequestsFulfilled, thawRequestData.thawRequestsFulfilledListIds.length); + assertEq(thawRequestsFulfilled, thawRequestData.thawRequestsFulfilledListTokens.length); + + return thawRequestData; + } + + function _getThawRequestList( + IHorizonStakingTypes.ThawRequestType thawRequestType, + address serviceProvider, + address verifier, + address owner + ) private view returns (LinkedList.List memory) { + return staking.getThawRequestList(thawRequestType, serviceProvider, verifier, owner); + } + + function _getThawRequest( + IHorizonStakingTypes.ThawRequestType thawRequestType, + bytes32 thawRequestId + ) private view returns (ThawRequest memory) { + return staking.getThawRequest(thawRequestType, thawRequestId); + } +} diff --git a/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol b/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol new file mode 100644 index 000000000..72e92dfde --- /dev/null +++ b/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IPaymentsEscrow } from "../../../contracts/interfaces/IPaymentsEscrow.sol"; +import { GraphBaseTest } from "../../GraphBase.t.sol"; + +abstract contract PaymentsEscrowSharedTest is GraphBaseTest { + /* + * MODIFIERS + */ + + modifier useGateway() { + vm.startPrank(users.gateway); + _; + vm.stopPrank(); + } + + /* + * HELPERS + */ + + function _depositTokens(address _collector, address _receiver, uint256 _tokens) internal { + (, address msgSender, ) = vm.readCallers(); + (uint256 escrowBalanceBefore, , ) = escrow.escrowAccounts(msgSender, _collector, _receiver); + token.approve(address(escrow), _tokens); + + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.Deposit(msgSender, _collector, _receiver, _tokens); + escrow.deposit(_collector, _receiver, _tokens); + + (uint256 escrowBalanceAfter, , ) = escrow.escrowAccounts(msgSender, _collector, _receiver); + assertEq(escrowBalanceAfter - _tokens, escrowBalanceBefore); + } + + function _depositToTokens(address _payer, address _collector, address _receiver, uint256 _tokens) internal { + (uint256 escrowBalanceBefore, , ) = escrow.escrowAccounts(_payer, _collector, _receiver); + token.approve(address(escrow), _tokens); + + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.Deposit(_payer, _collector, _receiver, _tokens); + escrow.depositTo(_payer, _collector, _receiver, _tokens); + + (uint256 escrowBalanceAfter, , ) = escrow.escrowAccounts(_payer, _collector, _receiver); + assertEq(escrowBalanceAfter - _tokens, escrowBalanceBefore); + } +} diff --git a/packages/horizon/test/staking/HorizonStaking.t.sol b/packages/horizon/test/staking/HorizonStaking.t.sol new file mode 100644 index 000000000..5dd4d6153 --- /dev/null +++ b/packages/horizon/test/staking/HorizonStaking.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; + +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; + +contract HorizonStakingTest is HorizonStakingSharedTest { + using stdStorage for StdStorage; + + /* + * MODIFIERS + */ + + modifier usePausedStaking() { + vm.startPrank(users.governor); + controller.setPaused(true); + vm.stopPrank(); + _; + } + + modifier useThawAndDeprovision(uint256 amount, uint64 thawingPeriod) { + vm.assume(amount > 0); + _thaw(users.indexer, subgraphDataServiceAddress, amount); + skip(thawingPeriod + 1); + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + _; + } + + modifier useDelegation(uint256 delegationAmount) { + address msgSender; + (, msgSender, ) = vm.readCallers(); + vm.assume(delegationAmount >= MIN_DELEGATION); + vm.assume(delegationAmount <= MAX_STAKING_TOKENS); + vm.startPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + vm.startPrank(msgSender); + _; + } + + modifier useLockedVerifier(address verifier) { + address msgSender; + (, msgSender, ) = vm.readCallers(); + resetPrank(users.governor); + _setAllowedLockedVerifier(verifier, true); + resetPrank(msgSender); + _; + } + + modifier useDelegationSlashing() { + address msgSender; + (, msgSender, ) = vm.readCallers(); + resetPrank(users.governor); + staking.setDelegationSlashingEnabled(); + resetPrank(msgSender); + _; + } + + modifier useUndelegate(uint256 shares) { + resetPrank(users.delegator); + + DelegationPoolInternalTest memory pool = _getStorage_DelegationPoolInternal( + users.indexer, + subgraphDataServiceAddress, + false + ); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + + shares = bound(shares, 1, delegation.shares); + uint256 tokens = (shares * (pool.tokens - pool.tokensThawing)) / pool.shares; + if (shares < delegation.shares) { + uint256 remainingTokens = (shares * (pool.tokens - pool.tokensThawing - tokens)) / pool.shares; + vm.assume(remainingTokens >= MIN_DELEGATION); + } + + _undelegate(users.indexer, subgraphDataServiceAddress, shares); + _; + } +} diff --git a/packages/horizon/test/staking/allocation/allocation.t.sol b/packages/horizon/test/staking/allocation/allocation.t.sol new file mode 100644 index 000000000..189423632 --- /dev/null +++ b/packages/horizon/test/staking/allocation/allocation.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; +import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol"; + +contract HorizonStakingAllocationTest is HorizonStakingTest { + /* + * TESTS + */ + + function testAllocation_GetAllocationState_Active(uint256 tokens) public useIndexer useAllocation(tokens) { + IHorizonStakingExtension.AllocationState state = staking.getAllocationState(_allocationId); + assertEq(uint16(state), uint16(IHorizonStakingExtension.AllocationState.Active)); + } + + function testAllocation_GetAllocationState_Null() public view { + IHorizonStakingExtension.AllocationState state = staking.getAllocationState(_allocationId); + assertEq(uint16(state), uint16(IHorizonStakingExtension.AllocationState.Null)); + } + + function testAllocation_IsAllocation(uint256 tokens) public useIndexer useAllocation(tokens) { + bool isAllocation = staking.isAllocation(_allocationId); + assertTrue(isAllocation); + } + + function testAllocation_IsNotAllocation() public view { + bool isAllocation = staking.isAllocation(_allocationId); + assertFalse(isAllocation); + } +} diff --git a/packages/horizon/test/staking/allocation/close.t.sol b/packages/horizon/test/staking/allocation/close.t.sol new file mode 100644 index 000000000..da2e869a3 --- /dev/null +++ b/packages/horizon/test/staking/allocation/close.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; +import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol"; +import { PPMMath } from "../../../contracts/libraries/PPMMath.sol"; + +contract HorizonStakingCloseAllocationTest is HorizonStakingTest { + using PPMMath for uint256; + + bytes32 internal constant _poi = keccak256("poi"); + + /* + * MODIFIERS + */ + + modifier useLegacyOperator() { + resetPrank(users.indexer); + _setOperator(subgraphDataServiceLegacyAddress, users.operator, true); + vm.startPrank(users.operator); + _; + vm.stopPrank(); + } + + /* + * TESTS + */ + + function testCloseAllocation(uint256 tokens) public useIndexer useAllocation(1 ether) { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + // Skip 15 epochs + vm.roll(15); + + _closeAllocation(_allocationId, _poi); + } + + function testCloseAllocation_Operator(uint256 tokens) public useLegacyOperator useAllocation(1 ether) { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + // Skip 15 epochs + vm.roll(15); + + _closeAllocation(_allocationId, _poi); + } + + function testCloseAllocation_WithBeneficiaryAddress(uint256 tokens) public useIndexer useAllocation(1 ether) { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + address beneficiary = makeAddr("beneficiary"); + _setStorage_RewardsDestination(users.indexer, beneficiary); + + // Skip 15 epochs + vm.roll(15); + + _closeAllocation(_allocationId, _poi); + } + + function testCloseAllocation_RevertWhen_NotActive() public { + vm.expectRevert("!active"); + staking.closeAllocation(_allocationId, _poi); + } + + function testCloseAllocation_RevertWhen_NotIndexer() public useIndexer useAllocation(1 ether) { + resetPrank(users.delegator); + vm.expectRevert("!auth"); + staking.closeAllocation(_allocationId, _poi); + } + + function testCloseAllocation_AfterMaxEpochs_AnyoneCanClose( + uint256 tokens + ) public useIndexer useAllocation(1 ether) { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + // Skip to over the max allocation epochs + vm.roll((MAX_ALLOCATION_EPOCHS + 1) * EPOCH_LENGTH + 1); + + resetPrank(users.delegator); + _closeAllocation(_allocationId, 0x0); + } + + function testCloseAllocation_RevertWhen_ZeroTokensNotAuthorized() public useIndexer useAllocation(1 ether) { + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, 100 ether, 0, 0); + + resetPrank(users.delegator); + vm.expectRevert("!auth"); + staking.closeAllocation(_allocationId, 0x0); + } + + function testCloseAllocation_WithDelegation( + uint256 tokens, + uint256 delegationTokens, + uint32 indexingRewardCut + ) public useIndexer useAllocation(1 ether) { + tokens = bound(tokens, 2, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, 0, MAX_STAKING_TOKENS); + vm.assume(indexingRewardCut <= MAX_PPM); + + uint256 legacyAllocationTokens = tokens / 2; + uint256 provisionTokens = tokens - legacyAllocationTokens; + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, provisionTokens, 0, 0); + _setStorage_DelegationPool(users.indexer, delegationTokens, indexingRewardCut, 0); + + // Skip 15 epochs + vm.roll(15); + + _closeAllocation(_allocationId, _poi); + } +} diff --git a/packages/horizon/test/staking/allocation/collect.t.sol b/packages/horizon/test/staking/allocation/collect.t.sol new file mode 100644 index 000000000..6b0c6cfcd --- /dev/null +++ b/packages/horizon/test/staking/allocation/collect.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; +import { ExponentialRebates } from "../../../contracts/staking/libraries/ExponentialRebates.sol"; +import { PPMMath } from "../../../contracts/libraries/PPMMath.sol"; + +contract HorizonStakingCollectAllocationTest is HorizonStakingTest { + using PPMMath for uint256; + + /* + * TESTS + */ + + function testCollectAllocation_RevertWhen_InvalidAllocationId( + uint256 tokens + ) public useIndexer useAllocation(1 ether) { + vm.expectRevert("!alloc"); + staking.collect(tokens, address(0)); + } + + function testCollectAllocation_RevertWhen_Null(uint256 tokens) public { + vm.expectRevert("!collect"); + staking.collect(tokens, _allocationId); + } + + function testCollect_Tokenss( + uint256 allocationTokens, + uint256 collectTokens, + uint256 curationTokens, + uint32 curationPercentage, + uint32 protocolTaxPercentage, + uint256 delegationTokens, + uint32 queryFeeCut + ) public useIndexer useRebateParameters useAllocation(allocationTokens) { + collectTokens = bound(collectTokens, 0, MAX_STAKING_TOKENS); + curationTokens = bound(curationTokens, 0, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, 0, MAX_STAKING_TOKENS); + vm.assume(curationPercentage <= MAX_PPM); + vm.assume(protocolTaxPercentage <= MAX_PPM); + vm.assume(queryFeeCut <= MAX_PPM); + + resetPrank(users.indexer); + _setStorage_ProtocolTaxAndCuration(curationPercentage, protocolTaxPercentage); + console.log("queryFeeCut", queryFeeCut); + _setStorage_DelegationPool(users.indexer, delegationTokens, 0, queryFeeCut); + curation.signal(_subgraphDeploymentID, curationTokens); + + resetPrank(users.gateway); + approve(address(staking), collectTokens); + _collect(collectTokens, _allocationId); + } + + function testCollect_WithBeneficiaryAddress( + uint256 allocationTokens, + uint256 collectTokens + ) public useIndexer useRebateParameters useAllocation(allocationTokens) { + collectTokens = bound(collectTokens, 0, MAX_STAKING_TOKENS); + + address beneficiary = makeAddr("beneficiary"); + _setStorage_RewardsDestination(users.indexer, beneficiary); + + resetPrank(users.gateway); + approve(address(staking), collectTokens); + _collect(collectTokens, _allocationId); + + uint256 newRebates = ExponentialRebates.exponentialRebates( + collectTokens, + allocationTokens, + alphaNumerator, + alphaDenominator, + lambdaNumerator, + lambdaDenominator + ); + uint256 payment = newRebates > collectTokens ? collectTokens : newRebates; + + assertEq(token.balanceOf(beneficiary), payment); + } +} diff --git a/packages/horizon/test/staking/delegation/addToPool.t.sol b/packages/horizon/test/staking/delegation/addToPool.t.sol new file mode 100644 index 000000000..7652478e2 --- /dev/null +++ b/packages/horizon/test/staking/delegation/addToPool.t.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingDelegationAddToPoolTest is HorizonStakingTest { + modifier useValidDelegationAmount(uint256 tokens) { + vm.assume(tokens <= MAX_STAKING_TOKENS); + vm.assume(tokens >= MIN_DELEGATION); + _; + } + + modifier useValidAddToPoolAmount(uint256 tokens) { + vm.assume(tokens > 0); + vm.assume(tokens <= MAX_STAKING_TOKENS); + _; + } + + /* + * TESTS + */ + + function test_Delegation_AddToPool_Verifier( + uint256 amount, + uint256 delegationAmount, + uint256 addToPoolAmount + ) + public + useIndexer + useProvision(amount, 0, 0) + useValidDelegationAmount(delegationAmount) + useValidAddToPoolAmount(addToPoolAmount) + { + delegationAmount = bound(delegationAmount, 1, MAX_STAKING_TOKENS); + + // Initialize delegation pool + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + + resetPrank(subgraphDataServiceAddress); + mint(subgraphDataServiceAddress, addToPoolAmount); + token.approve(address(staking), addToPoolAmount); + _addToDelegationPool(users.indexer, subgraphDataServiceAddress, addToPoolAmount); + } + + function test_Delegation_AddToPool_Payments( + uint256 amount, + uint256 delegationAmount + ) + public + useIndexer + useProvision(amount, 0, 0) + useValidDelegationAmount(delegationAmount) + useValidAddToPoolAmount(delegationAmount) + { + // Initialize delegation pool + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + + resetPrank(address(payments)); + mint(address(payments), delegationAmount); + token.approve(address(staking), delegationAmount); + _addToDelegationPool(users.indexer, subgraphDataServiceAddress, delegationAmount); + } + + function test_Delegation_AddToPool_RevertWhen_ZeroTokens( + uint256 amount + ) public useIndexer useProvision(amount, 0, 0) { + vm.startPrank(subgraphDataServiceAddress); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidZeroTokens.selector + ); + vm.expectRevert(expectedError); + staking.addToDelegationPool(users.indexer, subgraphDataServiceAddress, 0); + } + + function test_Delegation_AddToPool_RevertWhen_PoolHasNoShares( + uint256 amount + ) public useIndexer useProvision(amount, 0, 0) { + vm.startPrank(subgraphDataServiceAddress); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidDelegationPool.selector, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.addToDelegationPool(users.indexer, subgraphDataServiceAddress, 1); + } + + function test_Delegation_AddToPool_RevertWhen_NoProvision() public { + vm.startPrank(subgraphDataServiceAddress); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidProvision.selector, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.addToDelegationPool(users.indexer, subgraphDataServiceAddress, 1); + } + + function test_Delegation_AddToPool_WhenInvalidPool( + uint256 tokens, + uint256 delegationTokens, + uint256 recoverAmount + ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing { + recoverAmount = bound(recoverAmount, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + + // create delegation pool + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // slash entire provision + pool + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // recover pool by adding tokens + resetPrank(users.indexer); + token.approve(address(staking), recoverAmount); + _addToDelegationPool(users.indexer, subgraphDataServiceAddress, recoverAmount); + } + + function test_Delegation_AddToPool_WhenInvalidPool_RevertWhen_PoolHasNoShares( + uint256 tokens, + uint256 delegationTokens, + uint256 recoverAmount + ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing { + recoverAmount = bound(recoverAmount, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + + // create delegation pool + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // undelegate shares so we have thawing shares/tokens + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + resetPrank(users.delegator); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); + + // slash entire provision + pool + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // addTokens + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidDelegationPool.selector, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.addToDelegationPool(users.indexer, subgraphDataServiceAddress, 1); + } +} diff --git a/packages/horizon/test/staking/delegation/delegate.t.sol b/packages/horizon/test/staking/delegation/delegate.t.sol new file mode 100644 index 000000000..463bc7b32 --- /dev/null +++ b/packages/horizon/test/staking/delegation/delegate.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingDelegateTest is HorizonStakingTest { + /* + * TESTS + */ + + function testDelegate_Tokens( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {} + + function testDelegate_Tokens_WhenThawing( + uint256 amount, + uint256 delegationAmount, + uint256 undelegateAmount + ) public useIndexer useProvision(amount, 0, 1 days) { + amount = bound(amount, 1 ether, MAX_STAKING_TOKENS); + // there is a min delegation amount of 1 ether after undelegating so we start with 1 ether + 1 wei + delegationAmount = bound(delegationAmount, 1 ether + 1 wei, MAX_STAKING_TOKENS); + + vm.startPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + undelegateAmount = bound(undelegateAmount, 1 wei, delegation.shares - 1 ether); + _undelegate(users.indexer, subgraphDataServiceAddress, undelegateAmount); + + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + } + + function testDelegate_Tokens_WhenAllThawing( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 1 days) { + delegationAmount = bound(delegationAmount, 1 ether, MAX_STAKING_TOKENS); + + vm.startPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); + + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + } + + function testDelegate_RevertWhen_ZeroTokens(uint256 amount) public useIndexer useProvision(amount, 0, 0) { + vm.startPrank(users.delegator); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.delegate(users.indexer, subgraphDataServiceAddress, 0, 0); + } + + function testDelegate_RevertWhen_UnderMinDelegation( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) { + delegationAmount = bound(delegationAmount, 1, MIN_DELEGATION - 1); + vm.startPrank(users.delegator); + token.approve(address(staking), delegationAmount); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInsufficientDelegationTokens.selector, + delegationAmount, + MIN_DELEGATION + ); + vm.expectRevert(expectedError); + staking.delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + } + + function testDelegate_LegacySubgraphService(uint256 amount, uint256 delegationAmount) public useIndexer { + amount = bound(amount, 1 ether, MAX_STAKING_TOKENS); + delegationAmount = bound(delegationAmount, MIN_DELEGATION, MAX_STAKING_TOKENS); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, amount, 0, 0); + + resetPrank(users.delegator); + _delegate(users.indexer, delegationAmount); + } + + function testDelegate_RevertWhen_InvalidPool( + uint256 tokens, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing { + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // slash entire provision + pool + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // attempt to delegate to a pool on invalid state, should revert + resetPrank(users.delegator); + token.approve(address(staking), delegationTokens); + vm.expectRevert( + abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidDelegationPoolState.selector, + users.indexer, + subgraphDataServiceAddress + ) + ); + staking.delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + } + + function testDelegate_RevertWhen_ThawingShares_InvalidPool( + uint256 tokens, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing { + delegationTokens = bound(delegationTokens, MIN_DELEGATION * 2, MAX_STAKING_TOKENS); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // undelegate some shares but not all + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares / 2); + + // slash entire provision + pool + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // attempt to delegate to a pool on invalid state, should revert + resetPrank(users.delegator); + token.approve(address(staking), delegationTokens); + vm.expectRevert( + abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidDelegationPoolState.selector, + users.indexer, + subgraphDataServiceAddress + ) + ); + staking.delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + } + + function testDelegate_AfterRecoveringPool( + uint256 tokens, + uint256 delegationTokens, + uint256 recoverAmount + ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing { + recoverAmount = bound(recoverAmount, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + + // create delegation pool + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // slash entire provision + pool + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // recover pool by adding tokens + resetPrank(users.indexer); + token.approve(address(staking), recoverAmount); + _addToDelegationPool(users.indexer, subgraphDataServiceAddress, recoverAmount); + + // delegate to pool - should be allowed now + vm.assume(delegationTokens >= recoverAmount); // to avoid getting issued 0 shares + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + } + + function testDelegate_RevertWhen_ProvisionNotCreated(uint256 delegationAmount) public { + delegationAmount = bound(delegationAmount, MIN_DELEGATION, MAX_STAKING_TOKENS); + + vm.startPrank(users.delegator); + token.approve(address(staking), delegationAmount); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidProvision.selector, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + } +} diff --git a/packages/horizon/test/staking/delegation/legacyWithdraw.t.sol b/packages/horizon/test/staking/delegation/legacyWithdraw.t.sol new file mode 100644 index 000000000..89b156085 --- /dev/null +++ b/packages/horizon/test/staking/delegation/legacyWithdraw.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol"; +import { LinkedList } from "../../../contracts/libraries/LinkedList.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingLegacyWithdrawDelegationTest is HorizonStakingTest { + /* + * MODIFIERS + */ + + modifier useDelegator() { + resetPrank(users.delegator); + _; + } + + /* + * HELPERS + */ + + function _setLegacyDelegation( + address _indexer, + address _delegator, + uint256 _shares, + uint256 __DEPRECATED_tokensLocked, + uint256 __DEPRECATED_tokensLockedUntil + ) public { + // Calculate the base storage slot for the serviceProvider in the mapping + bytes32 baseSlot = keccak256(abi.encode(_indexer, uint256(20))); + + // Calculate the slot for the delegator's DelegationInternal struct + bytes32 delegatorSlot = keccak256(abi.encode(_delegator, bytes32(uint256(baseSlot) + 4))); + + // Use vm.store to set each field of the struct + vm.store(address(staking), bytes32(uint256(delegatorSlot)), bytes32(_shares)); + vm.store(address(staking), bytes32(uint256(delegatorSlot) + 1), bytes32(__DEPRECATED_tokensLocked)); + vm.store(address(staking), bytes32(uint256(delegatorSlot) + 2), bytes32(__DEPRECATED_tokensLockedUntil)); + } + + /* + * ACTIONS + */ + + function _legacyWithdrawDelegated(address _indexer) internal { + (, address delegator, ) = vm.readCallers(); + IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool( + _indexer, + subgraphDataServiceLegacyAddress + ); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + uint256 beforeDelegatorBalance = token.balanceOf(users.delegator); + + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.StakeDelegatedWithdrawn(_indexer, delegator, pool.tokens); + staking.withdrawDelegated(users.indexer, address(0)); + + uint256 afterStakingBalance = token.balanceOf(address(staking)); + uint256 afterDelegatorBalance = token.balanceOf(users.delegator); + + assertEq(afterStakingBalance, beforeStakingBalance - pool.tokens); + assertEq(afterDelegatorBalance - pool.tokens, beforeDelegatorBalance); + + DelegationInternal memory delegation = _getStorage_Delegation( + _indexer, + subgraphDataServiceLegacyAddress, + delegator, + true + ); + assertEq(delegation.shares, 0); + assertEq(delegation.__DEPRECATED_tokensLocked, 0); + assertEq(delegation.__DEPRECATED_tokensLockedUntil, 0); + } + + /* + * TESTS + */ + + function testWithdraw_Legacy(uint256 tokensLocked) public useDelegator { + vm.assume(tokensLocked > 0); + + _setStorage_DelegationPool(users.indexer, tokensLocked, 0, 0); + _setLegacyDelegation(users.indexer, users.delegator, 0, tokensLocked, 1); + token.transfer(address(staking), tokensLocked); + + _legacyWithdrawDelegated(users.indexer); + } + + function testWithdraw_Legacy_RevertWhen_NoTokens() public useDelegator { + _setStorage_DelegationPool(users.indexer, 0, 0, 0); + _setLegacyDelegation(users.indexer, users.delegator, 0, 0, 0); + + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNothingToWithdraw()"); + vm.expectRevert(expectedError); + staking.withdrawDelegated(users.indexer, address(0)); + } +} diff --git a/packages/horizon/test/staking/delegation/redelegate.t.sol b/packages/horizon/test/staking/delegation/redelegate.t.sol new file mode 100644 index 000000000..a4d9c8e03 --- /dev/null +++ b/packages/horizon/test/staking/delegation/redelegate.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { + /* + * HELPERS + */ + + function _setupNewIndexer(uint256 tokens) private returns (address) { + (, address msgSender, ) = vm.readCallers(); + + address newIndexer = createUser("newIndexer"); + vm.startPrank(newIndexer); + _createProvision(newIndexer, subgraphDataServiceAddress, tokens, 0, MAX_THAWING_PERIOD); + + vm.startPrank(msgSender); + return newIndexer; + } + + function _setupNewIndexerAndVerifier(uint256 tokens) private returns (address, address) { + (, address msgSender, ) = vm.readCallers(); + + address newIndexer = createUser("newIndexer"); + address newVerifier = makeAddr("newVerifier"); + vm.startPrank(newIndexer); + _createProvision(newIndexer, newVerifier, tokens, 0, MAX_THAWING_PERIOD); + + vm.startPrank(msgSender); + return (newIndexer, newVerifier); + } + + /* + * TESTS + */ + + function testRedelegate_MoveToNewServiceProvider( + uint256 delegationAmount, + uint256 withdrawShares + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(withdrawShares) + { + skip(MAX_THAWING_PERIOD + 1); + + // Setup new service provider + address newIndexer = _setupNewIndexer(10_000_000 ether); + _redelegate(users.indexer, subgraphDataServiceAddress, newIndexer, subgraphDataServiceAddress, 0, 0); + } + + function testRedelegate_MoveToNewServiceProviderAndNewVerifier( + uint256 delegationAmount, + uint256 withdrawShares + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(withdrawShares) + { + skip(MAX_THAWING_PERIOD + 1); + + // Setup new service provider + (address newIndexer, address newVerifier) = _setupNewIndexerAndVerifier(10_000_000 ether); + _redelegate(users.indexer, subgraphDataServiceAddress, newIndexer, newVerifier, 0, 0); + } + + function testRedelegate_RevertWhen_VerifierZeroAddress( + uint256 delegationAmount + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(delegationAmount) + { + skip(MAX_THAWING_PERIOD + 1); + + // Setup new service provider + address newIndexer = _setupNewIndexer(10_000_000 ether); + vm.expectRevert(abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidVerifierZeroAddress.selector)); + staking.redelegate(users.indexer, subgraphDataServiceAddress, newIndexer, address(0), 0, 0); + } + + function testRedelegate_RevertWhen_ServiceProviderZeroAddress( + uint256 delegationAmount + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(delegationAmount) + { + skip(MAX_THAWING_PERIOD + 1); + + // Setup new verifier + address newVerifier = makeAddr("newVerifier"); + vm.expectRevert( + abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidServiceProviderZeroAddress.selector) + ); + staking.redelegate(users.indexer, subgraphDataServiceAddress, address(0), newVerifier, 0, 0); + } + + function testRedelegate_MoveZeroTokensToNewServiceProviderAndVerifier( + uint256 delegationAmount, + uint256 withdrawShares + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(withdrawShares) + { + // Setup new service provider + (address newIndexer, address newVerifier) = _setupNewIndexerAndVerifier(10_000_000 ether); + + uint256 previousBalance = token.balanceOf(users.delegator); + _redelegate(users.indexer, subgraphDataServiceAddress, newIndexer, newVerifier, 0, 0); + + uint256 newBalance = token.balanceOf(users.delegator); + assertEq(newBalance, previousBalance); + + uint256 delegatedTokens = staking.getDelegatedTokensAvailable(newIndexer, newVerifier); + assertEq(delegatedTokens, 0); + } +} diff --git a/packages/horizon/test/staking/delegation/undelegate.t.sol b/packages/horizon/test/staking/delegation/undelegate.t.sol new file mode 100644 index 000000000..54ad91cff --- /dev/null +++ b/packages/horizon/test/staking/delegation/undelegate.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingUndelegateTest is HorizonStakingTest { + /* + * TESTS + */ + + function testUndelegate_Tokens( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + resetPrank(users.delegator); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); + } + + function testMultipleUndelegate_Tokens( + uint256 amount, + uint256 delegationAmount, + uint256 undelegateSteps + ) public useIndexer useProvision(amount, 0, 0) { + undelegateSteps = bound(undelegateSteps, 1, 10); + delegationAmount = bound(delegationAmount, MIN_DELEGATION * undelegateSteps, MAX_STAKING_TOKENS); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + + uint256 undelegateAmount = delegation.shares / undelegateSteps; + for (uint i = 0; i < undelegateSteps - 1; i++) { + _undelegate(users.indexer, subgraphDataServiceAddress, undelegateAmount); + } + + delegation = _getStorage_Delegation(users.indexer, subgraphDataServiceAddress, users.delegator, false); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); + } + + function testUndelegate_RevertWhen_InsuficientTokens( + uint256 amount, + uint256 delegationAmount, + uint256 undelegateAmount + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + undelegateAmount = bound(undelegateAmount, 1, delegationAmount); + resetPrank(users.delegator); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + undelegateAmount = bound(undelegateAmount, delegation.shares - MIN_DELEGATION + 1, delegation.shares - 1); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInsufficientTokens.selector, + delegation.shares - undelegateAmount, + MIN_DELEGATION + ); + vm.expectRevert(expectedError); + staking.undelegate(users.indexer, subgraphDataServiceAddress, undelegateAmount); + } + + function testUndelegate_RevertWhen_TooManyUndelegations() + public + useIndexer + useProvision(1000 ether, 0, 0) + useDelegation(10000 ether) + { + resetPrank(users.delegator); + + for (uint i = 0; i < MAX_THAW_REQUESTS; i++) { + _undelegate(users.indexer, subgraphDataServiceAddress, 1 ether); + } + + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingTooManyThawRequests()"); + vm.expectRevert(expectedError); + staking.undelegate(users.indexer, subgraphDataServiceAddress, 1 ether); + } + + function testUndelegate_RevertWhen_ZeroShares( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + resetPrank(users.delegator); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroShares()"); + vm.expectRevert(expectedError); + staking.undelegate(users.indexer, subgraphDataServiceAddress, 0); + } + + function testUndelegate_RevertWhen_OverShares( + uint256 amount, + uint256 delegationAmount, + uint256 overDelegationShares + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + resetPrank(users.delegator); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + overDelegationShares = bound(overDelegationShares, delegation.shares + 1, MAX_STAKING_TOKENS + 1); + + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientShares(uint256,uint256)", + delegation.shares, + overDelegationShares + ); + vm.expectRevert(expectedError); + staking.undelegate(users.indexer, subgraphDataServiceAddress, overDelegationShares); + } + + function testUndelegate_LegacySubgraphService(uint256 amount, uint256 delegationAmount) public useIndexer { + amount = bound(amount, 1, MAX_STAKING_TOKENS); + delegationAmount = bound(delegationAmount, MIN_DELEGATION, MAX_STAKING_TOKENS); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, amount, 0, 0); + + resetPrank(users.delegator); + _delegate(users.indexer, delegationAmount); + + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + true + ); + _undelegate(users.indexer, delegation.shares); + } + + function testUndelegate_RevertWhen_InvalidPool( + uint256 tokens, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing { + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // slash all of the provision + delegation + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // attempt to undelegate - should revert + resetPrank(users.delegator); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + vm.expectRevert( + abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidDelegationPoolState.selector, + users.indexer, + subgraphDataServiceAddress + ) + ); + staking.undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); + } + + function testUndelegate_AfterRecoveringPool( + uint256 tokens, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing { + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + + // delegate + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // slash all of the provision + delegation + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // recover the delegation pool + resetPrank(users.indexer); + token.approve(address(staking), delegationTokens); + _addToDelegationPool(users.indexer, subgraphDataServiceAddress, delegationTokens); + + // undelegate -- should now work + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + resetPrank(users.delegator); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); + } + + function testUndelegate_ThawingShares_AfterRecoveringPool() + public + useIndexer + useProvision(MAX_STAKING_TOKENS, 0, 0) + useDelegationSlashing + { + uint256 delegationTokens = MAX_STAKING_TOKENS / 10; + + // delegate + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // undelegate half shares so we have some thawing shares/tokens + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + resetPrank(users.delegator); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares / 2); + + // slash all of the provision + delegation + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, MAX_STAKING_TOKENS + delegationTokens, 0); + + // recover the delegation pool + resetPrank(users.indexer); + token.approve(address(staking), delegationTokens); + _addToDelegationPool(users.indexer, subgraphDataServiceAddress, delegationTokens); + + // undelegate the rest + resetPrank(users.delegator); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares - delegation.shares / 2); + } +} diff --git a/packages/horizon/test/staking/delegation/withdraw.t.sol b/packages/horizon/test/staking/delegation/withdraw.t.sol new file mode 100644 index 000000000..bbf7ecd1b --- /dev/null +++ b/packages/horizon/test/staking/delegation/withdraw.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { LinkedList } from "../../../contracts/libraries/LinkedList.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { + /* + * TESTS + */ + + function testWithdrawDelegation_Tokens( + uint256 delegationAmount, + uint256 withdrawShares + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(withdrawShares) + { + LinkedList.List memory thawingRequests = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Delegation, + users.indexer, + subgraphDataServiceAddress, + users.delegator + ); + ThawRequest memory thawRequest = staking.getThawRequest( + IHorizonStakingTypes.ThawRequestType.Delegation, + thawingRequests.tail + ); + + skip(thawRequest.thawingUntil + 1); + + _withdrawDelegated(users.indexer, subgraphDataServiceAddress, 0); + } + + function testWithdrawDelegation_RevertWhen_NotThawing( + uint256 delegationAmount + ) public useIndexer useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) useDelegation(delegationAmount) { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNothingThawing()"); + vm.expectRevert(expectedError); + staking.withdrawDelegated(users.indexer, subgraphDataServiceAddress, 0); + } + + function testWithdrawDelegation_ZeroTokens( + uint256 delegationAmount + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(delegationAmount) + { + uint256 previousBalance = token.balanceOf(users.delegator); + _withdrawDelegated(users.indexer, subgraphDataServiceAddress, 0); + + // Nothing changed since thawing period hasn't finished + uint256 newBalance = token.balanceOf(users.delegator); + assertEq(newBalance, previousBalance); + } + + function testWithdrawDelegation_LegacySubgraphService(uint256 delegationAmount) public useIndexer { + delegationAmount = bound(delegationAmount, MIN_DELEGATION, MAX_STAKING_TOKENS); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, 10_000_000 ether, 0, MAX_THAWING_PERIOD); + + resetPrank(users.delegator); + _delegate(users.indexer, delegationAmount); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + true + ); + _undelegate(users.indexer, delegation.shares); + + LinkedList.List memory thawingRequests = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Delegation, + users.indexer, + subgraphDataServiceLegacyAddress, + users.delegator + ); + ThawRequest memory thawRequest = staking.getThawRequest( + IHorizonStakingTypes.ThawRequestType.Delegation, + thawingRequests.tail + ); + + skip(thawRequest.thawingUntil + 1); + + _withdrawDelegated(users.indexer, subgraphDataServiceLegacyAddress, 0); + } + + function testWithdrawDelegation_RevertWhen_InvalidPool( + uint256 tokens, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, 0, MAX_THAWING_PERIOD) useDelegationSlashing { + delegationTokens = bound(delegationTokens, MIN_DELEGATION * 2, MAX_STAKING_TOKENS); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // undelegate some shares + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares / 2); + + // slash all of the provision + delegation + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // fast forward in time and attempt to withdraw + skip(MAX_THAWING_PERIOD + 1); + resetPrank(users.delegator); + vm.expectRevert( + abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidDelegationPoolState.selector, + users.indexer, + subgraphDataServiceAddress + ) + ); + staking.withdrawDelegated(users.indexer, subgraphDataServiceAddress, 0); + } + + function testWithdrawDelegation_AfterRecoveringPool( + uint256 tokens + ) public useIndexer useProvision(tokens, 0, MAX_THAWING_PERIOD) useDelegationSlashing { + uint256 delegationTokens = MAX_STAKING_TOKENS / 10; + + // delegate + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // undelegate some shares + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares / 2); + + // slash all of the provision + delegation + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + + // recover the delegation pool + resetPrank(users.indexer); + token.approve(address(staking), delegationTokens); + _addToDelegationPool(users.indexer, subgraphDataServiceAddress, delegationTokens); + + // fast forward in time and withdraw - this withdraw will net 0 tokens + skip(MAX_THAWING_PERIOD + 1); + resetPrank(users.delegator); + _withdrawDelegated(users.indexer, subgraphDataServiceAddress, 0); + } +} diff --git a/packages/horizon/test/staking/governance/governance.t.sol b/packages/horizon/test/staking/governance/governance.t.sol new file mode 100644 index 000000000..2fe4a46da --- /dev/null +++ b/packages/horizon/test/staking/governance/governance.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingGovernanceTest is HorizonStakingTest { + /* + * MODIFIERS + */ + + modifier useGovernor() { + vm.startPrank(users.governor); + _; + } + + /* + * TESTS + */ + + function testGovernance_SetAllowedLockedVerifier() public useGovernor { + _setAllowedLockedVerifier(subgraphDataServiceAddress, true); + } + + function testGovernance_RevertWhen_SetAllowedLockedVerifier_NotGovernor() public useIndexer { + bytes memory expectedError = abi.encodeWithSignature("ManagedOnlyGovernor()"); + vm.expectRevert(expectedError); + staking.setAllowedLockedVerifier(subgraphDataServiceAddress, true); + } + + function testGovernance_SetDelgationSlashingEnabled() public useGovernor { + _setDelegationSlashingEnabled(); + } + + function testGovernance_SetDelgationSlashing_NotGovernor() public useIndexer { + bytes memory expectedError = abi.encodeWithSignature("ManagedOnlyGovernor()"); + vm.expectRevert(expectedError); + staking.setDelegationSlashingEnabled(); + } + + function testGovernance_ClearThawingPeriod(uint32 thawingPeriod) public useGovernor { + // simulate previous thawing period + _setStorage_DeprecatedThawingPeriod(thawingPeriod); + + _clearThawingPeriod(); + } + + function testGovernance_ClearThawingPeriod_NotGovernor() public useIndexer { + bytes memory expectedError = abi.encodeWithSignature("ManagedOnlyGovernor()"); + vm.expectRevert(expectedError); + staking.clearThawingPeriod(); + } + + function testGovernance__SetMaxThawingPeriod(uint64 maxThawingPeriod) public useGovernor { + _setMaxThawingPeriod(maxThawingPeriod); + } + + function testGovernance__SetMaxThawingPeriod_NotGovernor() public useIndexer { + bytes memory expectedError = abi.encodeWithSignature("ManagedOnlyGovernor()"); + vm.expectRevert(expectedError); + staking.setMaxThawingPeriod(MAX_THAWING_PERIOD); + } +} diff --git a/packages/horizon/test/staking/operator/locked.t.sol b/packages/horizon/test/staking/operator/locked.t.sol new file mode 100644 index 000000000..0568e8cb3 --- /dev/null +++ b/packages/horizon/test/staking/operator/locked.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingOperatorLockedTest is HorizonStakingTest { + /* + * TESTS + */ + + function testOperatorLocked_Set() public useIndexer useLockedVerifier(subgraphDataServiceAddress) { + _setOperatorLocked(subgraphDataServiceAddress, users.operator, true); + } + + function testOperatorLocked_RevertWhen_VerifierNotAllowed() public useIndexer { + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingVerifierNotAllowed(address)", + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.setOperatorLocked(subgraphDataServiceAddress, users.operator, true); + } + + function testOperatorLocked_RevertWhen_CallerIsServiceProvider() + public + useIndexer + useLockedVerifier(subgraphDataServiceAddress) + { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingCallerIsServiceProvider()"); + vm.expectRevert(expectedError); + staking.setOperatorLocked(subgraphDataServiceAddress, users.indexer, true); + } + + function testOperatorLocked_SetLegacySubgraphService() + public + useIndexer + useLockedVerifier(subgraphDataServiceLegacyAddress) + { + _setOperatorLocked(subgraphDataServiceLegacyAddress, users.operator, true); + } +} diff --git a/packages/horizon/test/staking/operator/operator.t.sol b/packages/horizon/test/staking/operator/operator.t.sol new file mode 100644 index 000000000..664414047 --- /dev/null +++ b/packages/horizon/test/staking/operator/operator.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingOperatorTest is HorizonStakingTest { + /* + * TESTS + */ + + function testOperator_SetOperator() public useIndexer { + _setOperator(subgraphDataServiceAddress, users.operator, true); + } + + function testOperator_RevertWhen_CallerIsServiceProvider() public useIndexer { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingCallerIsServiceProvider()"); + vm.expectRevert(expectedError); + staking.setOperator(subgraphDataServiceAddress, users.indexer, true); + } + + function testOperator_RemoveOperator() public useIndexer { + _setOperator(subgraphDataServiceAddress, users.operator, true); + _setOperator(subgraphDataServiceAddress, users.operator, false); + } +} diff --git a/packages/horizon/test/staking/provision/deprovision.t.sol b/packages/horizon/test/staking/provision/deprovision.t.sol new file mode 100644 index 000000000..4fa97da6c --- /dev/null +++ b/packages/horizon/test/staking/provision/deprovision.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingDeprovisionTest is HorizonStakingTest { + /* + * TESTS + */ + + function testDeprovision_AllRequests( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod, + uint256 thawCount, + uint256 deprovisionCount + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + thawCount = bound(thawCount, 1, 100); + deprovisionCount = bound(deprovisionCount, 0, thawCount); + vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step + uint256 individualThawAmount = amount / thawCount; + + for (uint i = 0; i < thawCount; i++) { + _thaw(users.indexer, subgraphDataServiceAddress, individualThawAmount); + } + + skip(thawingPeriod + 1); + + _deprovision(users.indexer, subgraphDataServiceAddress, deprovisionCount); + } + + function testDeprovision_ThawedRequests( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod, + uint256 thawCount + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + thawCount = bound(thawCount, 2, 100); + vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step + uint256 individualThawAmount = amount / thawCount; + + for (uint i = 0; i < thawCount / 2; i++) { + _thaw(users.indexer, subgraphDataServiceAddress, individualThawAmount); + } + + skip(thawingPeriod + 1); + + for (uint i = 0; i < thawCount / 2; i++) { + _thaw(users.indexer, subgraphDataServiceAddress, individualThawAmount); + } + + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + } + + function testDeprovision_OperatorMovingTokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useOperator { + _thaw(users.indexer, subgraphDataServiceAddress, amount); + skip(thawingPeriod + 1); + + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + } + + function testDeprovision_RevertWhen_OperatorNotAuthorized( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + _thaw(users.indexer, subgraphDataServiceAddress, amount); + + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.indexer, + subgraphDataServiceAddress, + users.operator + ); + vm.expectRevert(expectedError); + staking.deprovision(users.indexer, subgraphDataServiceAddress, 0); + } + + function testDeprovision_RevertWhen_NoThawingTokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNothingThawing()"); + vm.expectRevert(expectedError); + staking.deprovision(users.indexer, subgraphDataServiceAddress, 0); + } + + function testDeprovision_StillThawing( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + vm.assume(thawingPeriod > 0); + + _thaw(users.indexer, subgraphDataServiceAddress, amount); + + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + } + + function testDeprovision_AfterProvisionFullySlashed( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + // thaw some funds so there are some shares and tokens thawing + thawAmount = bound(thawAmount, 1, amount); + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + + // slash all of it + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, amount, 0); + + // now deprovision + resetPrank(users.indexer); + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + } + + function testDeprovision_AfterResetingThawingPool( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + // thaw some funds so there are some shares and tokens thawing + thawAmount = bound(thawAmount, 1, amount); + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + + // slash all of it + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, amount, 0); + + // put some funds back in + resetPrank(users.indexer); + _stake(amount); + _addToProvision(users.indexer, subgraphDataServiceAddress, amount); + + // thaw some funds again + resetPrank(users.indexer); + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + + // now deprovision + resetPrank(users.indexer); + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + } +} diff --git a/packages/horizon/test/staking/provision/locked.t.sol b/packages/horizon/test/staking/provision/locked.t.sol new file mode 100644 index 000000000..bc44a32f1 --- /dev/null +++ b/packages/horizon/test/staking/provision/locked.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingProvisionLockedTest is HorizonStakingTest { + /* + * TESTS + */ + + function testProvisionLocked_Create( + uint256 amount + ) public useIndexer useStake(amount) useLockedVerifier(subgraphDataServiceAddress) { + uint256 provisionTokens = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + assertEq(provisionTokens, 0); + + _setOperatorLocked(subgraphDataServiceAddress, users.operator, true); + + vm.startPrank(users.operator); + _provisionLocked(users.indexer, subgraphDataServiceAddress, amount, MAX_PPM, MAX_THAWING_PERIOD); + + provisionTokens = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + assertEq(provisionTokens, amount); + } + + function testProvisionLocked_RevertWhen_VerifierNotAllowed( + uint256 amount + ) public useIndexer useStake(amount) useLockedVerifier(subgraphDataServiceAddress) { + uint256 provisionTokens = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + assertEq(provisionTokens, 0); + + // Set operator + _setOperatorLocked(subgraphDataServiceAddress, users.operator, true); + + // Disable locked verifier + vm.startPrank(users.governor); + _setAllowedLockedVerifier(subgraphDataServiceAddress, false); + + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingVerifierNotAllowed(address)", + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.provisionLocked(users.indexer, subgraphDataServiceAddress, amount, MAX_PPM, MAX_THAWING_PERIOD); + } + + function testProvisionLocked_RevertWhen_OperatorNotAllowed( + uint256 amount + ) public useIndexer useStake(amount) useLockedVerifier(subgraphDataServiceAddress) { + uint256 provisionTokens = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + assertEq(provisionTokens, 0); + + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.indexer, + subgraphDataServiceAddress, + users.operator + ); + vm.expectRevert(expectedError); + staking.provisionLocked(users.indexer, subgraphDataServiceAddress, amount, MAX_PPM, MAX_THAWING_PERIOD); + } +} diff --git a/packages/horizon/test/staking/provision/parameters.t.sol b/packages/horizon/test/staking/provision/parameters.t.sol new file mode 100644 index 000000000..6e0b4fabf --- /dev/null +++ b/packages/horizon/test/staking/provision/parameters.t.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; + +contract HorizonStakingProvisionParametersTest is HorizonStakingTest { + /* + * MODIFIERS + */ + + modifier useValidParameters(uint32 maxVerifierCut, uint64 thawingPeriod) { + vm.assume(maxVerifierCut <= MAX_PPM); + vm.assume(thawingPeriod <= MAX_THAWING_PERIOD); + _; + } + + /* + * TESTS + */ + + function test_ProvisionParametersSet( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, 0, 0) useValidParameters(maxVerifierCut, thawingPeriod) { + _setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + } + + function test_ProvisionParametersSet_RevertWhen_ProvisionNotExists( + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useValidParameters(maxVerifierCut, thawingPeriod) { + vm.expectRevert( + abi.encodeWithSignature( + "HorizonStakingInvalidProvision(address,address)", + users.indexer, + subgraphDataServiceAddress + ) + ); + staking.setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + } + + function test_ProvisionParametersSet_RevertWhen_CallerNotAuthorized( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + vm.startPrank(msg.sender); // stop impersonating the indexer + vm.expectRevert( + abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.indexer, + subgraphDataServiceAddress, + msg.sender + ) + ); + staking.setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + vm.stopPrank(); + } + + function test_ProvisionParametersSet_RevertWhen_ProtocolPaused( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) usePausedStaking { + vm.expectRevert(abi.encodeWithSignature("ManagedIsPaused()")); + staking.setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + } + + function test_ProvisionParametersAccept( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + _setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + + vm.startPrank(subgraphDataServiceAddress); + _acceptProvisionParameters(users.indexer); + vm.stopPrank(); + } + + function test_ProvisionParametersAccept_SameParameters( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + _setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + + vm.startPrank(subgraphDataServiceAddress); + _acceptProvisionParameters(users.indexer); + _acceptProvisionParameters(users.indexer); + vm.stopPrank(); + } + + function test_ProvisionParameters_RevertIf_InvalidMaxVerifierCut( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + maxVerifierCut = uint32(bound(maxVerifierCut, MAX_PPM + 1, type(uint32).max)); + vm.assume(thawingPeriod <= MAX_THAWING_PERIOD); + vm.expectRevert( + abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidMaxVerifierCut.selector, maxVerifierCut) + ); + staking.setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + } + + function test_ProvisionParameters_RevertIf_InvalidThawingPeriod( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + vm.assume(maxVerifierCut <= MAX_PPM); + thawingPeriod = uint64(bound(thawingPeriod, MAX_THAWING_PERIOD + 1, type(uint64).max)); + vm.expectRevert( + abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidThawingPeriod.selector, + thawingPeriod, + MAX_THAWING_PERIOD + ) + ); + staking.setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + } + + function test_ProvisionParametersAccept_RevertWhen_ProvisionNotExists() public useIndexer { + resetPrank(subgraphDataServiceAddress); + vm.expectRevert( + abi.encodeWithSignature( + "HorizonStakingInvalidProvision(address,address)", + users.indexer, + subgraphDataServiceAddress + ) + ); + staking.acceptProvisionParameters(users.indexer); + } +} diff --git a/packages/horizon/test/staking/provision/provision.t.sol b/packages/horizon/test/staking/provision/provision.t.sol new file mode 100644 index 000000000..ba580be25 --- /dev/null +++ b/packages/horizon/test/staking/provision/provision.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingProvisionTest is HorizonStakingTest { + /* + * TESTS + */ + + function testProvision_Create(uint256 tokens, uint32 maxVerifierCut, uint64 thawingPeriod) public useIndexer { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + maxVerifierCut = uint32(bound(maxVerifierCut, 0, MAX_PPM)); + thawingPeriod = uint32(bound(thawingPeriod, 0, MAX_THAWING_PERIOD)); + + _createProvision(users.indexer, subgraphDataServiceAddress, tokens, maxVerifierCut, thawingPeriod); + } + + function testProvision_RevertWhen_ZeroTokens() public useIndexer useStake(1000 ether) { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, 0, 0, 0); + } + + function testProvision_RevertWhen_MaxVerifierCutTooHigh( + uint256 amount, + uint32 maxVerifierCut + ) public useIndexer useStake(amount) { + vm.assume(maxVerifierCut > MAX_PPM); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInvalidMaxVerifierCut(uint32)", + maxVerifierCut + ); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount, maxVerifierCut, 0); + } + + function testProvision_RevertWhen_ThawingPeriodTooHigh( + uint256 amount, + uint64 thawingPeriod + ) public useIndexer useStake(amount) { + vm.assume(thawingPeriod > MAX_THAWING_PERIOD); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInvalidThawingPeriod(uint64,uint64)", + thawingPeriod, + MAX_THAWING_PERIOD + ); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount, 0, thawingPeriod); + } + + function testProvision_RevertWhen_ThereIsNoIdleStake( + uint256 amount, + uint256 provisionTokens + ) public useIndexer useStake(amount) { + vm.assume(provisionTokens > amount); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientIdleStake(uint256,uint256)", + provisionTokens, + amount + ); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, provisionTokens, 0, 0); + } + + function testProvision_RevertWhen_AlreadyExists( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount / 2, maxVerifierCut, thawingPeriod) { + resetPrank(users.indexer); + + token.approve(address(staking), amount / 2); + _stake(amount / 2); + + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingProvisionAlreadyExists()"); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount / 2, maxVerifierCut, thawingPeriod); + } + + function testProvision_OperatorAddTokensToProvision( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod, + uint256 tokensToAdd + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useOperator { + tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS); + + // Add more tokens to the provision + _stakeTo(users.indexer, tokensToAdd); + _addToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd); + } + + function testProvision_RevertWhen_OperatorNotAuthorized( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.indexer, + subgraphDataServiceAddress, + users.operator + ); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount, maxVerifierCut, thawingPeriod); + } + + function testProvision_RevertWhen_VerifierIsNotSubgraphDataServiceDuringTransitionPeriod( + uint256 amount + ) public useIndexer useStake(amount) { + // simulate the transition period + _setStorage_DeprecatedThawingPeriod(THAWING_PERIOD_IN_BLOCKS); + + // oddly we use subgraphDataServiceLegacyAddress as the subgraph service address + // so subgraphDataServiceAddress is not the subgraph service ¯\_(ツ)_/¯ + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInvalidVerifier(address)", + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount, 0, 0); + } +} diff --git a/packages/horizon/test/staking/provision/reprovision.t.sol b/packages/horizon/test/staking/provision/reprovision.t.sol new file mode 100644 index 000000000..be650019f --- /dev/null +++ b/packages/horizon/test/staking/provision/reprovision.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingReprovisionTest is HorizonStakingTest { + /* + * VARIABLES + */ + + address private newDataService = makeAddr("newDataService"); + + /* + * TESTS + */ + + function testReprovision_MovingTokens( + uint64 thawingPeriod, + uint256 provisionAmount + ) public useIndexer useProvision(provisionAmount, 0, thawingPeriod) { + _thaw(users.indexer, subgraphDataServiceAddress, provisionAmount); + skip(thawingPeriod + 1); + + _createProvision(users.indexer, newDataService, 1 ether, 0, thawingPeriod); + + _reprovision(users.indexer, subgraphDataServiceAddress, newDataService, 0); + } + + function testReprovision_OperatorMovingTokens( + uint64 thawingPeriod, + uint256 provisionAmount + ) public useOperator useProvision(provisionAmount, 0, thawingPeriod) { + _thaw(users.indexer, subgraphDataServiceAddress, provisionAmount); + skip(thawingPeriod + 1); + + // Switch to indexer to set operator for new data service + vm.startPrank(users.indexer); + _setOperator(newDataService, users.operator, true); + + // Switch back to operator + vm.startPrank(users.operator); + _createProvision(users.indexer, newDataService, 1 ether, 0, thawingPeriod); + _reprovision(users.indexer, subgraphDataServiceAddress, newDataService, 0); + } + + function testReprovision_RevertWhen_OperatorNotAuthorizedForNewDataService( + uint256 provisionAmount + ) public useOperator useProvision(provisionAmount, 0, 0) { + _thaw(users.indexer, subgraphDataServiceAddress, provisionAmount); + + // Switch to indexer to create new provision + vm.startPrank(users.indexer); + _createProvision(users.indexer, newDataService, 1 ether, 0, 0); + + // Switch back to operator + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.indexer, + newDataService, + users.operator + ); + vm.expectRevert(expectedError); + staking.reprovision(users.indexer, subgraphDataServiceAddress, newDataService, 0); + } + + function testReprovision_RevertWhen_NoThawingTokens(uint256 amount) public useIndexer useProvision(amount, 0, 0) { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNothingThawing()"); + vm.expectRevert(expectedError); + staking.reprovision(users.indexer, subgraphDataServiceAddress, newDataService, 0); + } +} diff --git a/packages/horizon/test/staking/provision/thaw.t.sol b/packages/horizon/test/staking/provision/thaw.t.sol new file mode 100644 index 000000000..a16dd7253 --- /dev/null +++ b/packages/horizon/test/staking/provision/thaw.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingThawTest is HorizonStakingTest { + /* + * TESTS + */ + + function testThaw_Tokens( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + thawAmount = bound(thawAmount, 1, amount); + + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + function testThaw_MultipleRequests( + uint256 amount, + uint64 thawingPeriod, + uint256 thawCount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + thawCount = bound(thawCount, 1, 100); + vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step + uint256 individualThawAmount = amount / thawCount; + + for (uint i = 0; i < thawCount; i++) { + _thaw(users.indexer, subgraphDataServiceAddress, individualThawAmount); + } + } + + function testThaw_OperatorCanStartThawing( + uint256 amount, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, 0, thawingPeriod) useOperator { + _thaw(users.indexer, subgraphDataServiceAddress, amount); + } + + function testThaw_RevertWhen_OperatorNotAuthorized( + uint256 amount, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.indexer, + subgraphDataServiceAddress, + users.operator + ); + vm.expectRevert(expectedError); + staking.thaw(users.indexer, subgraphDataServiceAddress, amount); + } + + function testThaw_RevertWhen_InsufficientTokensAvailable( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + vm.assume(thawAmount > amount); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientTokens(uint256,uint256)", + amount, + thawAmount + ); + vm.expectRevert(expectedError); + staking.thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + function testThaw_RevertWhen_OverMaxThawRequests() public useIndexer useProvision(10000 ether, 0, 0) { + uint256 thawAmount = 1 ether; + + for (uint256 i = 0; i < MAX_THAW_REQUESTS; i++) { + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingTooManyThawRequests()"); + vm.expectRevert(expectedError); + staking.thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + function testThaw_RevertWhen_ThawingZeroTokens( + uint256 amount, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + uint256 thawAmount = 0 ether; + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + function testThaw_RevertWhen_ProvisionFullySlashed( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + thawAmount = bound(thawAmount, 1, amount); + + // slash all of it + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, amount, 0); + + // Attempt to thaw on a provision that has been fully slashed + resetPrank(users.indexer); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientTokens(uint256,uint256)", + 0, + thawAmount + ); + vm.expectRevert(expectedError); + staking.thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + function testThaw_AfterResetingThawingPool( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + // thaw some funds so there are some shares thawing and tokens thawing + thawAmount = bound(thawAmount, 1, amount); + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + + // slash all of it + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, amount, 0); + + // put some funds back in + resetPrank(users.indexer); + _stake(amount); + _addToProvision(users.indexer, subgraphDataServiceAddress, amount); + + // we should be able to thaw again + resetPrank(users.indexer); + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + function testThaw_GetThawedTokens( + uint256 amount, + uint64 thawingPeriod, + uint256 thawSteps + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + thawSteps = bound(thawSteps, 1, 10); + + uint256 thawAmount = amount / thawSteps; + vm.assume(thawAmount > 0); + for (uint256 i = 0; i < thawSteps; i++) { + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + skip(thawingPeriod + 1); + + uint256 thawedTokens = staking.getThawedTokens( + ThawRequestType.Provision, + users.indexer, + subgraphDataServiceAddress, + users.indexer + ); + vm.assertEq(thawedTokens, thawAmount * thawSteps); + } +} diff --git a/packages/horizon/test/staking/serviceProvider/serviceProvider.t.sol b/packages/horizon/test/staking/serviceProvider/serviceProvider.t.sol new file mode 100644 index 000000000..6c5fe4904 --- /dev/null +++ b/packages/horizon/test/staking/serviceProvider/serviceProvider.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingServiceProviderTest is HorizonStakingTest { + /* + * TESTS + */ + + function testServiceProvider_GetProvider( + uint256 amount, + uint256 operatorAmount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + operatorAmount = bound(operatorAmount, 1, MAX_STAKING_TOKENS); + ServiceProvider memory sp = staking.getServiceProvider(users.indexer); + assertEq(sp.tokensStaked, amount); + assertEq(sp.tokensProvisioned, amount); + + _setOperator(subgraphDataServiceAddress, users.operator, true); + resetPrank(users.operator); + _stakeTo(users.indexer, operatorAmount); + sp = staking.getServiceProvider(users.indexer); + assertEq(sp.tokensStaked, amount + operatorAmount); + assertEq(sp.tokensProvisioned, amount); + } + + function testServiceProvider_SetDelegationFeeCut(uint256 feeCut, uint8 paymentTypeInput) public useIndexer { + vm.assume(paymentTypeInput < 3); + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes(paymentTypeInput); + feeCut = bound(feeCut, 0, MAX_PPM); + _setDelegationFeeCut(users.indexer, subgraphDataServiceAddress, paymentType, feeCut); + } + + function testServiceProvider_GetProvision( + uint256 amount, + uint256 thawAmount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + thawAmount = bound(thawAmount, 1, amount); + Provision memory p = staking.getProvision(users.indexer, subgraphDataServiceAddress); + assertEq(p.tokens, amount); + assertEq(p.tokensThawing, 0); + assertEq(p.sharesThawing, 0); + assertEq(p.maxVerifierCut, maxVerifierCut); + assertEq(p.thawingPeriod, thawingPeriod); + assertEq(p.createdAt, block.timestamp); + assertEq(p.maxVerifierCutPending, maxVerifierCut); + assertEq(p.thawingPeriodPending, thawingPeriod); + + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + p = staking.getProvision(users.indexer, subgraphDataServiceAddress); + assertEq(p.tokensThawing, thawAmount); + } + + function testServiceProvider_GetTokensAvailable( + uint256 amount, + uint256 thawAmount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + thawAmount = bound(thawAmount, 1, amount); + uint256 tokensAvailable = staking.getTokensAvailable(users.indexer, subgraphDataServiceAddress, 0); + assertEq(tokensAvailable, amount); + + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + tokensAvailable = staking.getTokensAvailable(users.indexer, subgraphDataServiceAddress, 0); + assertEq(tokensAvailable, amount - thawAmount); + } + + function testServiceProvider_GetTokensAvailable_WithDelegation( + uint256 amount, + uint256 delegationAmount, + uint32 delegationRatio + ) public useIndexer useProvision(amount, MAX_PPM, MAX_THAWING_PERIOD) useDelegation(delegationAmount) { + uint256 tokensAvailable = staking.getTokensAvailable( + users.indexer, + subgraphDataServiceAddress, + delegationRatio + ); + + uint256 tokensDelegatedMax = amount * (uint256(delegationRatio)); + uint256 tokensDelegatedCapacity = delegationAmount > tokensDelegatedMax ? tokensDelegatedMax : delegationAmount; + assertEq(tokensAvailable, amount + tokensDelegatedCapacity); + } + + function testServiceProvider_GetProviderTokensAvailable( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, MAX_PPM, MAX_THAWING_PERIOD) useDelegation(delegationAmount) { + uint256 providerTokensAvailable = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + // Should not include delegated tokens + assertEq(providerTokensAvailable, amount); + } + + function testServiceProvider_HasStake( + uint256 amount + ) public useIndexer useProvision(amount, MAX_PPM, MAX_THAWING_PERIOD) { + assertTrue(staking.hasStake(users.indexer)); + + _thaw(users.indexer, subgraphDataServiceAddress, amount); + skip(MAX_THAWING_PERIOD + 1); + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + staking.unstake(amount); + + assertFalse(staking.hasStake(users.indexer)); + } + + function testServiceProvider_GetIndexerStakedTokens( + uint256 amount + ) public useIndexer useProvision(amount, MAX_PPM, MAX_THAWING_PERIOD) { + assertEq(staking.getIndexerStakedTokens(users.indexer), amount); + + _thaw(users.indexer, subgraphDataServiceAddress, amount); + // Does not discount thawing tokens + assertEq(staking.getIndexerStakedTokens(users.indexer), amount); + + skip(MAX_THAWING_PERIOD + 1); + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + // Does not discount thawing tokens + assertEq(staking.getIndexerStakedTokens(users.indexer), amount); + + staking.unstake(amount); + assertEq(staking.getIndexerStakedTokens(users.indexer), 0); + } + + function testServiceProvider_RevertIf_InvalidDelegationFeeCut( + uint256 cut, + uint8 paymentTypeInput + ) public useIndexer { + vm.assume(paymentTypeInput < 3); + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes(paymentTypeInput); + cut = bound(cut, MAX_PPM + 1, MAX_PPM + 100); + vm.expectRevert( + abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidDelegationFeeCut.selector, cut) + ); + staking.setDelegationFeeCut(users.indexer, subgraphDataServiceAddress, paymentType, cut); + } +} diff --git a/packages/horizon/test/staking/slash/legacySlash.t.sol b/packages/horizon/test/staking/slash/legacySlash.t.sol new file mode 100644 index 000000000..abb587404 --- /dev/null +++ b/packages/horizon/test/staking/slash/legacySlash.t.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingLegacySlashTest is HorizonStakingTest { + /* + * MODIFIERS + */ + + modifier useLegacySlasher(address slasher) { + bytes32 storageKey = keccak256(abi.encode(slasher, 18)); + vm.store(address(staking), storageKey, bytes32(uint256(1))); + _; + } + + /* + * HELPERS + */ + + function _setIndexer( + address _indexer, + uint256 _tokensStaked, + uint256 _tokensAllocated, + uint256 _tokensLocked, + uint256 _tokensLockedUntil + ) public { + bytes32 baseSlot = keccak256(abi.encode(_indexer, 14)); + + vm.store(address(staking), bytes32(uint256(baseSlot)), bytes32(_tokensStaked)); + vm.store(address(staking), bytes32(uint256(baseSlot) + 1), bytes32(_tokensAllocated)); + vm.store(address(staking), bytes32(uint256(baseSlot) + 2), bytes32(_tokensLocked)); + vm.store(address(staking), bytes32(uint256(baseSlot) + 3), bytes32(_tokensLockedUntil)); + } + + /* + * ACTIONS + */ + + function _legacySlash(address _indexer, uint256 _tokens, uint256 _rewards, address _beneficiary) internal { + // before + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + uint256 beforeRewardsDestinationBalance = token.balanceOf(_beneficiary); + + // slash + vm.expectEmit(address(staking)); + emit IHorizonStakingExtension.StakeSlashed(_indexer, _tokens, _rewards, _beneficiary); + staking.slash(_indexer, _tokens, _rewards, _beneficiary); + + // after + uint256 afterStakingBalance = token.balanceOf(address(staking)); + uint256 afterRewardsDestinationBalance = token.balanceOf(_beneficiary); + + assertEq(beforeStakingBalance - _tokens, afterStakingBalance); + assertEq(beforeRewardsDestinationBalance, afterRewardsDestinationBalance - _rewards); + } + + /* + * TESTS + */ + + function testSlash_Legacy( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 1); + slashTokens = bound(slashTokens, 1, tokens); + reward = bound(reward, 0, slashTokens); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + resetPrank(users.legacySlasher); + _legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_UsingLockedTokens( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 1); + slashTokens = bound(slashTokens, 1, tokens); + reward = bound(reward, 0, slashTokens); + + _setIndexer(users.indexer, tokens, 0, tokens, block.timestamp + 1); + // Send tokens manually to staking + token.transfer(address(staking), tokens); + + resetPrank(users.legacySlasher); + _legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_UsingAllocatedTokens( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 1); + slashTokens = bound(slashTokens, 1, tokens); + reward = bound(reward, 0, slashTokens); + + _setIndexer(users.indexer, tokens, 0, tokens, 0); + // Send tokens manually to staking + token.transfer(address(staking), tokens); + + resetPrank(users.legacySlasher); + staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_CallerNotSlasher( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer { + vm.assume(tokens > 0); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + vm.expectRevert("!slasher"); + staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_RewardsOverSlashTokens( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 0); + vm.assume(slashTokens > 0); + vm.assume(reward > slashTokens); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + resetPrank(users.legacySlasher); + vm.expectRevert("rewards>slash"); + staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_NoStake( + uint256 slashTokens, + uint256 reward + ) public useLegacySlasher(users.legacySlasher) { + vm.assume(slashTokens > 0); + reward = bound(reward, 0, slashTokens); + + resetPrank(users.legacySlasher); + vm.expectRevert("!stake"); + staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_ZeroTokens( + uint256 tokens + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 0); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + resetPrank(users.legacySlasher); + vm.expectRevert("!tokens"); + staking.legacySlash(users.indexer, 0, 0, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_NoBeneficiary( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 0); + slashTokens = bound(slashTokens, 1, tokens); + reward = bound(reward, 0, slashTokens); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + resetPrank(users.legacySlasher); + vm.expectRevert("!beneficiary"); + staking.legacySlash(users.indexer, slashTokens, reward, address(0)); + } + + function test_LegacySlash_WhenTokensAllocatedGreaterThanStake() + public + useIndexer + useLegacySlasher(users.legacySlasher) + { + // Setup indexer with: + // - tokensStaked = 1000 GRT + // - tokensAllocated = 800 GRT + // - tokensLocked = 300 GRT + // This means tokensUsed (1100 GRT) > tokensStaked (1000 GRT) + _setIndexer( + users.indexer, + 1000 ether, // tokensStaked + 800 ether, // tokensAllocated + 300 ether, // tokensLocked + 0 // tokensLockedUntil + ); + + // Send tokens manually to staking + token.transfer(address(staking), 1100 ether); + + resetPrank(users.legacySlasher); + _legacySlash(users.indexer, 1000 ether, 500 ether, makeAddr("fisherman")); + } + + function test_LegacySlash_WhenDelegateCallFails() public useIndexer useLegacySlasher(users.legacySlasher) { + // Setup indexer with: + // - tokensStaked = 1000 GRT + // - tokensAllocated = 800 GRT + // - tokensLocked = 300 GRT + + _setIndexer( + users.indexer, + 1000 ether, // tokensStaked + 800 ether, // tokensAllocated + 300 ether, // tokensLocked + 0 // tokensLockedUntil + ); + + // Send tokens manually to staking + token.transfer(address(staking), 1100 ether); + + // Change staking extension code to an invalid opcode so the delegatecall reverts + address stakingExtension = staking.getStakingExtension(); + vm.etch(stakingExtension, hex"fe"); + + resetPrank(users.legacySlasher); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingLegacySlashFailed()"); + vm.expectRevert(expectedError); + staking.slash(users.indexer, 1000 ether, 500 ether, makeAddr("fisherman")); + } +} diff --git a/packages/horizon/test/staking/slash/slash.t.sol b/packages/horizon/test/staking/slash/slash.t.sol new file mode 100644 index 000000000..3f67a9e59 --- /dev/null +++ b/packages/horizon/test/staking/slash/slash.t.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingSlashTest is HorizonStakingTest { + /* + * TESTS + */ + + function testSlash_Tokens( + uint256 tokens, + uint32 maxVerifierCut, + uint256 slashTokens, + uint256 verifierCutAmount + ) public useIndexer useProvision(tokens, maxVerifierCut, 0) { + slashTokens = bound(slashTokens, 1, tokens); + uint256 maxVerifierTokens = (slashTokens * maxVerifierCut) / MAX_PPM; + vm.assume(verifierCutAmount <= maxVerifierTokens); + + vm.startPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, slashTokens, verifierCutAmount); + } + + function testSlash_Tokens_RevertWhen_TooManyVerifierTokens( + uint256 tokens, + uint32 maxVerifierCut, + uint256 slashTokens, + uint256 verifierCutAmount + ) public useIndexer useProvision(tokens, maxVerifierCut, 0) { + slashTokens = bound(slashTokens, 1, tokens); + uint256 maxVerifierTokens = (slashTokens * maxVerifierCut) / MAX_PPM; + vm.assume(verifierCutAmount > maxVerifierTokens); + + vm.startPrank(subgraphDataServiceAddress); + vm.assume(slashTokens > 0); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingTooManyTokens.selector, + verifierCutAmount, + maxVerifierTokens + ); + vm.expectRevert(expectedError); + staking.slash(users.indexer, slashTokens, verifierCutAmount, subgraphDataServiceAddress); + } + + function testSlash_DelegationDisabled_SlashingOverProviderTokens( + uint256 tokens, + uint256 slashTokens, + uint256 verifierCutAmount, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, MAX_PPM, 0) { + vm.assume(slashTokens > tokens); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + verifierCutAmount = bound(verifierCutAmount, 0, MAX_PPM); + vm.assume(verifierCutAmount <= tokens); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + vm.startPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, slashTokens, verifierCutAmount); + } + + function testSlash_DelegationEnabled_SlashingOverProviderTokens( + uint256 tokens, + uint256 slashTokens, + uint256 verifierCutAmount, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, MAX_PPM, 0) useDelegationSlashing { + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + slashTokens = bound(slashTokens, tokens + 1, tokens + 1 + delegationTokens); + verifierCutAmount = bound(verifierCutAmount, 0, tokens); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + vm.startPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, slashTokens, verifierCutAmount); + } + + function testSlash_OverProvisionSize( + uint256 tokens, + uint256 slashTokens, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, MAX_PPM, 0) { + delegationTokens = bound(delegationTokens, 0, MAX_STAKING_TOKENS); + vm.assume(slashTokens > tokens + delegationTokens); + + vm.startPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, slashTokens, 0); + } + + function testSlash_RevertWhen_NoProvision(uint256 tokens, uint256 slashTokens) public useIndexer useStake(tokens) { + vm.assume(slashTokens > 0); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInsufficientTokens.selector, + 0, + slashTokens + ); + vm.expectRevert(expectedError); + vm.startPrank(subgraphDataServiceAddress); + staking.slash(users.indexer, slashTokens, 0, subgraphDataServiceAddress); + } + + function testSlash_Everything( + uint256 tokens, + uint256 delegationTokens + ) public useIndexer useProvision(tokens, MAX_PPM, 0) useDelegationSlashing { + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + vm.startPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + } + + function testSlash_Everything_WithUndelegation( + uint256 tokens + ) public useIndexer useProvision(tokens, MAX_PPM, 0) useDelegationSlashing { + uint256 delegationTokens = MAX_STAKING_TOKENS / 10; + + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + // undelegate half shares so we have some thawing shares/tokens + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + resetPrank(users.delegator); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares / 2); + + vm.startPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); + } + + function testSlash_RoundDown_TokensThawing_Provision() public useIndexer { + uint256 tokens = 1 ether + 1; + _useProvision(subgraphDataServiceAddress, tokens, MAX_PPM, MAX_THAWING_PERIOD); + + _thaw(users.indexer, subgraphDataServiceAddress, tokens); + + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, 1, 0); + + resetPrank(users.indexer); + Provision memory provision = staking.getProvision(users.indexer, subgraphDataServiceAddress); + assertEq(provision.tokens, tokens - 1); + // Tokens thawing should be rounded down + assertEq(provision.tokensThawing, tokens - 2); + } + + function testSlash_RoundDown_TokensThawing_Delegation( + uint256 tokens + ) public useIndexer useProvision(tokens, MAX_PPM, 0) useDelegationSlashing { + resetPrank(users.delegator); + uint256 delegationTokens = 1 ether + 1; + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); + + resetPrank(subgraphDataServiceAddress); + // Slash 1 token from delegation + _slash(users.indexer, subgraphDataServiceAddress, tokens + 1, 0); + + resetPrank(users.delegator); + DelegationPool memory pool = staking.getDelegationPool(users.indexer, subgraphDataServiceAddress); + assertEq(pool.tokens, delegationTokens - 1); + // Tokens thawing should be rounded down + assertEq(pool.tokensThawing, delegationTokens - 2); + } +} diff --git a/packages/horizon/test/staking/stake/stake.t.sol b/packages/horizon/test/staking/stake/stake.t.sol new file mode 100644 index 000000000..bf62de8b7 --- /dev/null +++ b/packages/horizon/test/staking/stake/stake.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingStakeTest is HorizonStakingTest { + /* + * TESTS + */ + + function testStake_Tokens(uint256 amount) public useIndexer { + amount = bound(amount, 1, MAX_STAKING_TOKENS); + _stake(amount); + } + + function testStake_RevertWhen_ZeroTokens() public useIndexer { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.stake(0); + } + + function testStakeTo_Tokens(uint256 amount) public useOperator { + amount = bound(amount, 1, MAX_STAKING_TOKENS); + _stakeTo(users.indexer, amount); + } + + function testStakeTo_RevertWhen_ZeroTokens() public useOperator { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.stakeTo(users.indexer, 0); + } +} diff --git a/packages/horizon/test/staking/stake/unstake.t.sol b/packages/horizon/test/staking/stake/unstake.t.sol new file mode 100644 index 000000000..83c6a0a81 --- /dev/null +++ b/packages/horizon/test/staking/stake/unstake.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingUnstakeTest is HorizonStakingTest { + /* + * TESTS + */ + + function testUnstake_Tokens( + uint256 tokens, + uint256 tokensToUnstake, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(tokens, maxVerifierCut, thawingPeriod) { + tokensToUnstake = bound(tokensToUnstake, 1, tokens); + + // thaw, wait and deprovision + _thaw(users.indexer, subgraphDataServiceAddress, tokens); + skip(thawingPeriod + 1); + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + + _unstake(tokensToUnstake); + } + + function testUnstake_LockingPeriodGreaterThanZero_NoThawing( + uint256 tokens, + uint256 tokensToUnstake, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(tokens, maxVerifierCut, thawingPeriod) { + tokensToUnstake = bound(tokensToUnstake, 1, tokens); + + // simulate transition period + _setStorage_DeprecatedThawingPeriod(THAWING_PERIOD_IN_BLOCKS); + + // thaw, wait and deprovision + _thaw(users.indexer, subgraphDataServiceAddress, tokens); + skip(thawingPeriod + 1); + _deprovision(users.indexer, subgraphDataServiceAddress, 0); + + // unstake + _unstake(tokensToUnstake); + } + + function testUnstake_LockingPeriodGreaterThanZero_TokensDoneThawing( + uint256 tokens, + uint256 tokensToUnstake, + uint256 tokensLocked + ) public useIndexer { + // bounds + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + tokensToUnstake = bound(tokensToUnstake, 1, tokens); + tokensLocked = bound(tokensLocked, 1, MAX_STAKING_TOKENS); + + // simulate locked tokens with past locking period + _setStorage_DeprecatedThawingPeriod(THAWING_PERIOD_IN_BLOCKS); + token.transfer(address(staking), tokensLocked); + _setStorage_ServiceProvider(users.indexer, tokensLocked, 0, tokensLocked, block.number, 0); + + // create provision, thaw and deprovision + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, MAX_THAWING_PERIOD); + _thaw(users.indexer, subgraphDataServiceLegacyAddress, tokens); + skip(MAX_THAWING_PERIOD + 1); + _deprovision(users.indexer, subgraphDataServiceLegacyAddress, 0); + + // unstake + _unstake(tokensToUnstake); + } + + function testUnstake_LockingPeriodGreaterThanZero_TokensStillThawing( + uint256 tokens, + uint256 tokensToUnstake, + uint256 tokensThawing, + uint32 tokensThawingUntilBlock + ) public useIndexer { + // bounds + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + tokensToUnstake = bound(tokensToUnstake, 1, tokens); + tokensThawing = bound(tokensThawing, 1, MAX_STAKING_TOKENS); + vm.assume(tokensThawingUntilBlock > block.number); + vm.assume(tokensThawingUntilBlock < block.number + THAWING_PERIOD_IN_BLOCKS); + + // simulate locked tokens still thawing + _setStorage_DeprecatedThawingPeriod(THAWING_PERIOD_IN_BLOCKS); + token.transfer(address(staking), tokensThawing); + _setStorage_ServiceProvider(users.indexer, tokensThawing, 0, tokensThawing, tokensThawingUntilBlock, 0); + + // create provision, thaw and deprovision + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, MAX_THAWING_PERIOD); + _thaw(users.indexer, subgraphDataServiceLegacyAddress, tokens); + skip(MAX_THAWING_PERIOD + 1); + _deprovision(users.indexer, subgraphDataServiceLegacyAddress, 0); + + // unstake + _unstake(tokensToUnstake); + } + + function testUnstake_RevertWhen_ZeroTokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) + public + useIndexer + useProvision(amount, maxVerifierCut, thawingPeriod) + useThawAndDeprovision(amount, thawingPeriod) + { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.unstake(0); + } + + function testUnstake_RevertWhen_NoIdleStake( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientIdleStake(uint256,uint256)", + amount, + 0 + ); + vm.expectRevert(expectedError); + staking.unstake(amount); + } + + function testUnstake_RevertWhen_NotDeprovision( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + _thaw(users.indexer, subgraphDataServiceAddress, amount); + skip(thawingPeriod + 1); + + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientIdleStake(uint256,uint256)", + amount, + 0 + ); + vm.expectRevert(expectedError); + staking.unstake(amount); + } +} diff --git a/packages/horizon/test/staking/stake/withdraw.t.sol b/packages/horizon/test/staking/stake/withdraw.t.sol new file mode 100644 index 000000000..b28dea022 --- /dev/null +++ b/packages/horizon/test/staking/stake/withdraw.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingWithdrawTest is HorizonStakingTest { + /* + * TESTS + */ + + function testWithdraw_Tokens(uint256 tokens, uint256 tokensLocked) public useIndexer { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + tokensLocked = bound(tokensLocked, 1, tokens); + + // simulate locked tokens ready to withdraw + token.transfer(address(staking), tokens); + _setStorage_ServiceProvider(users.indexer, tokens, 0, tokensLocked, block.number, 0); + + _createProvision(users.indexer, subgraphDataServiceAddress, tokens, 0, MAX_THAWING_PERIOD); + + _withdraw(); + } + + function testWithdraw_RevertWhen_ZeroTokens(uint256 tokens) public useIndexer { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + + // simulate zero locked tokens + token.transfer(address(staking), tokens); + _setStorage_ServiceProvider(users.indexer, tokens, 0, 0, 0, 0); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, MAX_THAWING_PERIOD); + + vm.expectRevert(abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidZeroTokens.selector)); + staking.withdraw(); + } + + function testWithdraw_RevertWhen_StillThawing(uint256 tokens, uint256 tokensLocked) public useIndexer { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + tokensLocked = bound(tokensLocked, 1, tokens); + + // simulate locked tokens still thawing + uint256 thawUntil = block.timestamp + 1; + token.transfer(address(staking), tokens); + _setStorage_ServiceProvider(users.indexer, tokens, 0, tokensLocked, thawUntil, 0); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, MAX_THAWING_PERIOD); + + vm.expectRevert(abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingStillThawing.selector, thawUntil)); + staking.withdraw(); + } +} diff --git a/packages/horizon/test/utilities/Authorizable.t.sol b/packages/horizon/test/utilities/Authorizable.t.sol new file mode 100644 index 000000000..c31721af8 --- /dev/null +++ b/packages/horizon/test/utilities/Authorizable.t.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +import { Authorizable } from "../../contracts/utilities/Authorizable.sol"; +import { IAuthorizable } from "../../contracts/interfaces/IAuthorizable.sol"; +import { Bounder } from "../utils/Bounder.t.sol"; + +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +contract AuthorizableImp is Authorizable { + constructor(uint256 _revokeAuthorizationThawingPeriod) Authorizable(_revokeAuthorizationThawingPeriod) {} +} + +contract AuthorizableTest is Test, Bounder { + AuthorizableImp public authorizable; + AuthorizableHelper authHelper; + + modifier withFuzzyThaw(uint256 _thawPeriod) { + // Max thaw period is 1 year to allow for thawing tests + _thawPeriod = bound(_thawPeriod, 1, 60 * 60 * 24 * 365); + setupAuthorizable(new AuthorizableImp(_thawPeriod)); + _; + } + + function setUp() public virtual { + setupAuthorizable(new AuthorizableImp(0)); + } + + function setupAuthorizable(AuthorizableImp _authorizable) internal { + authorizable = _authorizable; + authHelper = new AuthorizableHelper(authorizable); + } + + function test_AuthorizeSigner(uint256 _unboundedKey, address _authorizer) public { + vm.assume(_authorizer != address(0)); + uint256 signerKey = boundKey(_unboundedKey); + + authHelper.authorizeSignerWithChecks(_authorizer, signerKey); + } + + function test_AuthorizeSigner_Revert_WhenAlreadyAuthorized( + uint256[] memory _unboundedAuthorizers, + uint256 _unboundedKey + ) public { + vm.assume(_unboundedAuthorizers.length > 1); + address[] memory authorizers = new address[](_unboundedAuthorizers.length); + for (uint256 i = 0; i < authorizers.length; i++) { + authorizers[i] = boundAddr(_unboundedAuthorizers[i]); + } + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + address validAuthorizer = authorizers[0]; + authHelper.authorizeSignerWithChecks(validAuthorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerAlreadyAuthorized.selector, + validAuthorizer, + signer, + false + ); + + for (uint256 i = 0; i < authorizers.length; i++) { + vm.expectRevert(expectedErr); + vm.prank(authorizers[i]); + authorizable.authorizeSigner(signer, 0, ""); + } + } + + function test_AuthorizeSigner_Revert_WhenInvalidProofDeadline(uint256 _proofDeadline, uint256 _now) public { + _proofDeadline = bound(_proofDeadline, 0, _now); + vm.warp(_now); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableInvalidSignerProofDeadline.selector, + _proofDeadline, + _now + ); + vm.expectRevert(expectedErr); + authorizable.authorizeSigner(address(0), _proofDeadline, ""); + } + + function test_AuthorizeSigner_Revert_WhenAuthorizableInvalidSignerProof( + uint256 _now, + uint256 _unboundedAuthorizer, + uint256 _unboundedKey, + uint256 _proofDeadline, + uint256 _chainid, + uint256 _wrong + ) public { + _now = bound(_now, 0, type(uint256).max - 1); + address authorizer = boundAddr(_unboundedAuthorizer); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + _proofDeadline = boundTimestampMin(_proofDeadline, _now + 1); + vm.assume(_wrong != _proofDeadline); + _chainid = boundChainId(_chainid); + vm.assume(_wrong != _chainid); + (uint256 wrongKey, address wrongAddress) = boundAddrAndKey(_wrong); + vm.assume(wrongKey != signerKey); + vm.assume(wrongAddress != authorizer); + + vm.chainId(_chainid); + vm.warp(_now); + + bytes memory validProof = authHelper.generateAuthorizationProof( + _chainid, + address(authorizable), + _proofDeadline, + authorizer, + signerKey + ); + bytes[5] memory proofs = [ + authHelper.generateAuthorizationProof(_wrong, address(authorizable), _proofDeadline, authorizer, signerKey), + authHelper.generateAuthorizationProof(_chainid, wrongAddress, _proofDeadline, authorizer, signerKey), + authHelper.generateAuthorizationProof(_chainid, address(authorizable), _wrong, authorizer, signerKey), + authHelper.generateAuthorizationProof( + _chainid, + address(authorizable), + _proofDeadline, + wrongAddress, + signerKey + ), + authHelper.generateAuthorizationProof(_chainid, address(authorizable), _proofDeadline, authorizer, wrongKey) + ]; + + for (uint256 i = 0; i < proofs.length; i++) { + vm.expectRevert(IAuthorizable.AuthorizableInvalidSignerProof.selector); + vm.prank(authorizer); + authorizable.authorizeSigner(signer, _proofDeadline, proofs[i]); + } + + vm.prank(authorizer); + authorizable.authorizeSigner(signer, _proofDeadline, validProof); + authHelper.assertAuthorized(authorizer, signer); + } + + function test_ThawSigner(address _authorizer, uint256 _unboundedKey, uint256 _thaw) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + uint256 signerKey = boundKey(_unboundedKey); + + authHelper.authorizeAndThawSignerWithChecks(_authorizer, signerKey); + } + + function test_ThawSigner_Revert_WhenNotAuthorized(address _authorizer, address _signer) public { + vm.assume(_authorizer != address(0)); + vm.assume(_signer != address(0)); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + _signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.thawSigner(_signer); + } + + function test_ThawSigner_Revert_WhenAuthorizationRevoked( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + authHelper.authorizeAndRevokeSignerWithChecks(_authorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.thawSigner(signer); + } + + function test_CancelThawSigner( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + authHelper.authorizeAndThawSignerWithChecks(_authorizer, signerKey); + vm.expectEmit(address(authorizable)); + emit IAuthorizable.SignerThawCanceled(_authorizer, signer, authorizable.getThawEnd(signer)); + vm.prank(_authorizer); + authorizable.cancelThawSigner(signer); + + authHelper.assertAuthorized(_authorizer, signer); + } + + function test_CancelThawSigner_Revert_When_NotAuthorized(address _authorizer, address _signer) public { + vm.assume(_authorizer != address(0)); + vm.assume(_signer != address(0)); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + _signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.cancelThawSigner(_signer); + } + + function test_CancelThawSigner_Revert_WhenAuthorizationRevoked( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + authHelper.authorizeAndRevokeSignerWithChecks(_authorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.cancelThawSigner(signer); + } + + function test_CancelThawSigner_Revert_When_NotThawing(address _authorizer, uint256 _unboundedKey) public { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + authHelper.authorizeSignerWithChecks(_authorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector(IAuthorizable.AuthorizableSignerNotThawing.selector, signer); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.cancelThawSigner(signer); + } + + function test_RevokeAuthorizedSigner( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + uint256 signerKey = boundKey(_unboundedKey); + + authHelper.authorizeAndRevokeSignerWithChecks(_authorizer, signerKey); + } + + function test_RevokeAuthorizedSigner_Revert_WhenNotAuthorized(address _authorizer, address _signer) public { + vm.assume(_authorizer != address(0)); + vm.assume(_signer != address(0)); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + _signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(_signer); + } + + function test_RevokeAuthorizedSigner_Revert_WhenAuthorizationRevoked( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + authHelper.authorizeAndRevokeSignerWithChecks(_authorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(signer); + } + + function test_RevokeAuthorizedSigner_Revert_WhenNotThawing(address _authorizer, uint256 _unboundedKey) public { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + authHelper.authorizeSignerWithChecks(_authorizer, signerKey); + bytes memory expectedErr = abi.encodeWithSelector(IAuthorizable.AuthorizableSignerNotThawing.selector, signer); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(signer); + } + + function test_RevokeAuthorizedSigner_Revert_WhenStillThawing( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw, + uint256 _skip + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + authHelper.authorizeAndThawSignerWithChecks(_authorizer, signerKey); + + _skip = bound(_skip, 0, authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() - 1); + skip(_skip); + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerStillThawing.selector, + block.timestamp, + block.timestamp - _skip + authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(signer); + } + + function test_IsAuthorized_Revert_WhenZero(address signer) public view { + authHelper.assertNotAuthorized(address(0), signer); + } +} + +contract AuthorizableHelper is Test { + AuthorizableImp internal authorizable; + + constructor(AuthorizableImp _authorizable) { + authorizable = _authorizable; + } + + function authorizeAndThawSignerWithChecks(address _authorizer, uint256 _signerKey) public { + address signer = vm.addr(_signerKey); + authorizeSignerWithChecks(_authorizer, _signerKey); + + uint256 thawEndTimestamp = block.timestamp + authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD(); + vm.expectEmit(address(authorizable)); + emit IAuthorizable.SignerThawing(_authorizer, signer, thawEndTimestamp); + vm.prank(_authorizer); + authorizable.thawSigner(signer); + + assertAuthorized(_authorizer, signer); + } + + function authorizeAndRevokeSignerWithChecks(address _authorizer, uint256 _signerKey) public { + address signer = vm.addr(_signerKey); + authorizeAndThawSignerWithChecks(_authorizer, _signerKey); + skip(authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() + 1); + vm.expectEmit(address(authorizable)); + emit IAuthorizable.SignerRevoked(_authorizer, signer); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(signer); + + assertNotAuthorized(_authorizer, signer); + } + + function authorizeSignerWithChecks(address _authorizer, uint256 _signerKey) public { + address signer = vm.addr(_signerKey); + assertNotAuthorized(_authorizer, signer); + + uint256 proofDeadline = block.timestamp + 1; + bytes memory proof = generateAuthorizationProof( + block.chainid, + address(authorizable), + proofDeadline, + _authorizer, + _signerKey + ); + vm.expectEmit(address(authorizable)); + emit IAuthorizable.SignerAuthorized(_authorizer, signer); + vm.prank(_authorizer); + authorizable.authorizeSigner(signer, proofDeadline, proof); + + assertAuthorized(_authorizer, signer); + } + + function assertNotAuthorized(address _authorizer, address _signer) public view { + assertFalse(authorizable.isAuthorized(_authorizer, _signer), "Should not be authorized"); + } + + function assertAuthorized(address _authorizer, address _signer) public view { + assertTrue(authorizable.isAuthorized(_authorizer, _signer), "Should be authorized"); + } + + function generateAuthorizationProof( + uint256 _chainId, + address _verifyingContract, + uint256 _proofDeadline, + address _authorizer, + uint256 _signerPrivateKey + ) public pure returns (bytes memory) { + // Generate the message hash + bytes32 messageHash = keccak256( + abi.encodePacked(_chainId, _verifyingContract, "authorizeSignerProof", _proofDeadline, _authorizer) + ); + + // Generate the digest to sign + bytes32 digest = MessageHashUtils.toEthSignedMessageHash(messageHash); + + // Sign the digest + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPrivateKey, digest); + + // Encode the signature + return abi.encodePacked(r, s, v); + } +} diff --git a/packages/horizon/test/utilities/GraphDirectory.t.sol b/packages/horizon/test/utilities/GraphDirectory.t.sol new file mode 100644 index 000000000..606933699 --- /dev/null +++ b/packages/horizon/test/utilities/GraphDirectory.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { GraphBaseTest } from "../GraphBase.t.sol"; +import { GraphDirectory } from "./../../contracts/utilities/GraphDirectory.sol"; +import { GraphDirectoryImplementation } from "./GraphDirectoryImplementation.sol"; + +contract GraphDirectoryTest is GraphBaseTest { + function test_WhenTheContractIsDeployedWithAValidController() external { + vm.expectEmit(); + emit GraphDirectory.GraphDirectoryInitialized( + _getContractFromController("GraphToken"), + _getContractFromController("Staking"), + _getContractFromController("GraphPayments"), + _getContractFromController("PaymentsEscrow"), + address(controller), + _getContractFromController("EpochManager"), + _getContractFromController("RewardsManager"), + _getContractFromController("GraphTokenGateway"), + _getContractFromController("GraphProxyAdmin"), + _getContractFromController("Curation") + ); + _deployImplementation(address(controller)); + } + + function test_RevertWhen_TheContractIsDeployedWithAnInvalidController(address controller_) external { + vm.assume(controller_ != address(controller)); + vm.assume(uint160(controller_) > 9); // Skip precompiled contracts + + vm.expectRevert(); // call to getContractProxy on a random address reverts + _deployImplementation(controller_); + } + + function test_RevertWhen_TheContractIsDeployedWithTheZeroAddressAsTheInvalidController() external { + vm.expectRevert(abi.encodeWithSelector(GraphDirectory.GraphDirectoryInvalidZeroAddress.selector, "Controller")); // call to getContractProxy on a random address reverts + _deployImplementation(address(0)); + } + + function test_WhenTheContractGettersAreCalled() external { + GraphDirectoryImplementation directory = _deployImplementation(address(controller)); + + assertEq(_getContractFromController("GraphToken"), address(directory.graphToken())); + assertEq(_getContractFromController("Staking"), address(directory.graphStaking())); + assertEq(_getContractFromController("GraphPayments"), address(directory.graphPayments())); + assertEq(_getContractFromController("PaymentsEscrow"), address(directory.graphPaymentsEscrow())); + assertEq(_getContractFromController("EpochManager"), address(directory.graphEpochManager())); + assertEq(_getContractFromController("RewardsManager"), address(directory.graphRewardsManager())); + assertEq(_getContractFromController("GraphTokenGateway"), address(directory.graphTokenGateway())); + assertEq(_getContractFromController("GraphProxyAdmin"), address(directory.graphProxyAdmin())); + assertEq(_getContractFromController("Curation"), address(directory.graphCuration())); + } + + function test_RevertWhen_AnInvalidContractGetterIsCalled() external { + // Zero out the Staking contract address to simulate a non registered contract + bytes32 storageSlot = keccak256(abi.encode(keccak256("Staking"), 5)); + vm.store(address(controller), storageSlot, bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(GraphDirectory.GraphDirectoryInvalidZeroAddress.selector, "Staking")); + _deployImplementation(address(controller)); + } + + function _deployImplementation(address _controller) private returns (GraphDirectoryImplementation) { + return new GraphDirectoryImplementation(_controller); + } + + function _getContractFromController(bytes memory _contractName) private view returns (address) { + return controller.getContractProxy(keccak256(_contractName)); + } +} diff --git a/packages/horizon/test/utilities/GraphDirectory.tree b/packages/horizon/test/utilities/GraphDirectory.tree new file mode 100644 index 000000000..fc928ec29 --- /dev/null +++ b/packages/horizon/test/utilities/GraphDirectory.tree @@ -0,0 +1,11 @@ +GraphDirectoryTest +├── when the contract is deployed with a valid controller +│ └── it should emit an initialization event +├── when the contract is deployed with an invalid controller +│ └── it should revert +├── when the contract is deployed with the zero address as the invalid controller +│ └── it should revert +├── when the contract getters are called +│ └── it should return the contract address +└── when an invalid contract getter is called + └── it should revert \ No newline at end of file diff --git a/packages/horizon/test/utilities/GraphDirectoryImplementation.sol b/packages/horizon/test/utilities/GraphDirectoryImplementation.sol new file mode 100644 index 000000000..1095adc3a --- /dev/null +++ b/packages/horizon/test/utilities/GraphDirectoryImplementation.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IHorizonStaking } from "../../contracts/interfaces/IHorizonStaking.sol"; +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; +import { IPaymentsEscrow } from "../../contracts/interfaces/IPaymentsEscrow.sol"; + +import { IController } from "@graphprotocol/contracts/contracts/governance/IController.sol"; +import { IEpochManager } from "@graphprotocol/contracts/contracts/epochs/IEpochManager.sol"; +import { IRewardsManager } from "@graphprotocol/contracts/contracts/rewards/IRewardsManager.sol"; +import { ITokenGateway } from "@graphprotocol/contracts/contracts/arbitrum/ITokenGateway.sol"; +import { IGraphProxyAdmin } from "../../contracts/interfaces/IGraphProxyAdmin.sol"; +import { ICuration } from "@graphprotocol/contracts/contracts/curation/ICuration.sol"; + +import { GraphDirectory } from "./../../contracts/utilities/GraphDirectory.sol"; + +contract GraphDirectoryImplementation is GraphDirectory { + constructor(address controller) GraphDirectory(controller) {} + + function getContractFromController(bytes memory contractName) external view returns (address) { + return _graphController().getContractProxy(keccak256(contractName)); + } + function graphToken() external view returns (IGraphToken) { + return _graphToken(); + } + + function graphStaking() external view returns (IHorizonStaking) { + return _graphStaking(); + } + + function graphPayments() external view returns (IGraphPayments) { + return _graphPayments(); + } + + function graphPaymentsEscrow() external view returns (IPaymentsEscrow) { + return _graphPaymentsEscrow(); + } + + function graphController() external view returns (IController) { + return _graphController(); + } + + function graphEpochManager() external view returns (IEpochManager) { + return _graphEpochManager(); + } + + function graphRewardsManager() external view returns (IRewardsManager) { + return _graphRewardsManager(); + } + + function graphTokenGateway() external view returns (ITokenGateway) { + return _graphTokenGateway(); + } + + function graphProxyAdmin() external view returns (IGraphProxyAdmin) { + return _graphProxyAdmin(); + } + + function graphCuration() external view returns (ICuration) { + return _graphCuration(); + } +} diff --git a/packages/horizon/test/utils/Bounder.t.sol b/packages/horizon/test/utils/Bounder.t.sol new file mode 100644 index 000000000..44e977f57 --- /dev/null +++ b/packages/horizon/test/utils/Bounder.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +contract Bounder is Test { + uint256 constant SECP256K1_CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; + + function boundAddrAndKey(uint256 _value) internal pure returns (uint256, address) { + uint256 signerKey = bound(_value, 1, SECP256K1_CURVE_ORDER - 1); + return (signerKey, vm.addr(signerKey)); + } + + function boundAddr(uint256 _value) internal pure returns (address) { + (, address addr) = boundAddrAndKey(_value); + return addr; + } + + function boundKey(uint256 _value) internal pure returns (uint256) { + (uint256 key, ) = boundAddrAndKey(_value); + return key; + } + + function boundChainId(uint256 _value) internal pure returns (uint256) { + return bound(_value, 1, (2 ^ 64) - 1); + } + + function boundTimestampMin(uint256 _value, uint256 _min) internal pure returns (uint256) { + return bound(_value, _min, type(uint256).max); + } +} diff --git a/packages/horizon/test/utils/Constants.sol b/packages/horizon/test/utils/Constants.sol new file mode 100644 index 000000000..0aa53700d --- /dev/null +++ b/packages/horizon/test/utils/Constants.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +abstract contract Constants { + uint32 internal constant MAX_PPM = 1000000; // 100% in parts per million + uint256 internal constant delegationFeeCut = 100000; // 10% in parts per million + uint256 internal constant MAX_STAKING_TOKENS = 10_000_000_000 ether; + // GraphEscrow parameters + uint256 internal constant withdrawEscrowThawingPeriod = 60; + // GraphPayments parameters + uint256 internal constant protocolPaymentCut = 10000; + // Staking constants + uint256 internal constant MAX_THAW_REQUESTS = 1_000; + uint64 internal constant MAX_THAWING_PERIOD = 28 days; + uint32 internal constant THAWING_PERIOD_IN_BLOCKS = 300; + uint256 internal constant MIN_DELEGATION = 1e18; + // Epoch manager + uint256 internal constant EPOCH_LENGTH = 1; + // Rewards manager + uint256 internal constant ALLOCATIONS_REWARD_CUT = 100 ether; + // GraphTallyCollector + uint256 internal constant revokeSignerThawingPeriod = 7 days; +} diff --git a/packages/horizon/test/utils/Users.sol b/packages/horizon/test/utils/Users.sol new file mode 100644 index 000000000..6213e4e82 --- /dev/null +++ b/packages/horizon/test/utils/Users.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +struct Users { + address governor; + address deployer; + address indexer; + address operator; + address gateway; + address verifier; + address delegator; + address legacySlasher; +} diff --git a/packages/horizon/test/utils/Utils.sol b/packages/horizon/test/utils/Utils.sol new file mode 100644 index 000000000..be42f269f --- /dev/null +++ b/packages/horizon/test/utils/Utils.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +abstract contract Utils is Test { + /// @dev Stops the active prank and sets a new one. + function resetPrank(address msgSender) internal { + vm.stopPrank(); + vm.startPrank(msgSender); + } +} diff --git a/packages/horizon/tsconfig.json b/packages/horizon/tsconfig.json new file mode 100644 index 000000000..5f32ebc8c --- /dev/null +++ b/packages/horizon/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "node16", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist", + "moduleResolution": "node16" + }, + "include": [ + "hardhat.config.ts", + "types/**/*.ts", + "scripts/**/*.ts", + "tasks/**/*.ts", + "test/**/*.ts", + "ignition/**/*.ts", + "eslint.config.js", + "prettier.config.js", + "natspec-smells.config.js" + ] +} diff --git a/packages/horizon/types/hardhat-graph-protocol.d.ts b/packages/horizon/types/hardhat-graph-protocol.d.ts new file mode 100644 index 000000000..8b5985269 --- /dev/null +++ b/packages/horizon/types/hardhat-graph-protocol.d.ts @@ -0,0 +1,45 @@ +// TypeScript does not resolve correctly the type extensions when they are symlinked from the same monorepo. +// So we need to re-type it... this file should be a copy of hardhat-graph-protocol/src/type-extensions.ts +import 'hardhat/types/config' +import 'hardhat/types/runtime' +import type { GraphDeployments, GraphRuntimeEnvironment, GraphRuntimeEnvironmentOptions } from 'hardhat-graph-protocol' + +declare module 'hardhat/types/runtime' { + interface HardhatRuntimeEnvironment { + graph: (opts?: GraphRuntimeEnvironmentOptions) => GraphRuntimeEnvironment + } +} + +declare module 'hardhat/types/config' { + interface HardhatConfig { + graph: GraphRuntimeEnvironmentOptions + } + + interface HardhatUserConfig { + graph: GraphRuntimeEnvironmentOptions + } + + interface HardhatNetworkConfig { + deployments?: GraphDeployments + } + + interface HardhatNetworkUserConfig { + deployments?: GraphDeployments + } + + interface HttpNetworkConfig { + deployments?: GraphDeployments + } + + interface HttpNetworkUserConfig { + deployments?: GraphDeployments + } + + interface ProjectPathsConfig { + graph?: string + } + + interface ProjectPathsUserConfig { + graph?: string + } +} diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 20a787d51..9cc5a8e17 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,11 @@ # @graphprotocol/sdk +## 0.5.1 + +### Patch Changes + +- Bump contracts dependency + ## 0.5.0 ### Minor Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 18497431e..6083abdcb 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@graphprotocol/sdk", - "version": "0.5.0", + "version": "0.5.1", "description": "TypeScript based SDK to interact with The Graph protocol contracts", "main": "build/index.js", "types": "src/index.ts", @@ -21,7 +21,7 @@ "@arbitrum/sdk": "~3.1.13", "@ethersproject/experimental": "^5.7.0", "@graphprotocol/common-ts": "^2.0.7", - "@graphprotocol/contracts": "workspace:^6.2.0", + "@graphprotocol/contracts": "workspace:^7.0.0", "@nomicfoundation/hardhat-network-helpers": "^1.0.9", "@nomiclabs/hardhat-ethers": "^2.2.3", "debug": "^4.3.4", diff --git a/packages/solhint-graph-config/README.md b/packages/solhint-graph-config/README.md index 7a724946b..4257b2dd8 100644 --- a/packages/solhint-graph-config/README.md +++ b/packages/solhint-graph-config/README.md @@ -17,6 +17,13 @@ yarn add --dev solhint solhint-graph-config yarn add --dev solhint solhint-graph-config@workspace:^x.y.z ``` +To use graph plugin you'll also need to manually add the plugin to your `package.json`: +```json + "devDependencies": { + "solhint-plugin-graph": "file:node_modules/solhint-graph-config/plugin" + } +``` + ### Configuration Run `solhint` with `node_modules/solhint-graph-config/index.js` as the configuration file. We suggest creating an npm script to make it easier to run: diff --git a/packages/solhint-graph-config/index.js b/packages/solhint-graph-config/index.js index b37be2810..770350520 100644 --- a/packages/solhint-graph-config/index.js +++ b/packages/solhint-graph-config/index.js @@ -1,12 +1,33 @@ module.exports = { + plugins: [ 'graph' ], extends: 'solhint:recommended', rules: { - 'func-visibility': ['warn', { ignoreConstructors: true }], - 'compiler-version': ['off'], + // best practices + 'no-empty-blocks': 'off', 'constructor-syntax': 'warn', + + // style rules + 'private-vars-leading-underscore': 'off', // see graph/leading-underscore + 'const-name-snakecase': 'warn', + 'named-parameters-mapping': 'warn', + 'imports-on-top': 'warn', + 'ordering': 'warn', + 'visibility-modifier-order': 'warn', + 'func-name-mixedcase': 'off', // see graph/func-name-mixedcase + 'var-name-mixedcase': 'off', // see graph/var-name-mixedcase + + // miscellaneous 'quotes': ['error', 'double'], - 'reason-string': ['off'], + + // security + 'compiler-version': ['off'], + 'func-visibility': ['warn', { ignoreConstructors: true }], 'not-rely-on-time': 'off', - 'no-empty-blocks': 'off', + + // graph + 'graph/leading-underscore': 'warn', + 'graph/func-name-mixedcase': 'warn', + 'graph/var-name-mixedcase': 'warn', + 'gas-custom-errors': 'off' }, } diff --git a/packages/solhint-graph-config/package.json b/packages/solhint-graph-config/package.json index 2ead8bbef..5b516ed58 100644 --- a/packages/solhint-graph-config/package.json +++ b/packages/solhint-graph-config/package.json @@ -1,13 +1,17 @@ { "name": "solhint-graph-config", + "private": true, "version": "0.0.1", "description": "Linting and formatting rules for The Graph's Solidity projects", "main": "index.js", "author": "The Graph Team", "license": "GPL-2.0-or-later", + "dependencies": { + "solhint-plugin-graph": "workspace:*" + }, "peerDependencies": { "prettier": "^3.2.5", "prettier-plugin-solidity": "^1.3.1", - "solhint": "^4.1.1" + "solhint": "^4.5.4" } } diff --git a/packages/solhint-plugin-graph/index.js b/packages/solhint-plugin-graph/index.js new file mode 100644 index 000000000..7e134089f --- /dev/null +++ b/packages/solhint-plugin-graph/index.js @@ -0,0 +1,172 @@ +function hasLeadingUnderscore(text) { + return text && text[0] === '_' +} + +function match(text, regex) { + return text.replace(regex, '').length === 0 +} + +function isMixedCase(text) { + return match(text, /[_]*[a-z$]+[a-zA-Z0-9$]*[_]?/) +} + +function isUpperSnakeCase(text) { + return match(text, /_{0,2}[A-Z0-9$]+[_A-Z0-9$]*/) +} + +class Base { + constructor(reporter, config, source, fileName) { + this.ignoreDeprecated = true; + this.deprecatedPrefix = '__DEPRECATED_'; + this.underscorePrefix = '__'; + this.reporter = reporter; + this.ignored = this.constructor.global; + this.ruleId = this.constructor.ruleId; + if (this.ruleId === undefined) { + throw Error('missing ruleId static property'); + } + } + + error(node, message, fix) { + if (!this.ignored) { + this.reporter.error(node, this.ruleId, message, fix); + } + } +} + +module.exports = [ + class extends Base { + static ruleId = 'leading-underscore'; + + ContractDefinition(node) { + if (node.kind === 'library') { + this.inLibrary = true + } + } + + 'ContractDefinition:exit'() { + this.inLibrary = false + } + + StateVariableDeclaration() { + this.inStateVariableDeclaration = true + } + + 'StateVariableDeclaration:exit'() { + this.inStateVariableDeclaration = false + } + + VariableDeclaration(node) { + if (!this.inLibrary) { + if (!this.inStateVariableDeclaration) { + this.validateName(node, false, 'variable') + return + } + + this.validateName(node, 'variable') + } + + } + + FunctionDefinition(node) { + if (!this.inLibrary) { + if (!node.name) { + return + } + for (const parameter of node.parameters) { + parameter.visibility = node.visibility + } + + this.validateName(node, 'function') + + } + } + + validateName(node, type) { + if (this.ignoreDeprecated && node.name.startsWith(this.deprecatedPrefix)) { + return + } + + const isPrivate = node.visibility === 'private' + const isInternal = node.visibility === 'internal' || node.visibility === 'default' + const isConstant = node.isDeclaredConst + const isImmutable = node.isImmutable + const shouldHaveLeadingUnderscore = (isPrivate || isInternal) && !(isConstant || isImmutable) + + if (node.name === null) { + return + } + + if (hasLeadingUnderscore(node.name) !== shouldHaveLeadingUnderscore) { + this._error(node, node.name, shouldHaveLeadingUnderscore, type) + } + } + + fixStatement(node, shouldHaveLeadingUnderscore, type) { + let range + + if (type === 'function') { + range = node.range + range[0] += 8 + } else if (type === 'parameter') { + range = node.identifier.range + } else { + range = node.identifier.range + range[0] -= 1 + } + + return (fixer) => + shouldHaveLeadingUnderscore + ? fixer.insertTextBeforeRange(range, ' _') + : fixer.removeRange([range[0] + 1, range[0] + 1]) + } + + _error(node, name, shouldHaveLeadingUnderscore, type) { + this.error( + node, + `'${name}' ${shouldHaveLeadingUnderscore ? 'should' : 'should not'} start with _`, + // this.fixStatement(node, shouldHaveLeadingUnderscore, type) + ) + } + }, + class extends Base { + static ruleId = 'func-name-mixedcase'; + + FunctionDefinition(node) { + // Allow __DEPRECATED_ prefixed functions and __ prefixed functions + if (node.name.startsWith(this.deprecatedPrefix) || node.name.startsWith(this.underscorePrefix)) { + return + } + + if (!isMixedCase(node.name) && !node.isConstructor) { + // Allow external functions to be in UPPER_SNAKE_CASE - for immutable state getters + if (node.visibility === 'external' && isUpperSnakeCase(node.name)) { + return + } + this.error(node, 'Function name must be in mixedCase',) + } + } + }, + class extends Base { + static ruleId = 'var-name-mixedcase'; + + VariableDeclaration(node) { + if (node.name.startsWith(this.deprecatedPrefix)) { + return + } + if (!node.isDeclaredConst && !node.isImmutable) { + this.validateVariablesName(node) + } + } + + validateVariablesName(node) { + if (node.name.startsWith(this.deprecatedPrefix)) { + return + } + if (!isMixedCase(node.name)) { + this.error(node, 'Variable name must be in mixedCase') + } + } + } + +]; \ No newline at end of file diff --git a/packages/solhint-plugin-graph/package.json b/packages/solhint-plugin-graph/package.json new file mode 100644 index 000000000..1b1c0f568 --- /dev/null +++ b/packages/solhint-plugin-graph/package.json @@ -0,0 +1,5 @@ +{ + "name": "solhint-plugin-graph", + "version": "0.0.1", + "private": true +} diff --git a/packages/subgraph-service/README.md b/packages/subgraph-service/README.md new file mode 100644 index 000000000..36755d41a --- /dev/null +++ b/packages/subgraph-service/README.md @@ -0,0 +1,55 @@ +# 🌅 Subgraph Service 🌅 + +The Subgraph Service is a data service designed to work with Graph Horizon that supports indexing subgraphs and serving queries to consumers. + +## Configuration + +The following environment variables might be required: + +| Variable | Description | +|----------|-------------| +| `ARBISCAN_API_KEY` | Arbiscan API key - for contract verification| +| `ARBITRUM_ONE_RPC` | Arbitrum One RPC URL - defaults to `https://arb1.arbitrum.io/rpc` | +| `ARBITRUM_SEPOLIA_RPC` | Arbitrum Sepolia RPC URL - defaults to `https://sepolia-rollup.arbitrum.io/rpc` | +| `LOCALHOST_RPC` | Localhost RPC URL - defaults to `http://localhost:8545` | +| `LOCALHOST_CHAIN_ID` | Localhost chain ID - defaults to `31337` | +| `LOCALHOST_ACCOUNTS_MNEMONIC` | Localhost accounts mnemonic - no default value. Note that setting this will override any secure accounts configuration. | + +You can set them using Hardhat: + +```bash +npx hardhat vars set +``` + +## Build + +```bash +yarn install +yarn build +``` + +## Deployment + +Note that this instructions will help you deploy Graph Horizon contracts alongside the Subgraph Service. If you want to deploy just the core Horizon contracts please refer to the [Horizon README](../horizon/README.md) for deploy instructions. + +### New deployment +To deploy Graph Horizon from scratch including the Subgraph Service run the following command: + +```bash +npx hardhat deploy:protocol --network hardhat +``` + +### Upgrade deployment +Usually you would run this against a network (or a fork) where the original Graph Protocol was previously deployed. To upgrade an existing deployment of the original Graph Protocol to Graph Horizon including the Subgraph Service, run the following commands. Note that some steps might need to be run by different accounts (deployer vs governor): + +```bash +cd ../ +cd horizon && npx hardhat deploy:migrate --network hardhat --step 1 && cd .. +cd subgraph-service && npx hardhat deploy:migrate --network hardhat --step 1 && cd .. +cd horizon && npx hardhat deploy:migrate --network hardhat --step 2 && cd .. # Run with governor. Optionally add --patch-config +cd horizon && npx hardhat deploy:migrate --network hardhat --step 3 && cd .. # Optionally add --patch-config +cd subgraph-service && npx hardhat deploy:migrate --network hardhat --step 2 && cd .. # Optionally add --patch-config +cd horizon && npx hardhat deploy:migrate --network hardhat --step 4 && cd .. # Run with governor. Optionally add --patch-config +``` + +Horizon Steps 2, 3 and 4, and Subgraph Service Step 2 require patching the configuration file with addresses from previous steps. The files are located in the `ignition/configs` directory and need to be manually edited. You can also pass `--patch-config` flag to the deploy command to automatically patch the configuration reading values from the address book. Note that this will NOT update the configuration file. \ No newline at end of file diff --git a/packages/subgraph-service/addresses.json b/packages/subgraph-service/addresses.json new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/packages/subgraph-service/addresses.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol new file mode 100644 index 000000000..8d80ce1c1 --- /dev/null +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -0,0 +1,642 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.27; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; +import { IDisputeManager } from "./interfaces/IDisputeManager.sol"; +import { ISubgraphService } from "./interfaces/ISubgraphService.sol"; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { MathUtils } from "@graphprotocol/horizon/contracts/libraries/MathUtils.sol"; +import { Allocation } from "./libraries/Allocation.sol"; +import { Attestation } from "./libraries/Attestation.sol"; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol"; +import { DisputeManagerV1Storage } from "./DisputeManagerStorage.sol"; +import { AttestationManager } from "./utilities/AttestationManager.sol"; + +/** + * @title DisputeManager + * @notice Provides a way to permissionlessly create disputes for incorrect behavior in the Subgraph Service. + * + * There are two types of disputes that can be created: Query disputes and Indexing disputes. + * + * Query Disputes: + * Graph nodes receive queries and return responses with signed receipts called attestations. + * An attestation can be disputed if the consumer thinks the query response was invalid. + * Indexers use the derived private key for an allocation to sign attestations. + * + * Indexing Disputes: + * Indexers periodically present a Proof of Indexing (POI) to prove they are indexing a subgraph. + * The Subgraph Service contract emits that proof which includes the POI. Any fisherman can dispute the + * validity of a POI by submitting a dispute to this contract along with a deposit. + * + * Arbitration: + * Disputes can only be accepted, rejected or drawn by the arbitrator role that can be delegated + * to a EOA or DAO. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +contract DisputeManager is + Initializable, + OwnableUpgradeable, + GraphDirectory, + AttestationManager, + DisputeManagerV1Storage, + IDisputeManager +{ + using TokenUtils for IGraphToken; + using PPMMath for uint256; + + // -- Constants -- + + // Maximum value for fisherman reward cut in PPM + uint32 public constant MAX_FISHERMAN_REWARD_CUT = 500000; // 50% + + // Minimum value for dispute deposit + uint256 public constant MIN_DISPUTE_DEPOSIT = 1e18; // 1 GRT + + // -- Modifiers -- + + /** + * @notice Check if the caller is the arbitrator. + */ + modifier onlyArbitrator() { + require(msg.sender == arbitrator, DisputeManagerNotArbitrator()); + _; + } + + /** + * @notice Check if the dispute exists and is pending. + * @param disputeId The dispute Id + */ + modifier onlyPendingDispute(bytes32 disputeId) { + require(isDisputeCreated(disputeId), DisputeManagerInvalidDispute(disputeId)); + require( + disputes[disputeId].status == IDisputeManager.DisputeStatus.Pending, + DisputeManagerDisputeNotPending(disputes[disputeId].status) + ); + _; + } + + /** + * @notice Check if the caller is the fisherman of the dispute. + * @param disputeId The dispute Id + */ + modifier onlyFisherman(bytes32 disputeId) { + require(isDisputeCreated(disputeId), DisputeManagerInvalidDispute(disputeId)); + require(msg.sender == disputes[disputeId].fisherman, DisputeManagerNotFisherman()); + _; + } + + /** + * @notice Contract constructor + * @param controller Address of the controller + */ + constructor(address controller) GraphDirectory(controller) { + _disableInitializers(); + } + + /// @inheritdoc IDisputeManager + function initialize( + address owner, + address arbitrator, + uint64 disputePeriod, + uint256 disputeDeposit, + uint32 fishermanRewardCut_, + uint32 maxSlashingCut_ + ) external override initializer { + __Ownable_init(owner); + __AttestationManager_init(); + + _setArbitrator(arbitrator); + _setDisputePeriod(disputePeriod); + _setDisputeDeposit(disputeDeposit); + _setFishermanRewardCut(fishermanRewardCut_); + _setMaxSlashingCut(maxSlashingCut_); + } + + /// @inheritdoc IDisputeManager + function createIndexingDispute(address allocationId, bytes32 poi) external override returns (bytes32) { + // Get funds from fisherman + _graphToken().pullTokens(msg.sender, disputeDeposit); + + // Create a dispute + return _createIndexingDisputeWithAllocation(msg.sender, disputeDeposit, allocationId, poi); + } + + /// @inheritdoc IDisputeManager + function createQueryDispute(bytes calldata attestationData) external override returns (bytes32) { + // Get funds from fisherman + _graphToken().pullTokens(msg.sender, disputeDeposit); + + // Create a dispute + return + _createQueryDisputeWithAttestation( + msg.sender, + disputeDeposit, + Attestation.parse(attestationData), + attestationData + ); + } + + /// @inheritdoc IDisputeManager + function createQueryDisputeConflict( + bytes calldata attestationData1, + bytes calldata attestationData2 + ) external override returns (bytes32, bytes32) { + address fisherman = msg.sender; + + // Parse each attestation + Attestation.State memory attestation1 = Attestation.parse(attestationData1); + Attestation.State memory attestation2 = Attestation.parse(attestationData2); + + // Test that attestations are conflicting + require( + Attestation.areConflicting(attestation1, attestation2), + DisputeManagerNonConflictingAttestations( + attestation1.requestCID, + attestation1.responseCID, + attestation1.subgraphDeploymentId, + attestation2.requestCID, + attestation2.responseCID, + attestation2.subgraphDeploymentId + ) + ); + + // Get funds from fisherman + _graphToken().pullTokens(msg.sender, disputeDeposit); + + // Create the disputes + // The deposit is zero for conflicting attestations + bytes32 dId1 = _createQueryDisputeWithAttestation( + fisherman, + disputeDeposit / 2, + attestation1, + attestationData1 + ); + bytes32 dId2 = _createQueryDisputeWithAttestation( + fisherman, + disputeDeposit / 2, + attestation2, + attestationData2 + ); + + // Store the linked disputes to be resolved + disputes[dId1].relatedDisputeId = dId2; + disputes[dId2].relatedDisputeId = dId1; + + // Emit event that links the two created disputes + emit DisputeLinked(dId1, dId2); + + return (dId1, dId2); + } + + /// @inheritdoc IDisputeManager + function acceptDispute( + bytes32 disputeId, + uint256 tokensSlash + ) external override onlyArbitrator onlyPendingDispute(disputeId) { + require(!_isDisputeInConflict(disputes[disputeId]), DisputeManagerDisputeInConflict(disputeId)); + Dispute storage dispute = disputes[disputeId]; + _acceptDispute(disputeId, dispute, tokensSlash); + } + + /// @inheritdoc IDisputeManager + function acceptDisputeConflict( + bytes32 disputeId, + uint256 tokensSlash, + bool acceptDisputeInConflict, + uint256 tokensSlashRelated + ) external override onlyArbitrator onlyPendingDispute(disputeId) { + require(_isDisputeInConflict(disputes[disputeId]), DisputeManagerDisputeNotInConflict(disputeId)); + Dispute storage dispute = disputes[disputeId]; + _acceptDispute(disputeId, dispute, tokensSlash); + + if (acceptDisputeInConflict) { + _acceptDispute(dispute.relatedDisputeId, disputes[dispute.relatedDisputeId], tokensSlashRelated); + } else { + _drawDispute(dispute.relatedDisputeId, disputes[dispute.relatedDisputeId]); + } + } + + /// @inheritdoc IDisputeManager + function rejectDispute(bytes32 disputeId) external override onlyArbitrator onlyPendingDispute(disputeId) { + Dispute storage dispute = disputes[disputeId]; + require(!_isDisputeInConflict(dispute), DisputeManagerDisputeInConflict(disputeId)); + _rejectDispute(disputeId, dispute); + } + + /// @inheritdoc IDisputeManager + function drawDispute(bytes32 disputeId) external override onlyArbitrator onlyPendingDispute(disputeId) { + Dispute storage dispute = disputes[disputeId]; + _drawDispute(disputeId, dispute); + + if (_isDisputeInConflict(dispute)) { + _drawDispute(dispute.relatedDisputeId, disputes[dispute.relatedDisputeId]); + } + } + + /// @inheritdoc IDisputeManager + function cancelDispute(bytes32 disputeId) external override onlyFisherman(disputeId) onlyPendingDispute(disputeId) { + Dispute storage dispute = disputes[disputeId]; + + // Check if dispute period has finished + require(dispute.createdAt + disputePeriod < block.timestamp, DisputeManagerDisputePeriodNotFinished()); + _cancelDispute(disputeId, dispute); + + if (_isDisputeInConflict(dispute)) { + _cancelDispute(dispute.relatedDisputeId, disputes[dispute.relatedDisputeId]); + } + } + + /// @inheritdoc IDisputeManager + function setArbitrator(address arbitrator) external override onlyOwner { + _setArbitrator(arbitrator); + } + + /// @inheritdoc IDisputeManager + function setDisputePeriod(uint64 disputePeriod) external override onlyOwner { + _setDisputePeriod(disputePeriod); + } + + /// @inheritdoc IDisputeManager + function setDisputeDeposit(uint256 disputeDeposit) external override onlyOwner { + _setDisputeDeposit(disputeDeposit); + } + + /// @inheritdoc IDisputeManager + function setFishermanRewardCut(uint32 fishermanRewardCut_) external override onlyOwner { + _setFishermanRewardCut(fishermanRewardCut_); + } + + /// @inheritdoc IDisputeManager + function setMaxSlashingCut(uint32 maxSlashingCut_) external override onlyOwner { + _setMaxSlashingCut(maxSlashingCut_); + } + + /// @inheritdoc IDisputeManager + function setSubgraphService(address subgraphService) external override onlyOwner { + _setSubgraphService(subgraphService); + } + + /// @inheritdoc IDisputeManager + function encodeReceipt(Attestation.Receipt calldata receipt) external view override returns (bytes32) { + return _encodeReceipt(receipt); + } + + /// @inheritdoc IDisputeManager + function getFishermanRewardCut() external view override returns (uint32) { + return fishermanRewardCut; + } + + /// @inheritdoc IDisputeManager + function getDisputePeriod() external view override returns (uint64) { + return disputePeriod; + } + + /// @inheritdoc IDisputeManager + function getStakeSnapshot(address indexer) external view override returns (uint256) { + IHorizonStaking.Provision memory provision = _graphStaking().getProvision( + indexer, + address(_getSubgraphService()) + ); + return _getStakeSnapshot(indexer, provision.tokens); + } + + /// @inheritdoc IDisputeManager + function areConflictingAttestations( + Attestation.State calldata attestation1, + Attestation.State calldata attestation2 + ) external pure override returns (bool) { + return Attestation.areConflicting(attestation1, attestation2); + } + + /// @inheritdoc IDisputeManager + function getAttestationIndexer(Attestation.State memory attestation) public view returns (address) { + // Get attestation signer. Indexers signs with the allocationId + address allocationId = _recoverSigner(attestation); + + Allocation.State memory alloc = _getSubgraphService().getAllocation(allocationId); + require(alloc.indexer != address(0), DisputeManagerIndexerNotFound(allocationId)); + require( + alloc.subgraphDeploymentId == attestation.subgraphDeploymentId, + DisputeManagerNonMatchingSubgraphDeployment(alloc.subgraphDeploymentId, attestation.subgraphDeploymentId) + ); + return alloc.indexer; + } + + /// @inheritdoc IDisputeManager + function isDisputeCreated(bytes32 disputeId) public view override returns (bool) { + return disputes[disputeId].status != DisputeStatus.Null; + } + + /** + * @notice Create a query dispute passing the parsed attestation. + * To be used in createQueryDispute() and createQueryDisputeConflict() + * to avoid calling parseAttestation() multiple times + * `attestationData` is only passed to be emitted + * @param _fisherman Creator of dispute + * @param _deposit Amount of tokens staked as deposit + * @param _attestation Attestation struct parsed from bytes + * @param _attestationData Attestation bytes submitted by the fisherman + * @return DisputeId + */ + function _createQueryDisputeWithAttestation( + address _fisherman, + uint256 _deposit, + Attestation.State memory _attestation, + bytes memory _attestationData + ) private returns (bytes32) { + // Get the indexer that signed the attestation + address indexer = getAttestationIndexer(_attestation); + + // The indexer is disputable + IHorizonStaking.Provision memory provision = _graphStaking().getProvision( + indexer, + address(_getSubgraphService()) + ); + require(provision.tokens != 0, DisputeManagerZeroTokens()); + + // Create a disputeId + bytes32 disputeId = keccak256( + abi.encodePacked( + _attestation.requestCID, + _attestation.responseCID, + _attestation.subgraphDeploymentId, + indexer, + _fisherman + ) + ); + + // Only one dispute for a (indexer, subgraphDeploymentId) at a time + require(!isDisputeCreated(disputeId), DisputeManagerDisputeAlreadyCreated(disputeId)); + + // Store dispute + uint256 stakeSnapshot = _getStakeSnapshot(indexer, provision.tokens); + disputes[disputeId] = Dispute( + indexer, + _fisherman, + _deposit, + 0, // no related dispute, + DisputeType.QueryDispute, + IDisputeManager.DisputeStatus.Pending, + block.timestamp, + stakeSnapshot + ); + + emit QueryDisputeCreated( + disputeId, + indexer, + _fisherman, + _deposit, + _attestation.subgraphDeploymentId, + _attestationData, + stakeSnapshot + ); + + return disputeId; + } + + /** + * @notice Create indexing dispute internal function. + * @param _fisherman The fisherman creating the dispute + * @param _deposit Amount of tokens staked as deposit + * @param _allocationId Allocation disputed + * @param _poi The POI being disputed + * @return The dispute id + */ + function _createIndexingDisputeWithAllocation( + address _fisherman, + uint256 _deposit, + address _allocationId, + bytes32 _poi + ) private returns (bytes32) { + // Create a disputeId + bytes32 disputeId = keccak256(abi.encodePacked(_allocationId, _poi)); + + // Only one dispute for an allocationId at a time + require(!isDisputeCreated(disputeId), DisputeManagerDisputeAlreadyCreated(disputeId)); + + // Allocation must exist + ISubgraphService subgraphService_ = _getSubgraphService(); + Allocation.State memory alloc = subgraphService_.getAllocation(_allocationId); + address indexer = alloc.indexer; + require(indexer != address(0), DisputeManagerIndexerNotFound(_allocationId)); + + // The indexer must be disputable + IHorizonStaking.Provision memory provision = _graphStaking().getProvision(indexer, address(subgraphService_)); + require(provision.tokens != 0, DisputeManagerZeroTokens()); + + // Store dispute + uint256 stakeSnapshot = _getStakeSnapshot(indexer, provision.tokens); + disputes[disputeId] = Dispute( + alloc.indexer, + _fisherman, + _deposit, + 0, + DisputeType.IndexingDispute, + IDisputeManager.DisputeStatus.Pending, + block.timestamp, + stakeSnapshot + ); + + emit IndexingDisputeCreated(disputeId, alloc.indexer, _fisherman, _deposit, _allocationId, _poi, stakeSnapshot); + + return disputeId; + } + + /** + * @notice Accept a dispute + * @param _disputeId The id of the dispute + * @param _dispute The dispute + * @param _tokensSlashed The amount of tokens to slash + */ + function _acceptDispute(bytes32 _disputeId, Dispute storage _dispute, uint256 _tokensSlashed) private { + uint256 tokensToReward = _slashIndexer(_dispute.indexer, _tokensSlashed, _dispute.stakeSnapshot); + _dispute.status = IDisputeManager.DisputeStatus.Accepted; + _graphToken().pushTokens(_dispute.fisherman, tokensToReward + _dispute.deposit); + + emit DisputeAccepted(_disputeId, _dispute.indexer, _dispute.fisherman, _dispute.deposit + tokensToReward); + } + + /** + * @notice Reject a dispute + * @param _disputeId The id of the dispute + * @param _dispute The dispute + */ + function _rejectDispute(bytes32 _disputeId, Dispute storage _dispute) private { + _dispute.status = IDisputeManager.DisputeStatus.Rejected; + _graphToken().burnTokens(_dispute.deposit); + + emit DisputeRejected(_disputeId, _dispute.indexer, _dispute.fisherman, _dispute.deposit); + } + + /** + * @notice Draw a dispute + * @param _disputeId The id of the dispute + * @param _dispute The dispute + */ + function _drawDispute(bytes32 _disputeId, Dispute storage _dispute) private { + _dispute.status = IDisputeManager.DisputeStatus.Drawn; + _graphToken().pushTokens(_dispute.fisherman, _dispute.deposit); + + emit DisputeDrawn(_disputeId, _dispute.indexer, _dispute.fisherman, _dispute.deposit); + } + + /** + * @notice Cancel a dispute + * @param _disputeId The id of the dispute + * @param _dispute The dispute + */ + function _cancelDispute(bytes32 _disputeId, Dispute storage _dispute) private { + _dispute.status = IDisputeManager.DisputeStatus.Cancelled; + _graphToken().pushTokens(_dispute.fisherman, _dispute.deposit); + + emit DisputeCancelled(_disputeId, _dispute.indexer, _dispute.fisherman, _dispute.deposit); + } + + /** + * @notice Make the subgraph service contract slash the indexer and reward the fisherman. + * Give the fisherman a reward equal to the fishermanRewardPercentage of slashed amount + * @param _indexer Address of the indexer + * @param _tokensSlash Amount of tokens to slash from the indexer + * @param _tokensStakeSnapshot Snapshot of the indexer's stake at the time of the dispute creation + * @return The amount of tokens rewarded to the fisherman + */ + function _slashIndexer( + address _indexer, + uint256 _tokensSlash, + uint256 _tokensStakeSnapshot + ) private returns (uint256) { + ISubgraphService subgraphService_ = _getSubgraphService(); + + // Get slashable amount for indexer + IHorizonStaking.Provision memory provision = _graphStaking().getProvision(_indexer, address(subgraphService_)); + + // Ensure slash amount is within the cap + uint256 maxTokensSlash = _tokensStakeSnapshot.mulPPM(maxSlashingCut); + require( + _tokensSlash != 0 && _tokensSlash <= maxTokensSlash, + DisputeManagerInvalidTokensSlash(_tokensSlash, maxTokensSlash) + ); + + // Rewards amount can only be extracted from service provider tokens so + // we grab the minimum between the slash amount and indexer's tokens + uint256 maxRewardableTokens = Math.min(_tokensSlash, provision.tokens); + uint256 tokensRewards = uint256(fishermanRewardCut).mulPPM(maxRewardableTokens); + + subgraphService_.slash(_indexer, abi.encode(_tokensSlash, tokensRewards)); + return tokensRewards; + } + + /** + * @notice Set the arbitrator address. + * @dev Update the arbitrator to `_arbitrator` + * @param _arbitrator The address of the arbitration contract or party + */ + function _setArbitrator(address _arbitrator) private { + require(_arbitrator != address(0), DisputeManagerInvalidZeroAddress()); + arbitrator = _arbitrator; + emit ArbitratorSet(_arbitrator); + } + + /** + * @notice Set the dispute period. + * @dev Update the dispute period to `_disputePeriod` in seconds + * @param _disputePeriod Dispute period in seconds + */ + function _setDisputePeriod(uint64 _disputePeriod) private { + require(_disputePeriod != 0, DisputeManagerDisputePeriodZero()); + disputePeriod = _disputePeriod; + emit DisputePeriodSet(_disputePeriod); + } + + /** + * @notice Set the dispute deposit required to create a dispute. + * @dev Update the dispute deposit to `_disputeDeposit` Graph Tokens + * @param _disputeDeposit The dispute deposit in Graph Tokens + */ + function _setDisputeDeposit(uint256 _disputeDeposit) private { + require(_disputeDeposit >= MIN_DISPUTE_DEPOSIT, DisputeManagerInvalidDisputeDeposit(_disputeDeposit)); + disputeDeposit = _disputeDeposit; + emit DisputeDepositSet(_disputeDeposit); + } + + /** + * @notice Set the percent reward that the fisherman gets when slashing occurs. + * @dev Update the reward percentage to `_percentage` + * @param _fishermanRewardCut The fisherman reward cut, in PPM + */ + function _setFishermanRewardCut(uint32 _fishermanRewardCut) private { + require( + _fishermanRewardCut <= MAX_FISHERMAN_REWARD_CUT, + DisputeManagerInvalidFishermanReward(_fishermanRewardCut) + ); + fishermanRewardCut = _fishermanRewardCut; + emit FishermanRewardCutSet(_fishermanRewardCut); + } + + /** + * @notice Set the maximum percentage that can be used for slashing indexers. + * @param _maxSlashingCut Max percentage slashing for disputes, in PPM + */ + function _setMaxSlashingCut(uint32 _maxSlashingCut) private { + require(PPMMath.isValidPPM(_maxSlashingCut), DisputeManagerInvalidMaxSlashingCut(_maxSlashingCut)); + maxSlashingCut = _maxSlashingCut; + emit MaxSlashingCutSet(maxSlashingCut); + } + + /** + * @notice Set the subgraph service address. + * @dev Update the subgraph service to `_subgraphService` + * @param _subgraphService The address of the subgraph service contract + */ + function _setSubgraphService(address _subgraphService) private { + require(_subgraphService != address(0), DisputeManagerInvalidZeroAddress()); + subgraphService = ISubgraphService(_subgraphService); + emit SubgraphServiceSet(_subgraphService); + } + + /** + * @notice Get the address of the subgraph service + * @dev Will revert if the subgraph service is not set + * @return The subgraph service address + */ + function _getSubgraphService() private view returns (ISubgraphService) { + require(address(subgraphService) != address(0), DisputeManagerSubgraphServiceNotSet()); + return subgraphService; + } + + /** + * @notice Returns whether the dispute is for a conflicting attestation or not. + * @param _dispute Dispute + * @return True conflicting attestation dispute + */ + function _isDisputeInConflict(Dispute storage _dispute) private view returns (bool) { + return _dispute.relatedDisputeId != bytes32(0); + } + + /** + * @notice Get the total stake snapshot for and indexer. + * @dev A few considerations: + * - We include both indexer and delegators stake. + * - Thawing stake is not excluded from the snapshot. + * - Delegators stake is capped at the delegation ratio to prevent delegators from inflating the snapshot + * to increase the indexer slash amount. + * @param _indexer Indexer address + * @param _indexerStake Indexer's stake + * @return Total stake snapshot + */ + function _getStakeSnapshot(address _indexer, uint256 _indexerStake) private view returns (uint256) { + ISubgraphService subgraphService_ = _getSubgraphService(); + uint256 delegatorsStake = _graphStaking().getDelegationPool(_indexer, address(subgraphService_)).tokens; + uint256 delegatorsStakeMax = _indexerStake * uint256(subgraphService_.getDelegationRatio()); + uint256 stakeSnapshot = _indexerStake + MathUtils.min(delegatorsStake, delegatorsStakeMax); + return stakeSnapshot; + } +} diff --git a/packages/subgraph-service/contracts/DisputeManagerStorage.sol b/packages/subgraph-service/contracts/DisputeManagerStorage.sol new file mode 100644 index 000000000..0f56b4cbe --- /dev/null +++ b/packages/subgraph-service/contracts/DisputeManagerStorage.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IDisputeManager } from "./interfaces/IDisputeManager.sol"; +import { ISubgraphService } from "./interfaces/ISubgraphService.sol"; + +/** + * @title DisputeManagerStorage + * @notice This contract holds all the storage variables for the Dispute Manager contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract DisputeManagerV1Storage { + /// @notice The Subgraph Service contract address + ISubgraphService public subgraphService; + + /// @notice The arbitrator is solely in control of arbitrating disputes + address public arbitrator; + + /// @notice dispute period in seconds + uint64 public disputePeriod; + + /// @notice Deposit required to create a Dispute + uint256 public disputeDeposit; + + /// @notice Percentage of indexer slashed funds to assign as a reward to fisherman in successful dispute. In PPM. + uint32 public fishermanRewardCut; + + /// @notice Maximum percentage of indexer stake that can be slashed on a dispute. In PPM. + uint32 public maxSlashingCut; + + /// @notice List of disputes created + mapping(bytes32 disputeId => IDisputeManager.Dispute dispute) public disputes; +} diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol new file mode 100644 index 000000000..edf918771 --- /dev/null +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -0,0 +1,538 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphTallyCollector } from "@graphprotocol/horizon/contracts/interfaces/IGraphTallyCollector.sol"; +import { IRewardsIssuer } from "@graphprotocol/contracts/contracts/rewards/IRewardsIssuer.sol"; +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ISubgraphService } from "./interfaces/ISubgraphService.sol"; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { DataServicePausableUpgradeable } from "@graphprotocol/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol"; +import { DataService } from "@graphprotocol/horizon/contracts/data-service/DataService.sol"; +import { DataServiceFees } from "@graphprotocol/horizon/contracts/data-service/extensions/DataServiceFees.sol"; +import { Directory } from "./utilities/Directory.sol"; +import { AllocationManager } from "./utilities/AllocationManager.sol"; +import { SubgraphServiceV1Storage } from "./SubgraphServiceStorage.sol"; + +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { Allocation } from "./libraries/Allocation.sol"; +import { LegacyAllocation } from "./libraries/LegacyAllocation.sol"; + +/** + * @title SubgraphService contract + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +contract SubgraphService is + Initializable, + OwnableUpgradeable, + MulticallUpgradeable, + DataService, + DataServicePausableUpgradeable, + DataServiceFees, + Directory, + AllocationManager, + SubgraphServiceV1Storage, + IRewardsIssuer, + ISubgraphService +{ + using PPMMath for uint256; + using Allocation for mapping(address => Allocation.State); + using Allocation for Allocation.State; + using TokenUtils for IGraphToken; + + /** + * @notice Checks that an indexer is registered + * @param indexer The address of the indexer + */ + modifier onlyRegisteredIndexer(address indexer) { + require(indexers[indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(indexer)); + _; + } + + /** + * @notice Constructor for the SubgraphService contract + * @dev DataService and Directory constructors set a bunch of immutable variables + * @param graphController The address of the Graph Controller contract + * @param disputeManager The address of the DisputeManager contract + * @param graphTallyCollector The address of the GraphTallyCollector contract + * @param curation The address of the Curation contract + */ + constructor( + address graphController, + address disputeManager, + address graphTallyCollector, + address curation + ) DataService(graphController) Directory(address(this), disputeManager, graphTallyCollector, curation) { + _disableInitializers(); + } + + /// @inheritdoc ISubgraphService + function initialize( + address owner, + uint256 minimumProvisionTokens, + uint32 maximumDelegationRatio, + uint256 stakeToFeesRatio + ) external initializer { + __Ownable_init(owner); + __Multicall_init(); + __DataService_init(); + __DataServicePausable_init(); + __AllocationManager_init("SubgraphService", "1.0"); + + _setProvisionTokensRange(minimumProvisionTokens, type(uint256).max); + _setDelegationRatio(maximumDelegationRatio); + _setStakeToFeesRatio(stakeToFeesRatio); + } + + /** + * @notice + * @dev Implements {IDataService.register} + * + * Requirements: + * - The indexer must not be already registered + * - The URL must not be empty + * - The provision must be valid according to the subgraph service rules + * + * Emits a {ServiceProviderRegistered} event + * + * @param indexer The address of the indexer to register + * @param data Encoded registration data: + * - address `url`: The URL of the indexer + * - string `geohash`: The geohash of the indexer + * - address `rewardsDestination`: The address where the indexer wants to receive indexing rewards. + * Use zero address for automatic reprovisioning to the subgraph service. + */ + /// @inheritdoc IDataService + function register( + address indexer, + bytes calldata data + ) external override onlyAuthorizedForProvision(indexer) onlyValidProvision(indexer) whenNotPaused { + (string memory url, string memory geohash, address rewardsDestination) = abi.decode( + data, + (string, string, address) + ); + + require(bytes(url).length > 0, SubgraphServiceEmptyUrl()); + require(bytes(geohash).length > 0, SubgraphServiceEmptyGeohash()); + require(indexers[indexer].registeredAt == 0, SubgraphServiceIndexerAlreadyRegistered()); + + // Register the indexer + indexers[indexer] = Indexer({ registeredAt: block.timestamp, url: url, geoHash: geohash }); + if (rewardsDestination != address(0)) { + _setRewardsDestination(indexer, rewardsDestination); + } + + emit ServiceProviderRegistered(indexer, data); + } + + /** + * @notice Accept staged parameters in the provision of a service provider + * @dev Implements {IDataService-acceptProvisionPendingParameters} + * + * Requirements: + * - The indexer must be registered + * - Must have previously staged provision parameters, using {IHorizonStaking-setProvisionParameters} + * - The new provision parameters must be valid according to the subgraph service rules + * + * Emits a {ProvisionPendingParametersAccepted} event + * + * @param indexer The address of the indexer to accept the provision for + */ + /// @inheritdoc IDataService + function acceptProvisionPendingParameters( + address indexer, + bytes calldata + ) external override onlyAuthorizedForProvision(indexer) onlyRegisteredIndexer(indexer) whenNotPaused { + _checkProvisionTokens(indexer); + _acceptProvisionParameters(indexer); + emit ProvisionPendingParametersAccepted(indexer); + } + + /** + * @notice Allocates tokens to subgraph deployment, manifesting the indexer's commitment to index it + * @dev This is the equivalent of the `allocate` function in the legacy Staking contract. + * + * Requirements: + * - The indexer must be registered + * - The provision must be valid according to the subgraph service rules + * - Allocation id cannot be zero + * - Allocation id cannot be reused from the legacy staking contract + * - The indexer must have enough available tokens to allocate + * + * The `allocationProof` is a 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationId)`. + * + * See {AllocationManager-allocate} for more details. + * + * Emits {ServiceStarted} and {AllocationCreated} events + * + * @param indexer The address of the indexer + * @param data Encoded data: + * - bytes32 `subgraphDeploymentId`: The id of the subgraph deployment + * - uint256 `tokens`: The amount of tokens to allocate + * - address `allocationId`: The id of the allocation + * - bytes `allocationProof`: Signed proof of the allocation id address ownership + */ + /// @inheritdoc IDataService + function startService( + address indexer, + bytes calldata data + ) + external + override + onlyAuthorizedForProvision(indexer) + onlyValidProvision(indexer) + onlyRegisteredIndexer(indexer) + whenNotPaused + { + (bytes32 subgraphDeploymentId, uint256 tokens, address allocationId, bytes memory allocationProof) = abi.decode( + data, + (bytes32, uint256, address, bytes) + ); + _allocate(indexer, allocationId, subgraphDeploymentId, tokens, allocationProof, _delegationRatio); + emit ServiceStarted(indexer, data); + } + + /** + * @notice Close an allocation, indicating that the indexer has stopped indexing the subgraph deployment + * @dev This is the equivalent of the `closeAllocation` function in the legacy Staking contract. + * There are a few notable differences with the legacy function: + * - allocations are nowlong lived. All service payments, including indexing rewards, should be collected periodically + * without the need of closing the allocation. Allocations should only be closed when indexers want to reclaim the allocated + * tokens for other purposes. + * - No POI is required to close an allocation. Indexers should present POIs to collect indexing rewards using {collect}. + * + * Requirements: + * - The indexer must be registered + * - Allocation must exist and be open + * + * Emits {ServiceStopped} and {AllocationClosed} events + * + * @param indexer The address of the indexer + * @param data Encoded data: + * - address `allocationId`: The id of the allocation + */ + /// @inheritdoc IDataService + function stopService( + address indexer, + bytes calldata data + ) external override onlyAuthorizedForProvision(indexer) onlyRegisteredIndexer(indexer) whenNotPaused { + address allocationId = abi.decode(data, (address)); + require( + _allocations.get(allocationId).indexer == indexer, + SubgraphServiceAllocationNotAuthorized(indexer, allocationId) + ); + _closeAllocation(allocationId); + emit ServiceStopped(indexer, data); + } + + /** + * @notice Collects payment for the service provided by the indexer + * Allows collecting different types of payments such as query fees and indexing rewards. + * It uses Graph Horizon payments protocol to process payments. + * Reverts if the payment type is not supported. + * @dev This function is the equivalent of the `collect` function for query fees and the `closeAllocation` function + * for indexing rewards in the legacy Staking contract. + * + * Requirements: + * - The indexer must be registered + * - The provision must be valid according to the subgraph service rules + * + * Emits a {ServicePaymentCollected} event. Emits payment type specific events. + * + * For query fees, see {SubgraphService-_collectQueryFees} for more details. + * For indexing rewards, see {AllocationManager-_collectIndexingRewards} for more details. + * + * @param indexer The address of the indexer + * @param paymentType The type of payment to collect as defined in {IGraphPayments} + */ + /// @inheritdoc IDataService + function collect( + address indexer, + IGraphPayments.PaymentTypes paymentType, + bytes calldata data + ) + external + override + onlyAuthorizedForProvision(indexer) + onlyValidProvision(indexer) + onlyRegisteredIndexer(indexer) + whenNotPaused + returns (uint256) + { + uint256 paymentCollected = 0; + + if (paymentType == IGraphPayments.PaymentTypes.QueryFee) { + IGraphTallyCollector.SignedRAV memory signedRav = abi.decode(data, (IGraphTallyCollector.SignedRAV)); + require( + signedRav.rav.serviceProvider == indexer, + SubgraphServiceIndexerMismatch(signedRav.rav.serviceProvider, indexer) + ); + paymentCollected = _collectQueryFees(signedRav); + } else if (paymentType == IGraphPayments.PaymentTypes.IndexingRewards) { + (address allocationId, bytes32 poi) = abi.decode(data, (address, bytes32)); + require( + _allocations.get(allocationId).indexer == indexer, + SubgraphServiceAllocationNotAuthorized(indexer, allocationId) + ); + paymentCollected = _collectIndexingRewards(allocationId, poi, _delegationRatio); + } else { + revert SubgraphServiceInvalidPaymentType(paymentType); + } + + emit ServicePaymentCollected(indexer, paymentType, paymentCollected); + return paymentCollected; + } + + /** + * @notice See {IHorizonStaking-slash} for more details. + * @dev Slashing is delegated to the {DisputeManager} contract which is the only one that can call this + * function. + */ + /// @inheritdoc IDataService + function slash(address indexer, bytes calldata data) external override onlyDisputeManager { + (uint256 tokens, uint256 reward) = abi.decode(data, (uint256, uint256)); + _graphStaking().slash(indexer, tokens, reward, address(_disputeManager())); + emit ServiceProviderSlashed(indexer, tokens); + } + + /// @inheritdoc ISubgraphService + function closeStaleAllocation(address allocationId) external override whenNotPaused { + Allocation.State memory allocation = _allocations.get(allocationId); + require(allocation.isStale(maxPOIStaleness), SubgraphServiceCannotForceCloseAllocation(allocationId)); + require(!allocation.isAltruistic(), SubgraphServiceAllocationIsAltruistic(allocationId)); + _closeAllocation(allocationId); + } + + /// @inheritdoc ISubgraphService + function resizeAllocation( + address indexer, + address allocationId, + uint256 tokens + ) + external + onlyAuthorizedForProvision(indexer) + onlyValidProvision(indexer) + onlyRegisteredIndexer(indexer) + whenNotPaused + { + require( + _allocations.get(allocationId).indexer == indexer, + SubgraphServiceAllocationNotAuthorized(indexer, allocationId) + ); + _resizeAllocation(allocationId, tokens, _delegationRatio); + } + + /// @inheritdoc ISubgraphService + function migrateLegacyAllocation( + address indexer, + address allocationId, + bytes32 subgraphDeploymentID + ) external override onlyOwner { + _migrateLegacyAllocation(indexer, allocationId, subgraphDeploymentID); + } + + /// @inheritdoc ISubgraphService + function setPauseGuardian(address pauseGuardian, bool allowed) external override onlyOwner { + _setPauseGuardian(pauseGuardian, allowed); + } + + /// @inheritdoc ISubgraphService + function setRewardsDestination(address rewardsDestination) external override { + _setRewardsDestination(msg.sender, rewardsDestination); + } + + /// @inheritdoc ISubgraphService + function setMinimumProvisionTokens(uint256 minimumProvisionTokens) external override onlyOwner { + _setProvisionTokensRange(minimumProvisionTokens, DEFAULT_MAX_PROVISION_TOKENS); + } + + /// @inheritdoc ISubgraphService + function setDelegationRatio(uint32 delegationRatio) external override onlyOwner { + _setDelegationRatio(delegationRatio); + } + + /// @inheritdoc ISubgraphService + function setStakeToFeesRatio(uint256 stakeToFeesRatio_) external override onlyOwner { + _setStakeToFeesRatio(stakeToFeesRatio_); + } + + /// @inheritdoc ISubgraphService + function setMaxPOIStaleness(uint256 maxPOIStaleness) external override onlyOwner { + _setMaxPOIStaleness(maxPOIStaleness); + } + + /// @inheritdoc ISubgraphService + function setCurationCut(uint256 curationCut) external override onlyOwner { + require(PPMMath.isValidPPM(curationCut), SubgraphServiceInvalidCurationCut(curationCut)); + curationFeesCut = curationCut; + emit CurationCutSet(curationCut); + } + + /// @inheritdoc ISubgraphService + function getAllocation(address allocationId) external view override returns (Allocation.State memory) { + return _allocations[allocationId]; + } + + /// @inheritdoc IRewardsIssuer + function getAllocationData( + address allocationId + ) external view override returns (address, bytes32, uint256, uint256, uint256) { + Allocation.State memory allo = _allocations[allocationId]; + return ( + allo.indexer, + allo.subgraphDeploymentId, + allo.tokens, + allo.accRewardsPerAllocatedToken, + allo.accRewardsPending + ); + } + + /// @inheritdoc IRewardsIssuer + function getSubgraphAllocatedTokens(bytes32 subgraphDeploymentId) external view override returns (uint256) { + return _subgraphAllocatedTokens[subgraphDeploymentId]; + } + + /// @inheritdoc ISubgraphService + function getLegacyAllocation(address allocationId) external view override returns (LegacyAllocation.State memory) { + return _legacyAllocations[allocationId]; + } + + /// @inheritdoc ISubgraphService + function getDisputeManager() external view override returns (address) { + return address(_disputeManager()); + } + + /// @inheritdoc ISubgraphService + function getGraphTallyCollector() external view override returns (address) { + return address(_graphTallyCollector()); + } + + /// @inheritdoc ISubgraphService + function getCuration() external view override returns (address) { + return address(_curation()); + } + + /// @inheritdoc ISubgraphService + function encodeAllocationProof(address indexer, address allocationId) external view override returns (bytes32) { + return _encodeAllocationProof(indexer, allocationId); + } + + /// @inheritdoc ISubgraphService + function isOverAllocated(address indexer) external view override returns (bool) { + return _isOverAllocated(indexer, _delegationRatio); + } + + // -- Data service parameter getters -- + /** + * @notice Getter for the accepted thawing period range for provisions + * @dev This override ensures {ProvisionManager} uses the thawing period from the {DisputeManager} + * @return The minimum thawing period which is defined by {DisputeManager-getDisputePeriod} + * @return The maximum is unbounded + */ + function _getThawingPeriodRange() internal view override returns (uint64, uint64) { + return (_disputeManager().getDisputePeriod(), DEFAULT_MAX_THAWING_PERIOD); + } + + /** + * @notice Getter for the accepted verifier cut range for provisions + * @return The minimum verifier cut which is defined by the fisherman reward cut {DisputeManager-getFishermanRewardCut} + * @return The maximum is 100% in PPM + */ + function _getVerifierCutRange() internal view override returns (uint32, uint32) { + return (_disputeManager().getFishermanRewardCut(), DEFAULT_MAX_VERIFIER_CUT); + } + + /** + * @notice Collect query fees + * Stake equal to the amount being collected times the `stakeToFeesRatio` is locked into a stake claim. + * This claim can be released at a later stage once expired. + * + * It's important to note that before collecting this function will attempt to release any expired stake claims. + * This could lead to an out of gas error if there are too many expired claims. In that case, the indexer will need to + * manually release the claims, see {IDataServiceFees-releaseStake}, before attempting to collect again. + * + * @dev This function is the equivalent of the legacy `collect` function for query fees. + * @dev Uses the {GraphTallyCollector} to collect payment from Graph Horizon payments protocol. + * Fees are distributed to service provider and delegators by {GraphPayments}, though curators + * share is distributed by this function. + * + * Query fees can be collected on closed allocations. + * + * Requirements: + * - Indexer must have enough available tokens to lock as economic security for fees + * + * Emits a {StakeClaimsReleased} event, and a {StakeClaimReleased} event for each claim released. + * Emits a {StakeClaimLocked} event. + * Emits a {QueryFeesCollected} event. + * + * @param _signedRav Signed RAV + * @return The amount of fees collected + */ + function _collectQueryFees(IGraphTallyCollector.SignedRAV memory _signedRav) private returns (uint256) { + address indexer = _signedRav.rav.serviceProvider; + + // Check that collectionId (256 bits) is a valid address (160 bits) + // collectionId is expected to be a zero padded address so it's safe to cast to uint160 + require( + uint256(_signedRav.rav.collectionId) <= type(uint160).max, + SubgraphServiceInvalidCollectionId(_signedRav.rav.collectionId) + ); + address allocationId = address(uint160(uint256(_signedRav.rav.collectionId))); + Allocation.State memory allocation = _allocations.get(allocationId); + + // Check RAV is consistent - RAV indexer must match the allocation's indexer + require(allocation.indexer == indexer, SubgraphServiceInvalidRAV(indexer, allocation.indexer)); + bytes32 subgraphDeploymentId = allocation.subgraphDeploymentId; + + // release expired stake claims + _releaseStake(indexer, 0); + + // Collect from GraphPayments - only curators cut is sent back to the subgraph service + uint256 balanceBefore = _graphToken().balanceOf(address(this)); + + uint256 curationCut = _curation().isCurated(subgraphDeploymentId) ? curationFeesCut : 0; + uint256 tokensCollected = _graphTallyCollector().collect( + IGraphPayments.PaymentTypes.QueryFee, + abi.encode(_signedRav, curationCut) + ); + + uint256 balanceAfter = _graphToken().balanceOf(address(this)); + require(balanceAfter >= balanceBefore, SubgraphServiceInconsistentCollection(balanceBefore, balanceAfter)); + uint256 tokensCurators = balanceAfter - balanceBefore; + + if (tokensCollected > 0) { + // lock stake as economic security for fees + uint256 tokensToLock = tokensCollected * stakeToFeesRatio; + uint256 unlockTimestamp = block.timestamp + _disputeManager().getDisputePeriod(); + _lockStake(indexer, tokensToLock, unlockTimestamp); + + if (tokensCurators > 0) { + // curation collection changes subgraph signal so we take rewards snapshot + _graphRewardsManager().onSubgraphSignalUpdate(subgraphDeploymentId); + + // Send GRT and bookkeep by calling collect() + _graphToken().pushTokens(address(_curation()), tokensCurators); + _curation().collect(subgraphDeploymentId, tokensCurators); + } + } + + emit QueryFeesCollected(indexer, _signedRav.rav.payer, tokensCollected, tokensCurators); + return tokensCollected; + } + + /** + * @notice Set the stake to fees ratio. + * @param _stakeToFeesRatio The stake to fees ratio + */ + function _setStakeToFeesRatio(uint256 _stakeToFeesRatio) private { + require(_stakeToFeesRatio != 0, SubgraphServiceInvalidZeroStakeToFeesRatio()); + stakeToFeesRatio = _stakeToFeesRatio; + emit StakeToFeesRatioSet(_stakeToFeesRatio); + } +} diff --git a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol new file mode 100644 index 000000000..9e08d5505 --- /dev/null +++ b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { ISubgraphService } from "./interfaces/ISubgraphService.sol"; + +/** + * @title SubgraphServiceStorage + * @notice This contract holds all the storage variables for the Subgraph Service contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract SubgraphServiceV1Storage { + /// @notice Service providers registered in the data service + mapping(address indexer => ISubgraphService.Indexer details) public indexers; + + ///@notice Multiplier for how many tokens back collected query fees + uint256 public stakeToFeesRatio; + + /// @notice The cut curators take from query fee payments. In PPM. + uint256 public curationFeesCut; +} diff --git a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol new file mode 100644 index 000000000..c8754ca44 --- /dev/null +++ b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol @@ -0,0 +1,554 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { Attestation } from "../libraries/Attestation.sol"; + +/** + * @title IDisputeManager + * @notice Interface for the {Dispute Manager} contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IDisputeManager { + /// @notice Types of disputes that can be created + enum DisputeType { + Null, + IndexingDispute, + QueryDispute + } + + /// @notice Status of a dispute + enum DisputeStatus { + Null, + Accepted, + Rejected, + Drawn, + Pending, + Cancelled + } + + /** + * @notice Dispute details + * @param indexer The indexer that is being disputed + * @param fisherman The fisherman that created the dispute + * @param deposit The amount of tokens deposited by the fisherman + * @param relatedDisputeId The link to a related dispute, used when creating dispute via conflicting attestations + * @param disputeType The type of dispute + * @param status The status of the dispute + * @param createdAt The timestamp when the dispute was created + * @param stakeSnapshot The stake snapshot of the indexer at the time of the dispute (includes delegation up to the delegation ratio) + */ + struct Dispute { + address indexer; + address fisherman; + uint256 deposit; + bytes32 relatedDisputeId; + DisputeType disputeType; + DisputeStatus status; + uint256 createdAt; + uint256 stakeSnapshot; + } + + /** + * @notice Emitted when arbitrator is set. + * @param arbitrator The address of the arbitrator. + */ + event ArbitratorSet(address indexed arbitrator); + + /** + * @notice Emitted when dispute period is set. + * @param disputePeriod The dispute period in seconds. + */ + event DisputePeriodSet(uint64 disputePeriod); + + /** + * @notice Emitted when dispute deposit is set. + * @param disputeDeposit The dispute deposit required to create a dispute. + */ + event DisputeDepositSet(uint256 disputeDeposit); + + /** + * @notice Emitted when max slashing cut is set. + * @param maxSlashingCut The maximum slashing cut that can be set. + */ + event MaxSlashingCutSet(uint32 maxSlashingCut); + + /** + * @notice Emitted when fisherman reward cut is set. + * @param fishermanRewardCut The fisherman reward cut. + */ + event FishermanRewardCutSet(uint32 fishermanRewardCut); + + /** + * @notice Emitted when subgraph service is set. + * @param subgraphService The address of the subgraph service. + */ + event SubgraphServiceSet(address indexed subgraphService); + + /** + * @dev Emitted when a query dispute is created for `subgraphDeploymentId` and `indexer` + * by `fisherman`. + * The event emits the amount of `tokens` deposited by the fisherman and `attestation` submitted. + * @param disputeId The dispute id + * @param indexer The indexer address + * @param fisherman The fisherman address + * @param tokens The amount of tokens deposited by the fisherman + * @param subgraphDeploymentId The subgraph deployment id + * @param attestation The attestation + * @param stakeSnapshot The stake snapshot of the indexer at the time of the dispute + */ + event QueryDisputeCreated( + bytes32 indexed disputeId, + address indexed indexer, + address indexed fisherman, + uint256 tokens, + bytes32 subgraphDeploymentId, + bytes attestation, + uint256 stakeSnapshot + ); + + /** + * @dev Emitted when an indexing dispute is created for `allocationId` and `indexer` + * by `fisherman`. + * The event emits the amount of `tokens` deposited by the fisherman. + * @param disputeId The dispute id + * @param indexer The indexer address + * @param fisherman The fisherman address + * @param tokens The amount of tokens deposited by the fisherman + * @param allocationId The allocation id + * @param poi The POI + * @param stakeSnapshot The stake snapshot of the indexer at the time of the dispute + */ + event IndexingDisputeCreated( + bytes32 indexed disputeId, + address indexed indexer, + address indexed fisherman, + uint256 tokens, + address allocationId, + bytes32 poi, + uint256 stakeSnapshot + ); + + /** + * @dev Emitted when arbitrator accepts a `disputeId` to `indexer` created by `fisherman`. + * The event emits the amount `tokens` transferred to the fisherman, the deposit plus reward. + * @param disputeId The dispute id + * @param indexer The indexer address + * @param fisherman The fisherman address + * @param tokens The amount of tokens transferred to the fisherman, the deposit plus reward + */ + event DisputeAccepted( + bytes32 indexed disputeId, + address indexed indexer, + address indexed fisherman, + uint256 tokens + ); + + /** + * @dev Emitted when arbitrator rejects a `disputeId` for `indexer` created by `fisherman`. + * The event emits the amount `tokens` burned from the fisherman deposit. + * @param disputeId The dispute id + * @param indexer The indexer address + * @param fisherman The fisherman address + * @param tokens The amount of tokens burned from the fisherman deposit + */ + event DisputeRejected( + bytes32 indexed disputeId, + address indexed indexer, + address indexed fisherman, + uint256 tokens + ); + + /** + * @dev Emitted when arbitrator draw a `disputeId` for `indexer` created by `fisherman`. + * The event emits the amount `tokens` used as deposit and returned to the fisherman. + * @param disputeId The dispute id + * @param indexer The indexer address + * @param fisherman The fisherman address + * @param tokens The amount of tokens returned to the fisherman - the deposit + */ + event DisputeDrawn(bytes32 indexed disputeId, address indexed indexer, address indexed fisherman, uint256 tokens); + + /** + * @dev Emitted when two disputes are in conflict to link them. + * This event will be emitted after each DisputeCreated event is emitted + * for each of the individual disputes. + * @param disputeId1 The first dispute id + * @param disputeId2 The second dispute id + */ + event DisputeLinked(bytes32 indexed disputeId1, bytes32 indexed disputeId2); + + /** + * @dev Emitted when a dispute is cancelled by the fisherman. + * The event emits the amount `tokens` returned to the fisherman. + * @param disputeId The dispute id + * @param indexer The indexer address + * @param fisherman The fisherman address + * @param tokens The amount of tokens returned to the fisherman - the deposit + */ + event DisputeCancelled( + bytes32 indexed disputeId, + address indexed indexer, + address indexed fisherman, + uint256 tokens + ); + + // -- Errors -- + + /** + * @notice Thrown when the caller is not the arbitrator + */ + error DisputeManagerNotArbitrator(); + + /** + * @notice Thrown when the caller is not the fisherman + */ + error DisputeManagerNotFisherman(); + + /** + * @notice Thrown when the address is the zero address + */ + error DisputeManagerInvalidZeroAddress(); + + /** + * @notice Thrown when the dispute period is zero + */ + error DisputeManagerDisputePeriodZero(); + + /** + * @notice Thrown when the indexer being disputed has no provisioned tokens + */ + error DisputeManagerZeroTokens(); + + /** + * @notice Thrown when the dispute id is invalid + * @param disputeId The dispute id + */ + error DisputeManagerInvalidDispute(bytes32 disputeId); + + /** + * @notice Thrown when the dispute deposit is invalid - less than the minimum dispute deposit + * @param disputeDeposit The dispute deposit + */ + error DisputeManagerInvalidDisputeDeposit(uint256 disputeDeposit); + + /** + * @notice Thrown when the fisherman reward cut is invalid + * @param cut The fisherman reward cut + */ + error DisputeManagerInvalidFishermanReward(uint32 cut); + + /** + * @notice Thrown when the max slashing cut is invalid + * @param maxSlashingCut The max slashing cut + */ + error DisputeManagerInvalidMaxSlashingCut(uint32 maxSlashingCut); + + /** + * @notice Thrown when the tokens slash is invalid + * @param tokensSlash The tokens slash + * @param maxTokensSlash The max tokens slash + */ + error DisputeManagerInvalidTokensSlash(uint256 tokensSlash, uint256 maxTokensSlash); + + /** + * @notice Thrown when the dispute is not pending + * @param status The status of the dispute + */ + error DisputeManagerDisputeNotPending(IDisputeManager.DisputeStatus status); + + /** + * @notice Thrown when the dispute is already created + * @param disputeId The dispute id + */ + error DisputeManagerDisputeAlreadyCreated(bytes32 disputeId); + + /** + * @notice Thrown when the dispute period is not finished + */ + error DisputeManagerDisputePeriodNotFinished(); + + /** + * @notice Thrown when the dispute is in conflict + * @param disputeId The dispute id + */ + error DisputeManagerDisputeInConflict(bytes32 disputeId); + + /** + * @notice Thrown when the dispute is not in conflict + * @param disputeId The dispute id + */ + error DisputeManagerDisputeNotInConflict(bytes32 disputeId); + + /** + * @notice Thrown when the dispute must be accepted + * @param disputeId The dispute id + * @param relatedDisputeId The related dispute id + */ + error DisputeManagerMustAcceptRelatedDispute(bytes32 disputeId, bytes32 relatedDisputeId); + + /** + * @notice Thrown when the indexer is not found + * @param allocationId The allocation id + */ + error DisputeManagerIndexerNotFound(address allocationId); + + /** + * @notice Thrown when the subgraph deployment is not matching + * @param subgraphDeploymentId1 The subgraph deployment id of the first attestation + * @param subgraphDeploymentId2 The subgraph deployment id of the second attestation + */ + error DisputeManagerNonMatchingSubgraphDeployment(bytes32 subgraphDeploymentId1, bytes32 subgraphDeploymentId2); + + /** + * @notice Thrown when the attestations are not conflicting + * @param requestCID1 The request CID of the first attestation + * @param responseCID1 The response CID of the first attestation + * @param subgraphDeploymentId1 The subgraph deployment id of the first attestation + * @param requestCID2 The request CID of the second attestation + * @param responseCID2 The response CID of the second attestation + * @param subgraphDeploymentId2 The subgraph deployment id of the second attestation + */ + error DisputeManagerNonConflictingAttestations( + bytes32 requestCID1, + bytes32 responseCID1, + bytes32 subgraphDeploymentId1, + bytes32 requestCID2, + bytes32 responseCID2, + bytes32 subgraphDeploymentId2 + ); + + /** + * @notice Thrown when attempting to get the subgraph service before it is set + */ + error DisputeManagerSubgraphServiceNotSet(); + + /** + * @notice Initialize this contract. + * @param owner The owner of the contract + * @param arbitrator Arbitrator role + * @param disputePeriod Dispute period in seconds + * @param disputeDeposit Deposit required to create a Dispute + * @param fishermanRewardCut_ Percent of slashed funds for fisherman (ppm) + * @param maxSlashingCut_ Maximum percentage of indexer stake that can be slashed (ppm) + */ + function initialize( + address owner, + address arbitrator, + uint64 disputePeriod, + uint256 disputeDeposit, + uint32 fishermanRewardCut_, + uint32 maxSlashingCut_ + ) external; + + /** + * @notice Set the dispute period. + * @dev Update the dispute period to `_disputePeriod` in seconds + * @param disputePeriod Dispute period in seconds + */ + function setDisputePeriod(uint64 disputePeriod) external; + + /** + * @notice Set the arbitrator address. + * @dev Update the arbitrator to `_arbitrator` + * @param arbitrator The address of the arbitration contract or party + */ + function setArbitrator(address arbitrator) external; + + /** + * @notice Set the dispute deposit required to create a dispute. + * @dev Update the dispute deposit to `_disputeDeposit` Graph Tokens + * @param disputeDeposit The dispute deposit in Graph Tokens + */ + function setDisputeDeposit(uint256 disputeDeposit) external; + + /** + * @notice Set the percent reward that the fisherman gets when slashing occurs. + * @dev Update the reward percentage to `_percentage` + * @param fishermanRewardCut_ Reward as a percentage of indexer stake + */ + function setFishermanRewardCut(uint32 fishermanRewardCut_) external; + + /** + * @notice Set the maximum percentage that can be used for slashing indexers. + * @param maxSlashingCut_ Max percentage slashing for disputes + */ + function setMaxSlashingCut(uint32 maxSlashingCut_) external; + + /** + * @notice Set the subgraph service address. + * @dev Update the subgraph service to `_subgraphService` + * @param subgraphService The address of the subgraph service contract + */ + function setSubgraphService(address subgraphService) external; + + // -- Dispute -- + + /** + * @notice Create a query dispute for the arbitrator to resolve. + * This function is called by a fisherman and it will pull `disputeDeposit` GRT tokens. + * + * * Requirements: + * - fisherman must have previously approved this contract to pull `disputeDeposit` amount + * of tokens from their balance. + * + * @param attestationData Attestation bytes submitted by the fisherman + * @return The dispute id + */ + function createQueryDispute(bytes calldata attestationData) external returns (bytes32); + + /** + * @notice Create query disputes for two conflicting attestations. + * A conflicting attestation is a proof presented by two different indexers + * where for the same request on a subgraph the response is different. + * Two linked disputes will be created and if the arbitrator resolve one, the other + * one will be automatically resolved. Note that: + * - it's not possible to reject a conflicting query dispute as by definition at least one + * of the attestations is incorrect. + * - if both attestations are proven to be incorrect, the arbitrator can slash the indexer twice. + * Requirements: + * - fisherman must have previously approved this contract to pull `disputeDeposit` amount + * of tokens from their balance. + * @param attestationData1 First attestation data submitted + * @param attestationData2 Second attestation data submitted + * @return The first dispute id + * @return The second dispute id + */ + function createQueryDisputeConflict( + bytes calldata attestationData1, + bytes calldata attestationData2 + ) external returns (bytes32, bytes32); + + /** + * @notice Create an indexing dispute for the arbitrator to resolve. + * The disputes are created in reference to an allocationId and specifically + * a POI for that allocation. + * This function is called by a fisherman and it will pull `disputeDeposit` GRT tokens. + * + * Requirements: + * - fisherman must have previously approved this contract to pull `disputeDeposit` amount + * of tokens from their balance. + * + * @param allocationId The allocation to dispute + * @param poi The Proof of Indexing (POI) being disputed + * @return The dispute id + */ + function createIndexingDispute(address allocationId, bytes32 poi) external returns (bytes32); + + // -- Arbitrator -- + + /** + * @notice The arbitrator accepts a dispute as being valid. + * This function will revert if the indexer is not slashable, whether because it does not have + * any stake available or the slashing percentage is configured to be zero. In those cases + * a dispute must be resolved using drawDispute or rejectDispute. + * This function will also revert if the dispute is in conflict, to accept a conflicting dispute + * use acceptDisputeConflict. + * @dev Accept a dispute with Id `disputeId` + * @param disputeId Id of the dispute to be accepted + * @param tokensSlash Amount of tokens to slash from the indexer + */ + function acceptDispute(bytes32 disputeId, uint256 tokensSlash) external; + + /** + * @notice The arbitrator accepts a conflicting dispute as being valid. + * This function will revert if the indexer is not slashable, whether because it does not have + * any stake available or the slashing percentage is configured to be zero. In those cases + * a dispute must be resolved using drawDispute. + * @param disputeId Id of the dispute to be accepted + * @param tokensSlash Amount of tokens to slash from the indexer for the first dispute + * @param acceptDisputeInConflict Accept the conflicting dispute. Otherwise it will be drawn automatically + * @param tokensSlashRelated Amount of tokens to slash from the indexer for the related dispute in case + * acceptDisputeInConflict is true, otherwise it will be ignored + */ + function acceptDisputeConflict( + bytes32 disputeId, + uint256 tokensSlash, + bool acceptDisputeInConflict, + uint256 tokensSlashRelated + ) external; + + /** + * @notice The arbitrator rejects a dispute as being invalid. + * Note that conflicting query disputes cannot be rejected. + * @dev Reject a dispute with Id `disputeId` + * @param disputeId Id of the dispute to be rejected + */ + function rejectDispute(bytes32 disputeId) external; + + /** + * @notice The arbitrator draws dispute. + * Note that drawing a conflicting query dispute should not be possible however it is allowed + * to give arbitrators greater flexibility when resolving disputes. + * @dev Ignore a dispute with Id `disputeId` + * @param disputeId Id of the dispute to be disregarded + */ + function drawDispute(bytes32 disputeId) external; + + /** + * @notice Once the dispute period ends, if the dispute status remains Pending, + * the fisherman can cancel the dispute and get back their initial deposit. + * Note that cancelling a conflicting query dispute will also cancel the related dispute. + * @dev Cancel a dispute with Id `disputeId` + * @param disputeId Id of the dispute to be cancelled + */ + function cancelDispute(bytes32 disputeId) external; + + // -- Getters -- + + /** + * @notice Get the fisherman reward cut. + * @return Fisherman reward cut in percentage (ppm) + */ + function getFishermanRewardCut() external view returns (uint32); + + /** + * @notice Get the dispute period. + * @return Dispute period in seconds + */ + function getDisputePeriod() external view returns (uint64); + + /** + * @notice Return whether a dispute exists or not. + * @dev Return if dispute with Id `disputeId` exists + * @param disputeId True if dispute already exists + * @return True if dispute already exists + */ + function isDisputeCreated(bytes32 disputeId) external view returns (bool); + + /** + * @notice Get the message hash that a indexer used to sign the receipt. + * Encodes a receipt using a domain separator, as described on + * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification. + * @dev Return the message hash used to sign the receipt + * @param receipt Receipt returned by indexer and submitted by fisherman + * @return Message hash used to sign the receipt + */ + function encodeReceipt(Attestation.Receipt memory receipt) external view returns (bytes32); + + /** + * @notice Returns the indexer that signed an attestation. + * @param attestation Attestation + * @return indexer address + */ + function getAttestationIndexer(Attestation.State memory attestation) external view returns (address); + + /** + * @notice Get the stake snapshot for an indexer. + * @param indexer The indexer address + * @return The stake snapshot + */ + function getStakeSnapshot(address indexer) external view returns (uint256); + + /** + * @notice Checks if two attestations are conflicting + * @param attestation1 The first attestation + * @param attestation2 The second attestation + * @return Whether the attestations are conflicting + */ + function areConflictingAttestations( + Attestation.State memory attestation1, + Attestation.State memory attestation2 + ) external pure returns (bool); +} diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol new file mode 100644 index 000000000..6ae78f5b7 --- /dev/null +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDataServiceFees } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataServiceFees.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; + +import { Allocation } from "../libraries/Allocation.sol"; +import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; + +/** + * @title Interface for the {SubgraphService} contract + * @dev This interface extends {IDataServiceFees} and {IDataService}. + * @notice The Subgraph Service is a data service built on top of Graph Horizon that supports the use case of + * subgraph indexing and querying. The {SubgraphService} contract implements the flows described in the Data + * Service framework to allow indexers to register as subgraph service providers, create allocations to signal + * their commitment to index a subgraph, and collect fees for indexing and querying services. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface ISubgraphService is IDataServiceFees { + /** + * @notice Indexer details + * @param registeredAt The timestamp when the indexer registered + * @param url The URL where the indexer can be reached at for queries + * @param geoHash The indexer's geo location, expressed as a geo hash + */ + struct Indexer { + uint256 registeredAt; + string url; + string geoHash; + } + + /** + * @notice Emitted when a subgraph service collects query fees from Graph Payments + * @param serviceProvider The address of the service provider + * @param payer The address paying for the query fees + * @param tokensCollected The amount of tokens collected + * @param tokensCurators The amount of tokens curators receive + */ + event QueryFeesCollected( + address indexed serviceProvider, + address indexed payer, + uint256 tokensCollected, + uint256 tokensCurators + ); + + /** + * @notice Emitted when the stake to fees ratio is set. + * @param ratio The stake to fees ratio + */ + event StakeToFeesRatioSet(uint256 ratio); + + /** + * @notice Emitted when curator cuts are set + * @param curationCut The curation cut + */ + event CurationCutSet(uint256 curationCut); + + /** + * @notice Thrown when trying to set a curation cut that is not a valid PPM value + * @param curationCut The curation cut value + */ + error SubgraphServiceInvalidCurationCut(uint256 curationCut); + + /** + * @notice Thrown when an indexer tries to register with an empty URL + */ + error SubgraphServiceEmptyUrl(); + + /** + * @notice Thrown when an indexer tries to register with an empty geohash + */ + error SubgraphServiceEmptyGeohash(); + + /** + * @notice Thrown when an indexer tries to register but they are already registered + */ + error SubgraphServiceIndexerAlreadyRegistered(); + + /** + * @notice Thrown when an indexer tries to perform an operation but they are not registered + * @param indexer The address of the indexer that is not registered + */ + error SubgraphServiceIndexerNotRegistered(address indexer); + + /** + * @notice Thrown when an indexer tries to collect fees for an unsupported payment type + * @param paymentType The payment type that is not supported + */ + error SubgraphServiceInvalidPaymentType(IGraphPayments.PaymentTypes paymentType); + + /** + * @notice Thrown when the contract GRT balance is inconsistent after collecting from Graph Payments + * @param balanceBefore The contract GRT balance before the collection + * @param balanceAfter The contract GRT balance after the collection + */ + error SubgraphServiceInconsistentCollection(uint256 balanceBefore, uint256 balanceAfter); + + /** + * @notice @notice Thrown when the service provider in the RAV does not match the expected indexer. + * @param providedIndexer The address of the provided indexer. + * @param expectedIndexer The address of the expected indexer. + */ + error SubgraphServiceIndexerMismatch(address providedIndexer, address expectedIndexer); + + /** + * @notice Thrown when the indexer in the allocation state does not match the expected indexer. + * @param indexer The address of the expected indexer. + * @param allocationId The id of the allocation. + */ + error SubgraphServiceAllocationNotAuthorized(address indexer, address allocationId); + + /** + * @notice Thrown when collecting a RAV where the RAV indexer is not the same as the allocation indexer + * @param ravIndexer The address of the RAV indexer + * @param allocationIndexer The address of the allocation indexer + */ + error SubgraphServiceInvalidRAV(address ravIndexer, address allocationIndexer); + + /** + * @notice Thrown when trying to force close an allocation that is not stale and the indexer is not over-allocated + * @param allocationId The id of the allocation + */ + error SubgraphServiceCannotForceCloseAllocation(address allocationId); + + /** + * @notice Thrown when trying to force close an altruistic allocation + * @param allocationId The id of the allocation + */ + error SubgraphServiceAllocationIsAltruistic(address allocationId); + + /** + * @notice Thrown when trying to set stake to fees ratio to zero + */ + error SubgraphServiceInvalidZeroStakeToFeesRatio(); + + /** + * @notice Thrown when collectionId is not a valid address + * @param collectionId The collectionId + */ + error SubgraphServiceInvalidCollectionId(bytes32 collectionId); + + /** + * @notice Initialize the contract + * @dev The thawingPeriod and verifierCut ranges are not set here because they are variables + * on the DisputeManager. We use the {ProvisionManager} overrideable getters to get the ranges. + * @param owner The owner of the contract + * @param minimumProvisionTokens The minimum amount of provisioned tokens required to create an allocation + * @param maximumDelegationRatio The maximum delegation ratio allowed for an allocation + * @param stakeToFeesRatio The ratio of stake to fees to lock when collecting query fees + */ + function initialize( + address owner, + uint256 minimumProvisionTokens, + uint32 maximumDelegationRatio, + uint256 stakeToFeesRatio + ) external; + + /** + * @notice Force close a stale allocation + * @dev This function can be permissionlessly called when the allocation is stale. This + * ensures that rewards for other allocations are not diluted by an inactive allocation. + * + * Requirements: + * - Allocation must exist and be open + * - Allocation must be stale + * - Allocation cannot be altruistic + * + * Emits a {AllocationClosed} event. + * + * @param allocationId The id of the allocation + */ + function closeStaleAllocation(address allocationId) external; + + /** + * @notice Change the amount of tokens in an allocation + * @dev Requirements: + * - The indexer must be registered + * - The provision must be valid according to the subgraph service rules + * - `tokens` must be different from the current allocation size + * - The indexer must have enough available tokens to allocate if they are upsizing the allocation + * + * Emits a {AllocationResized} event. + * + * See {AllocationManager-_resizeAllocation} for more details. + * + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param tokens The new amount of tokens in the allocation + */ + function resizeAllocation(address indexer, address allocationId, uint256 tokens) external; + + /** + * @notice Imports a legacy allocation id into the subgraph service + * This is a governor only action that is required to prevent indexers from re-using allocation ids from the + * legacy staking contract. + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + */ + function migrateLegacyAllocation(address indexer, address allocationId, bytes32 subgraphDeploymentId) external; + + /** + * @notice Sets a pause guardian + * @param pauseGuardian The address of the pause guardian + * @param allowed True if the pause guardian is allowed to pause the contract, false otherwise + */ + function setPauseGuardian(address pauseGuardian, bool allowed) external; + + /** + * @notice Sets the minimum amount of provisioned tokens required to create an allocation + * @param minimumProvisionTokens The minimum amount of provisioned tokens required to create an allocation + */ + function setMinimumProvisionTokens(uint256 minimumProvisionTokens) external; + + /** + * @notice Sets the delegation ratio + * @param delegationRatio The delegation ratio + */ + function setDelegationRatio(uint32 delegationRatio) external; + + /** + * @notice Sets the stake to fees ratio + * @param stakeToFeesRatio The stake to fees ratio + */ + function setStakeToFeesRatio(uint256 stakeToFeesRatio) external; + + /** + * @notice Sets the max POI staleness + * See {AllocationManagerV1Storage-maxPOIStaleness} for more details. + * @param maxPOIStaleness The max POI staleness in seconds + */ + function setMaxPOIStaleness(uint256 maxPOIStaleness) external; + + /** + * @notice Sets the curators payment cut for query fees + * @dev Emits a {CuratorCutSet} event + * @param curationCut The curation cut for the payment type + */ + function setCurationCut(uint256 curationCut) external; + + /** + * @notice Sets the rewards destination for an indexer to receive indexing rewards + * @dev Emits a {RewardsDestinationSet} event + * @param rewardsDestination The address where indexing rewards should be sent + */ + function setRewardsDestination(address rewardsDestination) external; + + /** + * @notice Gets the details of an allocation + * For legacy allocations use {getLegacyAllocation} + * @param allocationId The id of the allocation + * @return The allocation details + */ + function getAllocation(address allocationId) external view returns (Allocation.State memory); + + /** + * @notice Gets the details of a legacy allocation + * For non-legacy allocations use {getAllocation} + * @param allocationId The id of the allocation + * @return The legacy allocation details + */ + function getLegacyAllocation(address allocationId) external view returns (LegacyAllocation.State memory); + + /** + * @notice Encodes the allocation proof for EIP712 signing + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @return The encoded allocation proof + */ + function encodeAllocationProof(address indexer, address allocationId) external view returns (bytes32); + + /** + * @notice Checks if an indexer is over-allocated + * @param allocationId The id of the allocation + * @return True if the indexer is over-allocated, false otherwise + */ + function isOverAllocated(address allocationId) external view returns (bool); + + /** + * @notice Gets the address of the dispute manager + * @return The address of the dispute manager + */ + function getDisputeManager() external view returns (address); + + /** + * @notice Gets the address of the graph tally collector + * @return The address of the graph tally collector + */ + function getGraphTallyCollector() external view returns (address); + + /** + * @notice Gets the address of the curation contract + * @return The address of the curation contract + */ + function getCuration() external view returns (address); +} diff --git a/packages/subgraph-service/contracts/libraries/Allocation.sol b/packages/subgraph-service/contracts/libraries/Allocation.sol new file mode 100644 index 000000000..6f6563068 --- /dev/null +++ b/packages/subgraph-service/contracts/libraries/Allocation.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @title Allocation library + * @notice A library to handle Allocations. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library Allocation { + using Allocation for State; + + /** + * @notice Allocation details + * @param indexer The indexer that owns the allocation + * @param subgraphDeploymentId The subgraph deployment id the allocation is for + * @param tokens The number of tokens allocated + * @param createdAt The timestamp when the allocation was created + * @param closedAt The timestamp when the allocation was closed + * @param lastPOIPresentedAt The timestamp when the last POI was presented + * @param accRewardsPerAllocatedToken The accumulated rewards per allocated token + * @param accRewardsPending The accumulated rewards that are pending to be claimed due allocation resize + * @param createdAtEpoch The epoch when the allocation was created + */ + struct State { + address indexer; + bytes32 subgraphDeploymentId; + uint256 tokens; + uint256 createdAt; + uint256 closedAt; + uint256 lastPOIPresentedAt; + uint256 accRewardsPerAllocatedToken; + uint256 accRewardsPending; + uint256 createdAtEpoch; + } + + /** + * @notice Thrown when attempting to create an allocation with an existing id + * @param allocationId The allocation id + */ + error AllocationAlreadyExists(address allocationId); + + /** + * @notice Thrown when trying to perform an operation on a non-existent allocation + * @param allocationId The allocation id + */ + error AllocationDoesNotExist(address allocationId); + + /** + * @notice Thrown when trying to perform an operation on a closed allocation + * @param allocationId The allocation id + * @param closedAt The timestamp when the allocation was closed + */ + error AllocationClosed(address allocationId, uint256 closedAt); + + /** + * @notice Create a new allocation + * @dev Requirements: + * - The allocation must not exist + * @param self The allocation list mapping + * @param indexer The indexer that owns the allocation + * @param allocationId The allocation id + * @param subgraphDeploymentId The subgraph deployment id the allocation is for + * @param tokens The number of tokens allocated + * @param accRewardsPerAllocatedToken The initial accumulated rewards per allocated token + * @param createdAtEpoch The epoch when the allocation was created + * @return The allocation + */ + function create( + mapping(address => State) storage self, + address indexer, + address allocationId, + bytes32 subgraphDeploymentId, + uint256 tokens, + uint256 accRewardsPerAllocatedToken, + uint256 createdAtEpoch + ) internal returns (State memory) { + require(!self[allocationId].exists(), AllocationAlreadyExists(allocationId)); + + State memory allocation = State({ + indexer: indexer, + subgraphDeploymentId: subgraphDeploymentId, + tokens: tokens, + createdAt: block.timestamp, + closedAt: 0, + lastPOIPresentedAt: 0, + accRewardsPerAllocatedToken: accRewardsPerAllocatedToken, + accRewardsPending: 0, + createdAtEpoch: createdAtEpoch + }); + + self[allocationId] = allocation; + + return allocation; + } + + /** + * @notice Present a POI for an allocation + * @dev It only updates the last POI presented timestamp. + * Requirements: + * - The allocation must be open + * @param self The allocation list mapping + * @param allocationId The allocation id + */ + function presentPOI(mapping(address => State) storage self, address allocationId) internal { + State storage allocation = _get(self, allocationId); + require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); + allocation.lastPOIPresentedAt = block.timestamp; + } + + /** + * @notice Update the accumulated rewards per allocated token for an allocation + * @dev Requirements: + * - The allocation must be open + * @param self The allocation list mapping + * @param allocationId The allocation id + * @param accRewardsPerAllocatedToken The new accumulated rewards per allocated token + */ + function snapshotRewards( + mapping(address => State) storage self, + address allocationId, + uint256 accRewardsPerAllocatedToken + ) internal { + State storage allocation = _get(self, allocationId); + require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); + allocation.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + } + + /** + * @notice Update the accumulated rewards pending to be claimed for an allocation + * @dev Requirements: + * - The allocation must be open + * @param self The allocation list mapping + * @param allocationId The allocation id + */ + function clearPendingRewards(mapping(address => State) storage self, address allocationId) internal { + State storage allocation = _get(self, allocationId); + require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); + allocation.accRewardsPending = 0; + } + + /** + * @notice Close an allocation + * @dev Requirements: + * - The allocation must be open + * @param self The allocation list mapping + * @param allocationId The allocation id + */ + function close(mapping(address => State) storage self, address allocationId) internal { + State storage allocation = _get(self, allocationId); + require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); + allocation.closedAt = block.timestamp; + } + + /** + * @notice Get an allocation + * @param self The allocation list mapping + * @param allocationId The allocation id + * @return The allocation + */ + function get(mapping(address => State) storage self, address allocationId) internal view returns (State memory) { + return _get(self, allocationId); + } + + /** + * @notice Checks if an allocation is stale + * @param self The allocation + * @param staleThreshold The time in blocks to consider an allocation stale + * @return True if the allocation is stale + */ + function isStale(State memory self, uint256 staleThreshold) internal view returns (bool) { + uint256 timeSinceLastPOI = block.timestamp - Math.max(self.createdAt, self.lastPOIPresentedAt); + return self.isOpen() && timeSinceLastPOI > staleThreshold; + } + + /** + * @notice Checks if an allocation exists + * @param self The allocation + * @return True if the allocation exists + */ + function exists(State memory self) internal pure returns (bool) { + return self.createdAt != 0; + } + + /** + * @notice Checks if an allocation is open + * @param self The allocation + * @return True if the allocation is open + */ + function isOpen(State memory self) internal pure returns (bool) { + return self.exists() && self.closedAt == 0; + } + + /** + * @notice Checks if an allocation is alturistic + * @param self The allocation + * @return True if the allocation is alturistic + */ + function isAltruistic(State memory self) internal pure returns (bool) { + return self.exists() && self.tokens == 0; + } + + /** + * @notice Get the allocation for an allocation id + * @dev Reverts if the allocation does not exist + * @param self The allocation list mapping + * @param allocationId The allocation id + * @return The allocation + */ + function _get(mapping(address => State) storage self, address allocationId) private view returns (State storage) { + State storage allocation = self[allocationId]; + require(allocation.exists(), AllocationDoesNotExist(allocationId)); + return allocation; + } +} diff --git a/packages/subgraph-service/contracts/libraries/Attestation.sol b/packages/subgraph-service/contracts/libraries/Attestation.sol new file mode 100644 index 000000000..b7acd0a10 --- /dev/null +++ b/packages/subgraph-service/contracts/libraries/Attestation.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title Attestation library + * @notice A library to handle Attestation. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library Attestation { + /** + * @notice Receipt content sent from the service provider in response to request + * @param requestCID The request CID + * @param responseCID The response CID + * @param subgraphDeploymentId The subgraph deployment id + */ + struct Receipt { + bytes32 requestCID; + bytes32 responseCID; + bytes32 subgraphDeploymentId; + } + + /** + * @notice Attestation sent from the service provider in response to a request + * @param requestCID The request CID + * @param responseCID The response CID + * @param subgraphDeploymentId The subgraph deployment id + * @param r The r value of the signature + * @param s The s value of the signature + * @param v The v value of the signature + */ + struct State { + bytes32 requestCID; + bytes32 responseCID; + bytes32 subgraphDeploymentId; + bytes32 r; + bytes32 s; + uint8 v; + } + + /// @notice Attestation size is the sum of the receipt (96) + signature (65) + uint256 private constant RECEIPT_SIZE_BYTES = 96; + + /// @notice The length of the r value of the signature + uint256 private constant SIG_R_LENGTH = 32; + + /// @notice The length of the s value of the signature + uint256 private constant SIG_S_LENGTH = 32; + + /// @notice The length of the v value of the signature + uint256 private constant SIG_V_LENGTH = 1; + + /// @notice The offset of the r value of the signature + uint256 private constant SIG_R_OFFSET = RECEIPT_SIZE_BYTES; + + /// @notice The offset of the s value of the signature + uint256 private constant SIG_S_OFFSET = RECEIPT_SIZE_BYTES + SIG_R_LENGTH; + + /// @notice The offset of the v value of the signature + uint256 private constant SIG_V_OFFSET = RECEIPT_SIZE_BYTES + SIG_R_LENGTH + SIG_S_LENGTH; + + /// @notice The size of the signature + uint256 private constant SIG_SIZE_BYTES = SIG_R_LENGTH + SIG_S_LENGTH + SIG_V_LENGTH; + + /// @notice The size of the attestation + uint256 private constant ATTESTATION_SIZE_BYTES = RECEIPT_SIZE_BYTES + SIG_SIZE_BYTES; + + /// @notice The length of the uint8 value + uint256 private constant UINT8_BYTE_LENGTH = 1; + + /// @notice The length of the bytes32 value + uint256 private constant BYTES32_BYTE_LENGTH = 32; + + /** + * @notice The error thrown when the attestation data length is invalid + * @param length The length of the attestation data + * @param expectedLength The expected length of the attestation data + */ + error AttestationInvalidBytesLength(uint256 length, uint256 expectedLength); + + /** + * @dev Returns if two attestations are conflicting. + * Everything must match except for the responseId. + * @param _attestation1 Attestation + * @param _attestation2 Attestation + * @return True if the two attestations are conflicting + */ + function areConflicting( + Attestation.State memory _attestation1, + Attestation.State memory _attestation2 + ) internal pure returns (bool) { + return (_attestation1.requestCID == _attestation2.requestCID && + _attestation1.subgraphDeploymentId == _attestation2.subgraphDeploymentId && + _attestation1.responseCID != _attestation2.responseCID); + } + + /** + * @dev Parse the bytes attestation into a struct from `_data`. + * @param _data The bytes to parse + * @return Attestation struct + */ + function parse(bytes memory _data) internal pure returns (State memory) { + // Check attestation data length + require( + _data.length == ATTESTATION_SIZE_BYTES, + AttestationInvalidBytesLength(_data.length, ATTESTATION_SIZE_BYTES) + ); + + // Decode receipt + (bytes32 requestCID, bytes32 responseCID, bytes32 subgraphDeploymentId) = abi.decode( + _data, + (bytes32, bytes32, bytes32) + ); + + // Decode signature + // Signature is expected to be in the order defined in the Attestation struct + bytes32 r = _toBytes32(_data, SIG_R_OFFSET); + bytes32 s = _toBytes32(_data, SIG_S_OFFSET); + uint8 v = _toUint8(_data, SIG_V_OFFSET); + + return State(requestCID, responseCID, subgraphDeploymentId, r, s, v); + } + + /** + * @dev Parse a uint8 from `_bytes` starting at offset `_start`. + * @param _bytes The bytes to parse + * @param _start The start offset + * @return uint8 value + */ + function _toUint8(bytes memory _bytes, uint256 _start) private pure returns (uint8) { + require( + _bytes.length >= _start + UINT8_BYTE_LENGTH, + AttestationInvalidBytesLength(_bytes.length, _start + UINT8_BYTE_LENGTH) + ); + uint8 tempUint; + + // solhint-disable-next-line no-inline-assembly + assembly { + // Load the 32-byte word from memory starting at `_bytes + _start + 1` + // The `0x1` accounts for the fact that we want only the first byte (uint8) + // of the loaded 32 bytes. + tempUint := mload(add(add(_bytes, 0x1), _start)) + } + + return tempUint; + } + + /** + * @dev Parse a bytes32 from `_bytes` starting at offset `_start`. + * @param _bytes The bytes to parse + * @param _start The start offset + * @return bytes32 value + */ + function _toBytes32(bytes memory _bytes, uint256 _start) private pure returns (bytes32) { + require( + _bytes.length >= _start + BYTES32_BYTE_LENGTH, + AttestationInvalidBytesLength(_bytes.length, _start + BYTES32_BYTE_LENGTH) + ); + bytes32 tempBytes32; + + // solhint-disable-next-line no-inline-assembly + assembly { + tempBytes32 := mload(add(add(_bytes, 0x20), _start)) + } + + return tempBytes32; + } +} diff --git a/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol b/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol new file mode 100644 index 000000000..b2d751c5f --- /dev/null +++ b/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; + +/** + * @title LegacyAllocation library + * @notice A library to handle legacy Allocations. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +library LegacyAllocation { + using LegacyAllocation for State; + + /** + * @notice Legacy allocation details + * @dev Note that we are only storing the indexer and subgraphDeploymentId. The main point of tracking legacy allocations + * is to prevent them from being re used on the Subgraph Service. We don't need to store the rest of the allocation details. + * @param indexer The indexer that owns the allocation + * @param subgraphDeploymentId The subgraph deployment id the allocation is for + */ + struct State { + address indexer; + bytes32 subgraphDeploymentId; + } + + /** + * @notice Thrown when attempting to migrate an allocation with an existing id + * @param allocationId The allocation id + */ + error LegacyAllocationAlreadyExists(address allocationId); + + /** + * @notice Thrown when trying to get a non-existent allocation + * @param allocationId The allocation id + */ + error LegacyAllocationDoesNotExist(address allocationId); + + /** + * @notice Migrate a legacy allocation + * @dev Requirements: + * - The allocation must not have been previously migrated + * @param self The legacy allocation list mapping + * @param indexer The indexer that owns the allocation + * @param allocationId The allocation id + * @param subgraphDeploymentId The subgraph deployment id the allocation is for + * @custom:error LegacyAllocationAlreadyMigrated if the allocation has already been migrated + */ + function migrate( + mapping(address => State) storage self, + address indexer, + address allocationId, + bytes32 subgraphDeploymentId + ) internal { + require(!self[allocationId].exists(), LegacyAllocationAlreadyExists(allocationId)); + + State memory allocation = State({ indexer: indexer, subgraphDeploymentId: subgraphDeploymentId }); + self[allocationId] = allocation; + } + + /** + * @notice Get a legacy allocation + * @param self The legacy allocation list mapping + * @param allocationId The allocation id + * @return The legacy allocation details + */ + function get(mapping(address => State) storage self, address allocationId) internal view returns (State memory) { + return _get(self, allocationId); + } + + /** + * @notice Revert if a legacy allocation exists + * @dev We first check the migrated mapping then the old staking contract. + * @dev TRANSITION PERIOD: after the transition period when all the allocations are migrated we can + * remove the call to the staking contract. + * @param self The legacy allocation list mapping + * @param graphStaking The Horizon Staking contract + * @param allocationId The allocation id + */ + function revertIfExists( + mapping(address => State) storage self, + IHorizonStaking graphStaking, + address allocationId + ) internal view { + require(!self[allocationId].exists(), LegacyAllocationAlreadyExists(allocationId)); + require(!graphStaking.isAllocation(allocationId), LegacyAllocationAlreadyExists(allocationId)); + } + + /** + * @notice Check if a legacy allocation exists + * @param self The legacy allocation + * @return True if the allocation exists + */ + function exists(State memory self) internal pure returns (bool) { + return self.indexer != address(0); + } + + /** + * @notice Get a legacy allocation + * @param self The legacy allocation list mapping + * @param allocationId The allocation id + * @return The legacy allocation details + */ + function _get(mapping(address => State) storage self, address allocationId) private view returns (State storage) { + State storage allocation = self[allocationId]; + require(allocation.exists(), LegacyAllocationDoesNotExist(allocationId)); + return allocation; + } +} diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol new file mode 100644 index 000000000..5e79f04b4 --- /dev/null +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; + +import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol"; +import { AllocationManagerV1Storage } from "./AllocationManagerStorage.sol"; + +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import { Allocation } from "../libraries/Allocation.sol"; +import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; + +/** + * @title AllocationManager contract + * @notice A helper contract implementing allocation lifecycle management. + * Allows opening, resizing, and closing allocations, as well as collecting indexing rewards by presenting a Proof + * of Indexing (POI). + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, AllocationManagerV1Storage { + using ProvisionTracker for mapping(address => uint256); + using Allocation for mapping(address => Allocation.State); + using Allocation for Allocation.State; + using LegacyAllocation for mapping(address => LegacyAllocation.State); + using PPMMath for uint256; + using TokenUtils for IGraphToken; + + ///@dev EIP712 typehash for allocation proof + bytes32 private constant EIP712_ALLOCATION_PROOF_TYPEHASH = + keccak256("AllocationIdProof(address indexer,address allocationId)"); + + /** + * @notice Emitted when an indexer creates an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokens The amount of tokens allocated + * @param currentEpoch The current epoch + */ + event AllocationCreated( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokens, + uint256 currentEpoch + ); + + /** + * @notice Emitted when an indexer collects indexing rewards for an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokensRewards The amount of tokens collected + * @param tokensIndexerRewards The amount of tokens collected for the indexer + * @param tokensDelegationRewards The amount of tokens collected for delegators + * @param poi The POI presented + * @param currentEpoch The current epoch + */ + event IndexingRewardsCollected( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokensRewards, + uint256 tokensIndexerRewards, + uint256 tokensDelegationRewards, + bytes32 poi, + uint256 currentEpoch + ); + + /** + * @notice Emitted when an indexer resizes an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param newTokens The new amount of tokens allocated + * @param oldTokens The old amount of tokens allocated + */ + event AllocationResized( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 newTokens, + uint256 oldTokens + ); + + /** + * @dev Emitted when an indexer closes an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokens The amount of tokens allocated + */ + event AllocationClosed( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokens + ); + + /** + * @notice Emitted when a legacy allocation is migrated into the subgraph service + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + */ + event LegacyAllocationMigrated( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId + ); + + /** + * @notice Emitted when an indexer sets a new indexing rewards destination + * @param indexer The address of the indexer + * @param rewardsDestination The address where indexing rewards should be sent + */ + event RewardsDestinationSet(address indexed indexer, address indexed rewardsDestination); + + /** + * @notice Emitted when the maximum POI staleness is updated + * @param maxPOIStaleness The max POI staleness in seconds + */ + event MaxPOIStalenessSet(uint256 maxPOIStaleness); + + /** + * @notice Thrown when an allocation proof is invalid + * Both `signer` and `allocationId` should match for a valid proof. + * @param signer The address that signed the proof + * @param allocationId The id of the allocation + */ + error AllocationManagerInvalidAllocationProof(address signer, address allocationId); + + /** + * @notice Thrown when attempting to create an allocation with a zero allocation id + */ + error AllocationManagerInvalidZeroAllocationId(); + + /** + * @notice Thrown when attempting to collect indexing rewards on a closed allocationl + * @param allocationId The id of the allocation + */ + error AllocationManagerAllocationClosed(address allocationId); + + /** + * @notice Thrown when attempting to resize an allocation with the same size + * @param allocationId The id of the allocation + * @param tokens The amount of tokens + */ + error AllocationManagerAllocationSameSize(address allocationId, uint256 tokens); + + /** + * @notice Initializes the contract and parent contracts + * @param _name The name to use for EIP712 domain separation + * @param _version The version to use for EIP712 domain separation + */ + function __AllocationManager_init(string memory _name, string memory _version) internal onlyInitializing { + __EIP712_init(_name, _version); + __AllocationManager_init_unchained(); + } + + /** + * @notice Initializes the contract + */ + function __AllocationManager_init_unchained() internal onlyInitializing {} + + /** + * @notice Imports a legacy allocation id into the subgraph service + * This is a governor only action that is required to prevent indexers from re-using allocation ids from the + * legacy staking contract. It will revert with LegacyAllocationAlreadyMigrated if the allocation has already been migrated. + * @param _indexer The address of the indexer + * @param _allocationId The id of the allocation + * @param _subgraphDeploymentId The id of the subgraph deployment + */ + function _migrateLegacyAllocation(address _indexer, address _allocationId, bytes32 _subgraphDeploymentId) internal { + _legacyAllocations.migrate(_indexer, _allocationId, _subgraphDeploymentId); + emit LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentId); + } + + /** + * @notice Create an allocation + * @dev The `_allocationProof` is a 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationId)` + * + * Requirements: + * - `_allocationId` must not be the zero address + * + * Emits a {AllocationCreated} event + * + * @param _indexer The address of the indexer + * @param _allocationId The id of the allocation to be created + * @param _subgraphDeploymentId The subgraph deployment Id + * @param _tokens The amount of tokens to allocate + * @param _allocationProof Signed proof of allocation id address ownership + * @param _delegationRatio The delegation ratio to consider when locking tokens + * @return The allocation details + */ + function _allocate( + address _indexer, + address _allocationId, + bytes32 _subgraphDeploymentId, + uint256 _tokens, + bytes memory _allocationProof, + uint32 _delegationRatio + ) internal returns (Allocation.State memory) { + require(_allocationId != address(0), AllocationManagerInvalidZeroAllocationId()); + + _verifyAllocationProof(_indexer, _allocationId, _allocationProof); + + // Ensure allocation id is not reused + // need to check both subgraph service (on allocations.create()) and legacy allocations + _legacyAllocations.revertIfExists(_graphStaking(), _allocationId); + + uint256 currentEpoch = _graphEpochManager().currentEpoch(); + Allocation.State memory allocation = _allocations.create( + _indexer, + _allocationId, + _subgraphDeploymentId, + _tokens, + _graphRewardsManager().onSubgraphAllocationUpdate(_subgraphDeploymentId), + currentEpoch + ); + + // Check that the indexer has enough tokens available + // Note that the delegation ratio ensures overdelegation cannot be used + allocationProvisionTracker.lock(_graphStaking(), _indexer, _tokens, _delegationRatio); + + // Update total allocated tokens for the subgraph deployment + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] + + allocation.tokens; + + emit AllocationCreated(_indexer, _allocationId, _subgraphDeploymentId, allocation.tokens, currentEpoch); + return allocation; + } + + /** + * @notice Present a POI to collect indexing rewards for an allocation + * This function will mint indexing rewards using the {RewardsManager} and distribute them to the indexer and delegators. + * + * Conditions to qualify for indexing rewards: + * - POI must be non-zero + * - POI must not be stale, i.e: older than `maxPOIStaleness` + * - allocation must not be altruistic (allocated tokens = 0) + * - allocation must be open for at least one epoch + * + * Note that indexers are required to periodically (at most every `maxPOIStaleness`) present POIs to collect rewards. + * Rewards will not be issued to stale POIs, which means that indexers are advised to present a zero POI if they are + * unable to present a valid one to prevent being locked out of future rewards. + * + * Note on allocation duration restriction: this is required to ensure that non protocol chains have a valid block number for + * which to calculate POIs. EBO posts once per epoch typically at each epoch change, so we restrict rewards to allocations + * that have gone through at least one epoch change. + * + * Emits a {IndexingRewardsCollected} event. + * + * @param _allocationId The id of the allocation to collect rewards for + * @param _poi The POI being presented + * @param _delegationRatio The delegation ratio to consider when locking tokens + * @return The amount of tokens collected + */ + function _collectIndexingRewards( + address _allocationId, + bytes32 _poi, + uint32 _delegationRatio + ) internal returns (uint256) { + Allocation.State memory allocation = _allocations.get(_allocationId); + require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); + + uint256 currentEpoch = _graphEpochManager().currentEpoch(); + + // Mint indexing rewards if all conditions are met + uint256 tokensRewards = (!allocation.isStale(maxPOIStaleness) && + !allocation.isAltruistic() && + _poi != bytes32(0)) && currentEpoch > allocation.createdAtEpoch + ? _graphRewardsManager().takeRewards(_allocationId) + : 0; + + // ... but we still take a snapshot to ensure the rewards are not accumulated for the next valid POI + _allocations.snapshotRewards( + _allocationId, + _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) + ); + _allocations.presentPOI(_allocationId); + + // Any pending rewards should have been collected now + _allocations.clearPendingRewards(_allocationId); + + uint256 tokensIndexerRewards = 0; + uint256 tokensDelegationRewards = 0; + if (tokensRewards != 0) { + // Distribute rewards to delegators + uint256 delegatorCut = _graphStaking().getDelegationFeeCut( + allocation.indexer, + address(this), + IGraphPayments.PaymentTypes.IndexingRewards + ); + IHorizonStakingTypes.DelegationPool memory delegationPool = _graphStaking().getDelegationPool( + allocation.indexer, + address(this) + ); + // If delegation pool has no shares then we don't need to distribute rewards to delegators + tokensDelegationRewards = delegationPool.shares > 0 ? tokensRewards.mulPPM(delegatorCut) : 0; + if (tokensDelegationRewards > 0) { + _graphToken().approve(address(_graphStaking()), tokensDelegationRewards); + _graphStaking().addToDelegationPool(allocation.indexer, address(this), tokensDelegationRewards); + } + + // Distribute rewards to indexer + tokensIndexerRewards = tokensRewards - tokensDelegationRewards; + if (tokensIndexerRewards > 0) { + address rewardsDestination = rewardsDestination[allocation.indexer]; + if (rewardsDestination == address(0)) { + _graphToken().approve(address(_graphStaking()), tokensIndexerRewards); + _graphStaking().stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards); + } else { + _graphToken().pushTokens(rewardsDestination, tokensIndexerRewards); + } + } + } + + emit IndexingRewardsCollected( + allocation.indexer, + _allocationId, + allocation.subgraphDeploymentId, + tokensRewards, + tokensIndexerRewards, + tokensDelegationRewards, + _poi, + currentEpoch + ); + + // Check if the indexer is over-allocated and close the allocation if necessary + if (_isOverAllocated(allocation.indexer, _delegationRatio)) { + _closeAllocation(_allocationId); + } + + return tokensRewards; + } + + /** + * @notice Resize an allocation + * @dev Will lock or release tokens in the provision tracker depending on the new allocation size. + * Rewards accrued but not issued before the resize will be accounted for as pending rewards. + * These will be paid out when the indexer presents a POI. + * + * Requirements: + * - `_indexer` must be the owner of the allocation + * - Allocation must be open + * - `_tokens` must be different from the current allocation size + * + * Emits a {AllocationResized} event. + * + * @param _allocationId The id of the allocation to be resized + * @param _tokens The new amount of tokens to allocate + * @param _delegationRatio The delegation ratio to consider when locking tokens + * @return The allocation details + */ + function _resizeAllocation( + address _allocationId, + uint256 _tokens, + uint32 _delegationRatio + ) internal returns (Allocation.State memory) { + Allocation.State memory allocation = _allocations.get(_allocationId); + require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); + require(_tokens != allocation.tokens, AllocationManagerAllocationSameSize(_allocationId, _tokens)); + + // Update provision tracker + uint256 oldTokens = allocation.tokens; + if (_tokens > oldTokens) { + allocationProvisionTracker.lock(_graphStaking(), allocation.indexer, _tokens - oldTokens, _delegationRatio); + } else { + allocationProvisionTracker.release(allocation.indexer, oldTokens - _tokens); + } + + // Calculate rewards that have been accrued since the last snapshot but not yet issued + uint256 accRewardsPerAllocatedToken = _graphRewardsManager().onSubgraphAllocationUpdate( + allocation.subgraphDeploymentId + ); + uint256 accRewardsPerAllocatedTokenPending = !allocation.isAltruistic() + ? accRewardsPerAllocatedToken - allocation.accRewardsPerAllocatedToken + : 0; + + // Update the allocation + _allocations[_allocationId].tokens = _tokens; + _allocations[_allocationId].accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + _allocations[_allocationId].accRewardsPending += _graphRewardsManager().calcRewards( + oldTokens, + accRewardsPerAllocatedTokenPending + ); + + // Update total allocated tokens for the subgraph deployment + if (_tokens > oldTokens) { + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] += (_tokens - oldTokens); + } else { + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] -= (oldTokens - _tokens); + } + + emit AllocationResized(allocation.indexer, _allocationId, allocation.subgraphDeploymentId, _tokens, oldTokens); + return _allocations[_allocationId]; + } + + /** + * @notice Close an allocation + * Does not require presenting a POI, use {_collectIndexingRewards} to present a POI and collect rewards + * @dev Note that allocations are nowlong lived. All service payments, including indexing rewards, should be collected periodically + * without the need of closing the allocation. Allocations should only be closed when indexers want to reclaim the allocated + * tokens for other purposes. + * + * Emits a {AllocationClosed} event + * + * @param _allocationId The id of the allocation to be closed + */ + function _closeAllocation(address _allocationId) internal { + Allocation.State memory allocation = _allocations.get(_allocationId); + + // Take rewards snapshot to prevent other allos from counting tokens from this allo + _allocations.snapshotRewards( + _allocationId, + _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) + ); + + _allocations.close(_allocationId); + allocationProvisionTracker.release(allocation.indexer, allocation.tokens); + + // Update total allocated tokens for the subgraph deployment + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] - + allocation.tokens; + + emit AllocationClosed(allocation.indexer, _allocationId, allocation.subgraphDeploymentId, allocation.tokens); + } + + /** + * @notice Sets the rewards destination for an indexer to receive indexing rewards + * @dev Emits a {RewardsDestinationSet} event + * @param _indexer The address of the indexer + * @param _rewardsDestination The address where indexing rewards should be sent + */ + function _setRewardsDestination(address _indexer, address _rewardsDestination) internal { + rewardsDestination[_indexer] = _rewardsDestination; + emit RewardsDestinationSet(_indexer, _rewardsDestination); + } + + /** + * @notice Sets the maximum amount of time, in seconds, allowed between presenting POIs to qualify for indexing rewards + * @dev Emits a {MaxPOIStalenessSet} event + * @param _maxPOIStaleness The max POI staleness in seconds + */ + function _setMaxPOIStaleness(uint256 _maxPOIStaleness) internal { + maxPOIStaleness = _maxPOIStaleness; + emit MaxPOIStalenessSet(_maxPOIStaleness); + } + + /** + * @notice Gets the details of an allocation + * @param _allocationId The id of the allocation + * @return The allocation details + */ + function _getAllocation(address _allocationId) internal view returns (Allocation.State memory) { + return _allocations.get(_allocationId); + } + + /** + * @notice Gets the details of a legacy allocation + * @param _allocationId The id of the legacy allocation + * @return The legacy allocation details + */ + function _getLegacyAllocation(address _allocationId) internal view returns (LegacyAllocation.State memory) { + return _legacyAllocations.get(_allocationId); + } + + /** + * @notice Encodes the allocation proof for EIP712 signing + * @param _indexer The address of the indexer + * @param _allocationId The id of the allocation + * @return The encoded allocation proof + */ + function _encodeAllocationProof(address _indexer, address _allocationId) internal view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(EIP712_ALLOCATION_PROOF_TYPEHASH, _indexer, _allocationId))); + } + + /** + * @notice Checks if an allocation is over-allocated + * @param _indexer The address of the indexer + * @param _delegationRatio The delegation ratio to consider when locking tokens + * @return True if the allocation is over-allocated, false otherwise + */ + function _isOverAllocated(address _indexer, uint32 _delegationRatio) internal view returns (bool) { + return !allocationProvisionTracker.check(_graphStaking(), _indexer, _delegationRatio); + } + + /** + * @notice Verifies ownership of an allocation id by verifying an EIP712 allocation proof + * @dev Requirements: + * - Signer must be the allocation id address + * @param _indexer The address of the indexer + * @param _allocationId The id of the allocation + * @param _proof The EIP712 proof, an EIP712 signed message of (indexer,allocationId) + */ + function _verifyAllocationProof(address _indexer, address _allocationId, bytes memory _proof) private view { + address signer = ECDSA.recover(_encodeAllocationProof(_indexer, _allocationId), _proof); + require(signer == _allocationId, AllocationManagerInvalidAllocationProof(signer, _allocationId)); + } +} diff --git a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol new file mode 100644 index 000000000..e13ee1994 --- /dev/null +++ b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { Allocation } from "../libraries/Allocation.sol"; +import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; + +/** + * @title AllocationManagerStorage + * @notice This contract holds all the storage variables for the Allocation Manager contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract AllocationManagerV1Storage { + /// @notice Allocation details + mapping(address allocationId => Allocation.State allocation) internal _allocations; + + /// @notice Legacy allocation details + mapping(address allocationId => LegacyAllocation.State allocation) internal _legacyAllocations; + + /// @notice Tracks allocated tokens per indexer + mapping(address indexer => uint256 tokens) public allocationProvisionTracker; + + /// @notice Maximum amount of time, in seconds, allowed between presenting POIs to qualify for indexing rewards + uint256 public maxPOIStaleness; + + /// @notice Destination of accrued indexing rewards + mapping(address indexer => address destination) public rewardsDestination; + + /// @notice Track total tokens allocated per subgraph deployment + /// @dev Used to calculate indexing rewards + mapping(bytes32 subgraphDeploymentId => uint256 tokens) internal _subgraphAllocatedTokens; + + /// @dev Gap to allow adding variables in future upgrades + uint256[50] private __gap; +} diff --git a/packages/subgraph-service/contracts/utilities/AttestationManager.sol b/packages/subgraph-service/contracts/utilities/AttestationManager.sol new file mode 100644 index 000000000..a0771a841 --- /dev/null +++ b/packages/subgraph-service/contracts/utilities/AttestationManager.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { AttestationManagerV1Storage } from "./AttestationManagerStorage.sol"; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { Attestation } from "../libraries/Attestation.sol"; + +/** + * @title AttestationManager contract + * @notice A helper contract implementing attestation verification. + * Uses a custom implementation of EIP712 for backwards compatibility with attestations. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract AttestationManager is Initializable, AttestationManagerV1Storage { + /// @notice EIP712 type hash for Receipt struct + bytes32 private constant RECEIPT_TYPE_HASH = + keccak256("Receipt(bytes32 requestCID,bytes32 responseCID,bytes32 subgraphDeploymentID)"); + + /// @notice EIP712 domain type hash + bytes32 private constant DOMAIN_TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)"); + + /// @notice EIP712 domain name + bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Protocol"); + + /// @notice EIP712 domain version + bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); + + /// @notice EIP712 domain salt + bytes32 private constant DOMAIN_SALT = 0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2; + + /** + * @dev Initialize the AttestationManager contract and parent contracts + */ + // solhint-disable-next-line func-name-mixedcase + function __AttestationManager_init() internal onlyInitializing { + __AttestationManager_init_unchained(); + } + + /** + * @dev Initialize the AttestationManager contract + */ + // solhint-disable-next-line func-name-mixedcase + function __AttestationManager_init_unchained() internal onlyInitializing { + _domainSeparator = keccak256( + abi.encode( + DOMAIN_TYPE_HASH, + DOMAIN_NAME_HASH, + DOMAIN_VERSION_HASH, + block.chainid, + address(this), + DOMAIN_SALT + ) + ); + } + + /** + * @dev Recover the signer address of the `_attestation`. + * @param _attestation The attestation struct + * @return Signer address + */ + function _recoverSigner(Attestation.State memory _attestation) internal view returns (address) { + // Obtain the hash of the fully-encoded message, per EIP-712 encoding + Attestation.Receipt memory receipt = Attestation.Receipt( + _attestation.requestCID, + _attestation.responseCID, + _attestation.subgraphDeploymentId + ); + bytes32 messageHash = _encodeReceipt(receipt); + + // Obtain the signer of the fully-encoded EIP-712 message hash + // NOTE: The signer of the attestation is the indexer that served the request + return ECDSA.recover(messageHash, abi.encodePacked(_attestation.r, _attestation.s, _attestation.v)); + } + + /** + * @dev Get the message hash that a indexer used to sign the receipt. + * Encodes a receipt using a domain separator, as described on + * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification. + * @notice Return the message hash used to sign the receipt + * @param _receipt Receipt returned by indexer and submitted by fisherman + * @return Message hash used to sign the receipt + */ + function _encodeReceipt(Attestation.Receipt memory _receipt) internal view returns (bytes32) { + return + keccak256( + abi.encodePacked( + "\x19\x01", // EIP-191 encoding pad, EIP-712 version 1 + _domainSeparator, + keccak256( + abi.encode( + RECEIPT_TYPE_HASH, + _receipt.requestCID, + _receipt.responseCID, + _receipt.subgraphDeploymentId + ) // EIP 712-encoded message hash + ) + ) + ); + } +} diff --git a/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol new file mode 100644 index 000000000..1c720ec8c --- /dev/null +++ b/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title AttestationManagerStorage + * @notice This contract holds all the storage variables for the Attestation Manager contract. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract AttestationManagerV1Storage { + /// @dev EIP712 domain separator + bytes32 internal _domainSeparator; + + /// @dev Gap to allow adding variables in future upgrades + uint256[50] private __gap; +} diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol new file mode 100644 index 000000000..d068c74b3 --- /dev/null +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IDisputeManager } from "../interfaces/IDisputeManager.sol"; +import { ISubgraphService } from "../interfaces/ISubgraphService.sol"; +import { IGraphTallyCollector } from "@graphprotocol/horizon/contracts/interfaces/IGraphTallyCollector.sol"; +import { ICuration } from "@graphprotocol/contracts/contracts/curation/ICuration.sol"; + +/** + * @title Directory contract + * @notice This contract is meant to be inherited by {SubgraphService} contract. + * It contains the addresses of the contracts that the contract interacts with. + * Uses immutable variables to minimize gas costs. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract Directory { + /// @notice The Subgraph Service contract address + ISubgraphService private immutable SUBGRAPH_SERVICE; + + /// @notice The Dispute Manager contract address + IDisputeManager private immutable DISPUTE_MANAGER; + + /// @notice The Graph Tally Collector contract address + /// @dev Required to collect payments via Graph Horizon payments protocol + IGraphTallyCollector private immutable GRAPH_TALLY_COLLECTOR; + + /// @notice The Curation contract address + /// @dev Required for curation fees distribution + ICuration private immutable CURATION; + + /** + * @notice Emitted when the Directory is initialized + * @param subgraphService The Subgraph Service contract address + * @param disputeManager The Dispute Manager contract address + * @param graphTallyCollector The Graph Tally Collector contract address + * @param curation The Curation contract address + */ + event SubgraphServiceDirectoryInitialized( + address subgraphService, + address disputeManager, + address graphTallyCollector, + address curation + ); + + /** + * @notice Thrown when the caller is not the Dispute Manager + * @param caller The caller address + * @param disputeManager The Dispute Manager address + */ + error DirectoryNotDisputeManager(address caller, address disputeManager); + + /** + * @notice Checks that the caller is the Dispute Manager + */ + modifier onlyDisputeManager() { + require( + msg.sender == address(DISPUTE_MANAGER), + DirectoryNotDisputeManager(msg.sender, address(DISPUTE_MANAGER)) + ); + _; + } + + /** + * @notice Constructor for the Directory contract + * @param subgraphService The Subgraph Service contract address + * @param disputeManager The Dispute Manager contract address + * @param graphTallyCollector The Graph Tally Collector contract address + * @param curation The Curation contract address + */ + constructor(address subgraphService, address disputeManager, address graphTallyCollector, address curation) { + SUBGRAPH_SERVICE = ISubgraphService(subgraphService); + DISPUTE_MANAGER = IDisputeManager(disputeManager); + GRAPH_TALLY_COLLECTOR = IGraphTallyCollector(graphTallyCollector); + CURATION = ICuration(curation); + + emit SubgraphServiceDirectoryInitialized(subgraphService, disputeManager, graphTallyCollector, curation); + } + + /** + * @notice Returns the Subgraph Service contract address + * @return The Subgraph Service contract + */ + function _subgraphService() internal view returns (ISubgraphService) { + return SUBGRAPH_SERVICE; + } + + /** + * @notice Returns the Dispute Manager contract address + * @return The Dispute Manager contract + */ + function _disputeManager() internal view returns (IDisputeManager) { + return DISPUTE_MANAGER; + } + + /** + * @notice Returns the Graph Tally Collector contract address + * @return The Graph Tally Collector contract + */ + function _graphTallyCollector() internal view returns (IGraphTallyCollector) { + return GRAPH_TALLY_COLLECTOR; + } + + /** + * @notice Returns the Curation contract address + * @return The Curation contract + */ + function _curation() internal view returns (ICuration) { + return CURATION; + } +} diff --git a/packages/subgraph-service/eslint.config.js b/packages/subgraph-service/eslint.config.js new file mode 100644 index 000000000..c9e06b116 --- /dev/null +++ b/packages/subgraph-service/eslint.config.js @@ -0,0 +1,21 @@ +// @ts-check +/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +const eslintGraphConfig = require('eslint-graph-config') +module.exports = [ + ...eslintGraphConfig.default, + { + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + }, + }, + { + ignores: ['typechain-types/*', 'lib/*'], + }, +] diff --git a/packages/subgraph-service/foundry.toml b/packages/subgraph-service/foundry.toml new file mode 100644 index 000000000..d70cbf59d --- /dev/null +++ b/packages/subgraph-service/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +src = 'contracts' +out = 'build' +libs = ['node_modules', 'lib'] +test = 'test' +cache_path = 'cache_forge' +fs_permissions = [{ access = "read", path = "./"}] +optimizer = true +optimizer-runs = 100 diff --git a/packages/subgraph-service/hardhat.config.ts b/packages/subgraph-service/hardhat.config.ts new file mode 100644 index 000000000..9660e89b1 --- /dev/null +++ b/packages/subgraph-service/hardhat.config.ts @@ -0,0 +1,32 @@ +import { hardhatBaseConfig } from 'hardhat-graph-protocol/sdk' +import { HardhatUserConfig } from 'hardhat/config' + +// Hardhat plugins +import '@nomicfoundation/hardhat-foundry' +import '@nomicfoundation/hardhat-toolbox' +import '@nomicfoundation/hardhat-ignition-ethers' +import 'hardhat-storage-layout' +import 'hardhat-contract-sizer' +import 'hardhat-secure-accounts' +import 'solidity-docgen' + +// Skip importing hardhat-graph-protocol when building the project, it has circular dependency +if (process.env.BUILD_RUN !== 'true') { + require('hardhat-graph-protocol') + require('./tasks/deploy') +} + +const config: HardhatUserConfig = { + ...hardhatBaseConfig, + solidity: { + version: '0.8.27', + settings: { + optimizer: { + enabled: true, + runs: 50, + }, + }, + }, +} + +export default config diff --git a/packages/subgraph-service/ignition/configs/migrate.default.json5 b/packages/subgraph-service/ignition/configs/migrate.default.json5 new file mode 100644 index 000000000..6678781a7 --- /dev/null +++ b/packages/subgraph-service/ignition/configs/migrate.default.json5 @@ -0,0 +1,37 @@ +{ + "$global": { + "governor": "0x72ee30d43Fb5A90B3FE983156C5d2fBE6F6d07B3", + "arbitrator": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", + "pauseGuardian": "0xB0aD33a21b98bCA1761729A105e2E34e27153aAE", + + // Addresses for contracts deployed in the original Graph Protocol + "controllerAddress": "0x9DB3ee191681f092607035d9BDA6e59FbEaCa695", + + // Must be set for step 2 of the deployment + "disputeManagerProxyAddress": "" + }, + "DisputeManager": { + "disputePeriod": 2419200, + "disputeDeposit": "10000000000000000000000n", + "fishermanRewardCut": 500000, + "maxSlashingCut": 1000000, + + // Must be set for step 2 of the deployment + "disputeManagerProxyAdminAddress": "" + }, + "SubgraphService": { + "minimumProvisionTokens": "100000000000000000000000n", + "maximumDelegationRatio": 16, + "stakeToFeesRatio": 2, + "maxPOIStaleness": 2419200, // 28 days = 2419200 seconds + "curationCut": 100000, + + // Addresses for contracts deployed in the original Graph Protocol + "curationAddress": "0xDe761f075200E75485F4358978FB4d1dC8644FD5", + + // Must be set for step 2 of the deployment + "subgraphServiceProxyAddress": "", + "subgraphServiceProxyAdminAddress": "", + "graphTallyCollectorAddress": "" + } +} diff --git a/packages/subgraph-service/ignition/configs/protocol.default.json5 b/packages/subgraph-service/ignition/configs/protocol.default.json5 new file mode 100644 index 000000000..6bb5191bf --- /dev/null +++ b/packages/subgraph-service/ignition/configs/protocol.default.json5 @@ -0,0 +1,34 @@ +{ + "$global": { + "governor": "0x72ee30d43Fb5A90B3FE983156C5d2fBE6F6d07B3", + "arbitrator": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", + "pauseGuardian": "0xB0aD33a21b98bCA1761729A105e2E34e27153aAE", + + // Must be set for step 2 of the deployment + "controllerAddress": "", + "disputeManagerProxyAddress": "", + "curationAddress": "", + "curationImplementationAddress": "", + "subgraphServiceProxyAddress": "" + }, + "DisputeManager": { + "disputePeriod": 2419200, + "disputeDeposit": "10000000000000000000000n", + "fishermanRewardCut": 500000, + "maxSlashingCut": 1000000, + + // Must be set for step 2 of the deployment + "disputeManagerProxyAdminAddress": "", + }, + "SubgraphService": { + "minimumProvisionTokens": "100000000000000000000000n", + "maximumDelegationRatio": 16, + "stakeToFeesRatio": 2, + "maxPOIStaleness": 2419200, // 28 days = 2419200 seconds + "curationCut": 100000, + + // Must be set for step 2 of the deployment + "subgraphServiceProxyAdminAddress": "", + "graphTallyCollectorAddress": "" + } +} diff --git a/packages/subgraph-service/ignition/configs/protocol.local-network.json5 b/packages/subgraph-service/ignition/configs/protocol.local-network.json5 new file mode 100644 index 000000000..d43a2eb68 --- /dev/null +++ b/packages/subgraph-service/ignition/configs/protocol.local-network.json5 @@ -0,0 +1,35 @@ + +{ + "$global": { + "governor": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "arbitrator": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", + "pauseGuardian": "0xB0aD33a21b98bCA1761729A105e2E34e27153aAE", + + // Must be set for step 2 of the deployment + "controllerAddress": "", + "disputeManagerProxyAddress": "", + "curationAddress": "", + "curationImplementationAddress": "" + }, + "DisputeManager": { + "disputePeriod": 2419200, + "disputeDeposit": "10000000000000000000000n", + "fishermanRewardCut": 500000, + "maxSlashingCut": 1000000, + + // Must be set for step 2 of the deployment + "disputeManagerProxyAdminAddress": "", + }, + "SubgraphService": { + "minimumProvisionTokens": "100000000000000000000000n", + "maximumDelegationRatio": 16, + "stakeToFeesRatio": 2, + "maxPOIStaleness": 2419200, // 28 days = 2419200 seconds + "curationCut": 100000, + + // Must be set for step 2 of the deployment + "subgraphServiceProxyAddress": "", + "subgraphServiceProxyAdminAddress": "", + "graphTallyCollectorAddress": "" + } +} diff --git a/packages/subgraph-service/ignition/modules/Curation.ts b/packages/subgraph-service/ignition/modules/Curation.ts new file mode 100644 index 000000000..9851a86ed --- /dev/null +++ b/packages/subgraph-service/ignition/modules/Curation.ts @@ -0,0 +1,17 @@ +import { buildModule } from '@nomicfoundation/ignition-core' + +import CurationArtifact from '@graphprotocol/contracts/build/contracts/contracts/l2/curation/L2Curation.sol/L2Curation.json' + +// Note that this module is a no-op, we only run it to get curation addresses into the address book. +// Curation deployment should be managed by ignition scripts in subgraph-service package however +// due to tight coupling with HorizonStakingExtension contract it's easier to do it on the horizon package. +// Once the transition period is over we can migrate it. +export default buildModule('L2Curation', (m) => { + const curationAddress = m.getParameter('curationAddress') + const curationImplementationAddress = m.getParameter('curationImplementationAddress') + + const L2Curation = m.contractAt('L2CurationAddressBook', CurationArtifact, curationAddress) + const L2CurationImplementation = m.contractAt('L2CurationImplementationAddressBook', CurationArtifact, curationImplementationAddress) + + return { L2Curation, L2CurationImplementation } +}) diff --git a/packages/subgraph-service/ignition/modules/DisputeManager.ts b/packages/subgraph-service/ignition/modules/DisputeManager.ts new file mode 100644 index 000000000..00038e480 --- /dev/null +++ b/packages/subgraph-service/ignition/modules/DisputeManager.ts @@ -0,0 +1,57 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployImplementation } from '@graphprotocol/horizon/ignition/modules/proxy/implementation' +import { upgradeTransparentUpgradeableProxy } from '@graphprotocol/horizon/ignition/modules/proxy/TransparentUpgradeableProxy' + +import DisputeManagerArtifact from '../../build/contracts/contracts/DisputeManager.sol/DisputeManager.json' +import ProxyAdminArtifact from '@openzeppelin/contracts/build/contracts/ProxyAdmin.json' +import TransparentUpgradeableProxyArtifact from '@openzeppelin/contracts/build/contracts/TransparentUpgradeableProxy.json' + +export default buildModule('DisputeManager', (m) => { + const deployer = m.getAccount(0) + const governor = m.getParameter('governor') + const controllerAddress = m.getParameter('controllerAddress') + const subgraphServiceProxyAddress = m.getParameter('subgraphServiceProxyAddress') + const disputeManagerProxyAddress = m.getParameter('disputeManagerProxyAddress') + const disputeManagerProxyAdminAddress = m.getParameter('disputeManagerProxyAdminAddress') + const arbitrator = m.getParameter('arbitrator') + const disputePeriod = m.getParameter('disputePeriod') + const disputeDeposit = m.getParameter('disputeDeposit') + const fishermanRewardCut = m.getParameter('fishermanRewardCut') + const maxSlashingCut = m.getParameter('maxSlashingCut') + + const DisputeManagerProxyAdmin = m.contractAt('ProxyAdmin', ProxyAdminArtifact, disputeManagerProxyAdminAddress) + const DisputeManagerProxy = m.contractAt('DisputeManagerProxy', TransparentUpgradeableProxyArtifact, disputeManagerProxyAddress) + + // Deploy implementation + const DisputeManagerImplementation = deployImplementation(m, { + name: 'DisputeManager', + constructorArgs: [controllerAddress], + }) + + // Upgrade implementation + const DisputeManager = upgradeTransparentUpgradeableProxy(m, + DisputeManagerProxyAdmin, + DisputeManagerProxy, + DisputeManagerImplementation, { + name: 'DisputeManager', + artifact: DisputeManagerArtifact, + initArgs: [ + deployer, + arbitrator, + disputePeriod, + disputeDeposit, + fishermanRewardCut, + maxSlashingCut, + ], + }) + + const callSetSubgraphService = m.call(DisputeManager, 'setSubgraphService', [subgraphServiceProxyAddress]) + + m.call(DisputeManager, 'transferOwnership', [governor], { after: [callSetSubgraphService] }) + m.call(DisputeManagerProxyAdmin, 'transferOwnership', [governor], { after: [callSetSubgraphService] }) + + return { + DisputeManager, + DisputeManagerImplementation, + } +}) diff --git a/packages/subgraph-service/ignition/modules/Proxies.ts b/packages/subgraph-service/ignition/modules/Proxies.ts new file mode 100644 index 000000000..7fe2c8960 --- /dev/null +++ b/packages/subgraph-service/ignition/modules/Proxies.ts @@ -0,0 +1,30 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployTransparentUpgradeableProxy } from '@graphprotocol/horizon/ignition/modules/proxy/TransparentUpgradeableProxy' + +import DisputeManagerArtifact from '../../build/contracts/contracts/DisputeManager.sol/DisputeManager.json' +import SubgraphServiceArtifact from '../../build/contracts/contracts/SubgraphService.sol/SubgraphService.json' + +export default buildModule('SubgraphServiceProxies', (m) => { + // Deploy proxies contracts using OZ TransparentUpgradeableProxy + const { + Proxy: DisputeManagerProxy, + ProxyAdmin: DisputeManagerProxyAdmin, + } = deployTransparentUpgradeableProxy(m, { + name: 'DisputeManager', + artifact: DisputeManagerArtifact, + }) + const { + Proxy: SubgraphServiceProxy, + ProxyAdmin: SubgraphServiceProxyAdmin, + } = deployTransparentUpgradeableProxy(m, { + name: 'SubgraphService', + artifact: SubgraphServiceArtifact, + }) + + return { + SubgraphServiceProxy, + SubgraphServiceProxyAdmin, + DisputeManagerProxy, + DisputeManagerProxyAdmin, + } +}) diff --git a/packages/subgraph-service/ignition/modules/SubgraphService.ts b/packages/subgraph-service/ignition/modules/SubgraphService.ts new file mode 100644 index 000000000..b8ab37711 --- /dev/null +++ b/packages/subgraph-service/ignition/modules/SubgraphService.ts @@ -0,0 +1,60 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { deployImplementation } from '@graphprotocol/horizon/ignition/modules/proxy/implementation' +import { upgradeTransparentUpgradeableProxy } from '@graphprotocol/horizon/ignition/modules/proxy/TransparentUpgradeableProxy' + +import ProxyAdminArtifact from '@openzeppelin/contracts/build/contracts/ProxyAdmin.json' +import SubgraphServiceArtifact from '../../build/contracts/contracts/SubgraphService.sol/SubgraphService.json' +import TransparentUpgradeableProxyArtifact from '@openzeppelin/contracts/build/contracts/TransparentUpgradeableProxy.json' + +export default buildModule('SubgraphService', (m) => { + const deployer = m.getAccount(0) + const governor = m.getParameter('governor') + const pauseGuardian = m.getParameter('pauseGuardian') + const controllerAddress = m.getParameter('controllerAddress') + const subgraphServiceProxyAddress = m.getParameter('subgraphServiceProxyAddress') + const subgraphServiceProxyAdminAddress = m.getParameter('subgraphServiceProxyAdminAddress') + const disputeManagerProxyAddress = m.getParameter('disputeManagerProxyAddress') + const graphTallyCollectorAddress = m.getParameter('graphTallyCollectorAddress') + const curationAddress = m.getParameter('curationAddress') + const minimumProvisionTokens = m.getParameter('minimumProvisionTokens') + const maximumDelegationRatio = m.getParameter('maximumDelegationRatio') + const stakeToFeesRatio = m.getParameter('stakeToFeesRatio') + const maxPOIStaleness = m.getParameter('maxPOIStaleness') + const curationCut = m.getParameter('curationCut') + + const SubgraphServiceProxyAdmin = m.contractAt('ProxyAdmin', ProxyAdminArtifact, subgraphServiceProxyAdminAddress) + const SubgraphServiceProxy = m.contractAt('SubgraphServiceProxy', TransparentUpgradeableProxyArtifact, subgraphServiceProxyAddress) + + // Deploy implementation + const SubgraphServiceImplementation = deployImplementation(m, { + name: 'SubgraphService', + constructorArgs: [controllerAddress, disputeManagerProxyAddress, graphTallyCollectorAddress, curationAddress], + }) + + // Upgrade implementation + const SubgraphService = upgradeTransparentUpgradeableProxy(m, + SubgraphServiceProxyAdmin, + SubgraphServiceProxy, + SubgraphServiceImplementation, { + name: 'SubgraphService', + artifact: SubgraphServiceArtifact, + initArgs: [ + deployer, + minimumProvisionTokens, + maximumDelegationRatio, + stakeToFeesRatio, + ], + }) + + const callSetPauseGuardian = m.call(SubgraphService, 'setPauseGuardian', [pauseGuardian, true]) + const callSetMaxPOIStaleness = m.call(SubgraphService, 'setMaxPOIStaleness', [maxPOIStaleness]) + const callSetCurationCut = m.call(SubgraphService, 'setCurationCut', [curationCut]) + + m.call(SubgraphService, 'transferOwnership', [governor], { after: [callSetPauseGuardian, callSetMaxPOIStaleness, callSetCurationCut] }) + m.call(SubgraphServiceProxyAdmin, 'transferOwnership', [governor], { after: [callSetPauseGuardian, callSetMaxPOIStaleness, callSetCurationCut] }) + + return { + SubgraphService, + SubgraphServiceImplementation, + } +}) diff --git a/packages/subgraph-service/ignition/modules/deploy/deploy-1.ts b/packages/subgraph-service/ignition/modules/deploy/deploy-1.ts new file mode 100644 index 000000000..027c4563f --- /dev/null +++ b/packages/subgraph-service/ignition/modules/deploy/deploy-1.ts @@ -0,0 +1,19 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import ProxiesModule from '../Proxies' + +export default buildModule('SubgraphService_Deploy_1', (m) => { + const { + SubgraphServiceProxy, + SubgraphServiceProxyAdmin, + DisputeManagerProxy, + DisputeManagerProxyAdmin, + } = m.useModule(ProxiesModule) + + return { + Transparent_Proxy_SubgraphService: SubgraphServiceProxy, + Transparent_ProxyAdmin_SubgraphService: SubgraphServiceProxyAdmin, + Transparent_Proxy_DisputeManager: DisputeManagerProxy, + Transparent_ProxyAdmin_DisputeManager: DisputeManagerProxyAdmin, + } +}) diff --git a/packages/subgraph-service/ignition/modules/deploy/deploy-2.ts b/packages/subgraph-service/ignition/modules/deploy/deploy-2.ts new file mode 100644 index 000000000..6f9ca65c5 --- /dev/null +++ b/packages/subgraph-service/ignition/modules/deploy/deploy-2.ts @@ -0,0 +1,19 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import CurationModule from '../Curation' +import DisputeManagerModule from '../DisputeManager' +import SubgraphServiceModule from '../SubgraphService' + +export default buildModule('SubgraphService_Deploy_2', (m) => { + const { DisputeManager, DisputeManagerImplementation } = m.useModule(DisputeManagerModule) + const { SubgraphService, SubgraphServiceImplementation } = m.useModule(SubgraphServiceModule) + const { L2Curation, L2CurationImplementation } = m.useModule(CurationModule) + return { + Transparent_Proxy_DisputeManager: DisputeManager, + Implementation_DisputeManager: DisputeManagerImplementation, + Transparent_Proxy_SubgraphService: SubgraphService, + Implementation_SubgraphService: SubgraphServiceImplementation, + Graph_Proxy_L2Curation: L2Curation, + Implementation_L2Curation: L2CurationImplementation, + } +}) diff --git a/packages/subgraph-service/ignition/modules/migrate/migrate-1.ts b/packages/subgraph-service/ignition/modules/migrate/migrate-1.ts new file mode 100644 index 000000000..66b4bf74c --- /dev/null +++ b/packages/subgraph-service/ignition/modules/migrate/migrate-1.ts @@ -0,0 +1,19 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import ProxiesModule from '../Proxies' + +export default buildModule('SubgraphService_Migrate_1', (m) => { + const { + SubgraphServiceProxy, + SubgraphServiceProxyAdmin, + DisputeManagerProxy, + DisputeManagerProxyAdmin, + } = m.useModule(ProxiesModule) + + return { + Transparent_Proxy_SubgraphService: SubgraphServiceProxy, + Transparent_ProxyAdmin_SubgraphService: SubgraphServiceProxyAdmin, + Transparent_Proxy_DisputeManager: DisputeManagerProxy, + Transparent_ProxyAdmin_DisputeManager: DisputeManagerProxyAdmin, + } +}) diff --git a/packages/subgraph-service/ignition/modules/migrate/migrate-2.ts b/packages/subgraph-service/ignition/modules/migrate/migrate-2.ts new file mode 100644 index 000000000..e50115193 --- /dev/null +++ b/packages/subgraph-service/ignition/modules/migrate/migrate-2.ts @@ -0,0 +1,16 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +import DisputeManagerModule from '../DisputeManager' +import SubgraphServiceModule from '../SubgraphService' + +export default buildModule('SubgraphService_Migrate_2', (m) => { + const { DisputeManager, DisputeManagerImplementation } = m.useModule(DisputeManagerModule) + const { SubgraphService, SubgraphServiceImplementation } = m.useModule(SubgraphServiceModule) + + return { + Transparent_Proxy_DisputeManager: DisputeManager, + Transparent_ProxyAdmin_DisputeManager: DisputeManagerImplementation, + Transparent_Proxy_SubgraphService: SubgraphService, + Transparent_ProxyAdmin_SubgraphService: SubgraphServiceImplementation, + } +}) diff --git a/packages/subgraph-service/lib/forge-std b/packages/subgraph-service/lib/forge-std new file mode 160000 index 000000000..bb4ceea94 --- /dev/null +++ b/packages/subgraph-service/lib/forge-std @@ -0,0 +1 @@ +Subproject commit bb4ceea94d6f10eeb5b41dc2391c6c8bf8e734ef diff --git a/packages/subgraph-service/lib/openzeppelin-foundry-upgrades b/packages/subgraph-service/lib/openzeppelin-foundry-upgrades new file mode 160000 index 000000000..4cd15fc50 --- /dev/null +++ b/packages/subgraph-service/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit 4cd15fc50b141c77d8cc9ff8efb44d00e841a299 diff --git a/packages/subgraph-service/natspec-smells.config.js b/packages/subgraph-service/natspec-smells.config.js new file mode 100644 index 000000000..b5f020bc5 --- /dev/null +++ b/packages/subgraph-service/natspec-smells.config.js @@ -0,0 +1,10 @@ +/** + * List of supported options: https://github.com/defi-wonderland/natspec-smells?tab=readme-ov-file#options + */ + +/** @type {import('@defi-wonderland/natspec-smells').Config} */ +module.exports = { + include: 'contracts/**/*.sol', + exclude: 'test/**/*.sol', + constructorNatspec: true, +} diff --git a/packages/subgraph-service/package.json b/packages/subgraph-service/package.json new file mode 100644 index 000000000..fabb9cade --- /dev/null +++ b/packages/subgraph-service/package.json @@ -0,0 +1,81 @@ +{ + "name": "@graphprotocol/subgraph-service", + "version": "0.0.1", + "description": "", + "author": "The Graph core devs", + "license": "GPL-2.0-or-later", + "types": "typechain-types/index.ts", + "files": [ + "build/contracts/**/*", + "README.md", + "addresses.json" + ], + "scripts": { + "lint": "yarn lint:ts && yarn lint:sol", + "lint:ts": "eslint '**/*.{js,ts}' --fix", + "lint:sol": "yarn lint:sol:prettier && yarn lint:sol:solhint", + "lint:sol:prettier": "prettier --write contracts/**/*.sol test/**/*.sol", + "lint:sol:solhint": "solhint --noPrompt --fix contracts/**/*.sol --config node_modules/solhint-graph-config/index.js", + "lint:sol:natspec": "natspec-smells --config natspec-smells.config.js", + "clean": "rm -rf build dist cache cache_forge typechain-types", + "build": "BUILD_RUN=true hardhat compile", + "test": "forge test && hardhat test" + }, + "devDependencies": { + "@defi-wonderland/natspec-smells": "^1.1.6", + "@graphprotocol/contracts": "workspace:^7.0.0", + "@graphprotocol/horizon": "workspace:^0.0.1", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.8", + "@nomicfoundation/hardhat-foundry": "^1.1.1", + "@nomicfoundation/hardhat-ignition": "^0.15.9", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.9", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.10", + "@nomicfoundation/ignition-core": "^0.15.9", + "@openzeppelin/contracts": "^5.0.2", + "@openzeppelin/contracts-upgradeable": "^5.0.2", + "@typechain/ethers-v6": "^0.5.0", + "@typechain/hardhat": "^9.0.0", + "@types/chai": "^4.2.0", + "@types/mocha": ">=9.1.0", + "@types/node": ">=16.0.0", + "chai": "^4.2.0", + "eslint": "^8.56.0", + "eslint-graph-config": "workspace:^0.0.1", + "ethers": "^6.13.4", + "hardhat": "^2.22.18", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-gas-reporter": "^1.0.8", + "hardhat-graph-protocol": "workspace:^0.0.1", + "hardhat-secure-accounts": "^1.0.5", + "hardhat-storage-layout": "^0.1.7", + "json5": "^2.2.3", + "lint-staged": "^15.2.2", + "prettier": "^3.2.5", + "prettier-plugin-solidity": "^1.3.1", + "solhint": "^4.5.4", + "solhint-graph-config": "workspace:^0.0.1", + "solhint-plugin-graph": "workspace:^0.0.1", + "solidity-coverage": "^0.8.0", + "solidity-docgen": "^0.6.0-beta.36", + "ts-node": ">=8.0.0", + "typechain": "^8.3.0", + "typescript": "^5.3.3" + }, + "lint-staged": { + "contracts/**/*.sol": [ + "yarn lint:sol" + ], + "**/*.ts": [ + "yarn lint:ts" + ], + "**/*.js": [ + "yarn lint:ts" + ], + "**/*.json": [ + "yarn lint:ts" + ] + } +} diff --git a/packages/subgraph-service/prettier.config.js b/packages/subgraph-service/prettier.config.js new file mode 100644 index 000000000..5b8e866f2 --- /dev/null +++ b/packages/subgraph-service/prettier.config.js @@ -0,0 +1,2 @@ +const prettierGraphConfig = require('solhint-graph-config/prettier') +module.exports = prettierGraphConfig diff --git a/packages/subgraph-service/remappings.txt b/packages/subgraph-service/remappings.txt new file mode 100644 index 000000000..d5c1ff26c --- /dev/null +++ b/packages/subgraph-service/remappings.txt @@ -0,0 +1,9 @@ +@graphprotocol/contracts/=node_modules/@graphprotocol/contracts/ +@graphprotocol/horizon/=node_modules/@graphprotocol/horizon/ +forge-std/=lib/forge-std/src/ +ds-test/=lib/forge-std/lib/ds-test/src/ +eth-gas-reporter/=node_modules/eth-gas-reporter/ +hardhat/=node_modules/hardhat/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src \ No newline at end of file diff --git a/packages/subgraph-service/tasks/deploy.ts b/packages/subgraph-service/tasks/deploy.ts new file mode 100644 index 000000000..1e545c9f4 --- /dev/null +++ b/packages/subgraph-service/tasks/deploy.ts @@ -0,0 +1,208 @@ +/* eslint-disable no-case-declarations */ +import { task, types } from 'hardhat/config' +import { IgnitionHelper } from 'hardhat-graph-protocol/sdk' + +import type { AddressBook } from '../../hardhat-graph-protocol/src/sdk/address-book' +import type { HardhatRuntimeEnvironment } from 'hardhat/types' + +import Deploy1Module from '../ignition/modules/deploy/deploy-1' +import Deploy2Module from '../ignition/modules/deploy/deploy-2' +import HorizonModule from '@graphprotocol/horizon/ignition/modules/deploy' + +// Horizon needs the SubgraphService proxy address before it can be deployed +// But SubgraphService and DisputeManager implementations need Horizon... +// So the deployment order is: +// - Deploy SubgraphService and DisputeManager proxies +// - Deploy Horizon +// - Deploy SubgraphService and DisputeManager implementations +task('deploy:protocol', 'Deploy a new version of the Graph Protocol Horizon contracts - with Subgraph Service') + .addOptionalParam('subgraphServiceConfig', 'Name of the Subgraph Service configuration file to use. Format is "protocol..json5", file must be in the "ignition/configs/" directory. Defaults to network name.', undefined, types.string) + .addOptionalParam('horizonConfig', 'Name of the Horizon configuration file to use. Format is "protocol..json5", file must be in the "ignition/configs/" directory in the horizon package. Defaults to network name.', undefined, types.string) + .setAction(async (args, hre: HardhatRuntimeEnvironment) => { + const graph = hre.graph() + + // Load configuration files for the deployment + console.log('\n========== ⚙️ Deployment configuration ==========') + const { config: HorizonConfig, file: horizonFile } = IgnitionHelper.loadConfig('./node_modules/@graphprotocol/horizon/ignition/configs', 'protocol', args.horizonConfig ?? hre.network.name) + const { config: SubgraphServiceConfig, file: subgraphServiceFile } = IgnitionHelper.loadConfig('./ignition/configs/', 'protocol', args.subgraphServiceConfig ?? hre.network.name) + console.log(`Loaded Horizon migration configuration from ${horizonFile}`) + console.log(`Loaded Subgraph Service migration configuration from ${subgraphServiceFile}`) + + // Display the deployer -- this also triggers the secure accounts prompt if being used + console.log('\n========== 🔑 Deployer account ==========') + const signers = await hre.ethers.getSigners() + const deployer = signers[0] + console.log('Using deployer account:', deployer.address) + const balance = await hre.ethers.provider.getBalance(deployer.address) + console.log('Deployer balance:', hre.ethers.formatEther(balance), 'ETH') + if (balance === 0n) { + console.error('Error: Deployer account has no ETH balance') + process.exit(1) + } + + // 1. Deploy SubgraphService and DisputeManager proxies + console.log(`\n========== 🚧 SubgraphService and DisputeManager proxies ==========`) + const proxiesDeployment = await hre.ignition.deploy(Deploy1Module, { + displayUi: true, + parameters: SubgraphServiceConfig, + }) + + // 2. Deploy Horizon + console.log(`\n========== 🚧 Deploy Horizon ==========`) + const horizonDeployment = await hre.ignition.deploy(HorizonModule, { + displayUi: true, + parameters: IgnitionHelper.patchConfig(HorizonConfig, { + $global: { + subgraphServiceProxyAddress: proxiesDeployment.Transparent_Proxy_SubgraphService.target as string, + }, + }), + }) + + // 3. Deploy SubgraphService and DisputeManager implementations + console.log(`\n========== 🚧 Deploy SubgraphService implementations and upgrade them ==========`) + const subgraphServiceDeployment = await hre.ignition.deploy(Deploy2Module, { + displayUi: true, + parameters: IgnitionHelper.patchConfig(SubgraphServiceConfig, { + $global: { + controllerAddress: horizonDeployment.Controller.target as string, + disputeManagerProxyAddress: proxiesDeployment.Transparent_Proxy_DisputeManager.target as string, + curationAddress: horizonDeployment.Graph_Proxy_L2Curation.target as string, + curationImplementationAddress: horizonDeployment.Implementation_L2Curation.target as string, + subgraphServiceProxyAddress: proxiesDeployment.Transparent_Proxy_SubgraphService.target as string, + }, + DisputeManager: { + disputeManagerProxyAdminAddress: proxiesDeployment.Transparent_ProxyAdmin_DisputeManager.target as string, + }, + SubgraphService: { + subgraphServiceProxyAdminAddress: proxiesDeployment.Transparent_ProxyAdmin_SubgraphService.target as string, + graphTallyCollectorAddress: horizonDeployment.GraphTallyCollector.target as string, + }, + }), + }) + + // Save the addresses to the address book + console.log('\n========== 📖 Updating address book ==========') + IgnitionHelper.saveToAddressBook(horizonDeployment, hre.network.config.chainId, graph.horizon!.addressBook) + IgnitionHelper.saveToAddressBook(proxiesDeployment, hre.network.config.chainId, graph.subgraphService!.addressBook) + IgnitionHelper.saveToAddressBook(subgraphServiceDeployment, hre.network.config.chainId, graph.subgraphService!.addressBook) + console.log(`Address book at ${graph.horizon!.addressBook.file} updated!`) + console.log(`Address book at ${graph.subgraphService!.addressBook.file} updated!`) + console.log('Note that Horizon deployment addresses are updated in the Horizon address book') + + console.log('\n\n🎉 ✨ 🚀 ✅ Deployment complete! 🎉 ✨ 🚀 ✅') + }) + +task('deploy:migrate', 'Deploy the Subgraph Service on an existing Horizon deployment') + .addOptionalParam('step', 'Migration step to run (1, 2)', undefined, types.int) + .addOptionalParam('subgraphServiceConfig', 'Name of the Subgraph Service configuration file to use. Format is "migrate..json5", file must be in the "ignition/configs/" directory. Defaults to network name.', undefined, types.string) + .addFlag('patchConfig', 'Patch configuration file using address book values - does not save changes') + .setAction(async (args, hre: HardhatRuntimeEnvironment) => { + // Task parameters + const step: number = args.step ?? 0 + const patchConfig: boolean = args.patchConfig ?? false + + const graph = hre.graph() + console.log(getHorizonBanner()) + + // Migration step to run + console.log('\n========== 🏗️ Migration steps ==========') + const validSteps = [1, 2] + if (!validSteps.includes(step)) { + console.error(`Error: Invalid migration step provided: ${step}`) + console.error(`Valid steps are: ${validSteps.join(', ')}`) + process.exit(1) + } + console.log(`Running migration step: ${step}`) + + // Load configuration for the migration + console.log('\n========== ⚙️ Deployment configuration ==========') + const { config: SubgraphServiceMigrateConfig, file } = IgnitionHelper.loadConfig('./ignition/configs/', 'migrate', args.subgraphServiceConfig ?? hre.network.name) + console.log(`Loaded migration configuration from ${file}`) + + // Display the deployer -- this also triggers the secure accounts prompt if being used + console.log('\n========== 🔑 Deployer account ==========') + const signers = await hre.ethers.getSigners() + const deployer = signers[0] + console.log('Using deployer account:', deployer.address) + const balance = await hre.ethers.provider.getBalance(deployer.address) + console.log('Deployer balance:', hre.ethers.formatEther(balance), 'ETH') + if (balance === 0n) { + console.error('Error: Deployer account has no ETH balance') + process.exit(1) + } + + // Run migration step + console.log(`\n========== 🚧 Running migration: step ${step} ==========`) + const MigrationModule = require(`../ignition/modules/migrate/migrate-${step}`).default + const deployment = await hre.ignition.deploy( + MigrationModule, + { + displayUi: true, + parameters: patchConfig ? _patchStepConfig(step, SubgraphServiceMigrateConfig, graph.subgraphService!.addressBook, graph.horizon!.addressBook) : SubgraphServiceMigrateConfig, + deploymentId: `subgraph-service-${hre.network.name}`, + }) + + // Update address book + console.log('\n========== 📖 Updating address book ==========') + IgnitionHelper.saveToAddressBook(deployment, hre.network.config.chainId, graph.subgraphService!.addressBook) + console.log(`Address book at ${graph.subgraphService!.addressBook.file} updated!`) + + console.log('\n\n🎉 ✨ 🚀 ✅ Migration complete! 🎉 ✨ 🚀 ✅') + }) + +// This function patches the Ignition configuration object using an address book to fill in the gaps +// The resulting configuration is not saved back to the configuration file + +function _patchStepConfig( + step: number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: any, + addressBook: AddressBook, + horizonAddressBook: AddressBook, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + let patchedConfig = config + + switch (step) { + case 2: + const SubgraphService = addressBook.getEntry('SubgraphService') + const DisputeManager = addressBook.getEntry('DisputeManager') + const GraphTallyCollector = horizonAddressBook.getEntry('GraphTallyCollector') + + patchedConfig = IgnitionHelper.patchConfig(config, { + $global: { + subgraphServiceProxyAddress: SubgraphService.address, + }, + SubgraphService: { + subgraphServiceProxyAdminAddress: SubgraphService.proxyAdmin, + graphTallyCollectorAddress: GraphTallyCollector.address, + disputeManagerProxyAddress: DisputeManager.address, + }, + DisputeManager: { + disputeManagerProxyAddress: DisputeManager.address, + disputeManagerProxyAdminAddress: DisputeManager.proxyAdmin, + }, + }) + break + } + + return patchedConfig +} + +function getHorizonBanner(): string { + return ` + ██╗ ██╗ ██████╗ ██████╗ ██╗███████╗ ██████╗ ███╗ ██╗ + ██║ ██║██╔═══██╗██╔══██╗██║╚══███╔╝██╔═══██╗████╗ ██║ + ███████║██║ ██║██████╔╝██║ ███╔╝ ██║ ██║██╔██╗ ██║ + ██╔══██║██║ ██║██╔══██╗██║ ███╔╝ ██║ ██║██║╚██╗██║ + ██║ ██║╚██████╔╝██║ ██║██║███████╗╚██████╔╝██║ ╚████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ + + ██╗ ██╗██████╗ ██████╗ ██████╗ █████╗ ██████╗ ███████╗ + ██║ ██║██╔══██╗██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██╔════╝ + ██║ ██║██████╔╝██║ ███╗██████╔╝███████║██║ ██║█████╗ + ██║ ██║██╔═══╝ ██║ ██║██╔══██╗██╔══██║██║ ██║██╔══╝ + ╚██████╔╝██║ ╚██████╔╝██║ ██║██║ ██║██████╔╝███████╗ + ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝ + ` +} diff --git a/packages/subgraph-service/test/SubgraphBaseTest.t.sol b/packages/subgraph-service/test/SubgraphBaseTest.t.sol new file mode 100644 index 000000000..1a5929c60 --- /dev/null +++ b/packages/subgraph-service/test/SubgraphBaseTest.t.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; +import { GraphPayments } from "@graphprotocol/horizon/contracts/payments/GraphPayments.sol"; +import { GraphProxy } from "@graphprotocol/contracts/contracts/upgrades/GraphProxy.sol"; +import { GraphProxyAdmin } from "@graphprotocol/contracts/contracts/upgrades/GraphProxyAdmin.sol"; +import { HorizonStaking } from "@graphprotocol/horizon/contracts/staking/HorizonStaking.sol"; +import { HorizonStakingExtension } from "@graphprotocol/horizon/contracts/staking/HorizonStakingExtension.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; +import { IPaymentsEscrow } from "@graphprotocol/horizon/contracts/interfaces/IPaymentsEscrow.sol"; +import { IGraphTallyCollector } from "@graphprotocol/horizon/contracts/interfaces/IGraphTallyCollector.sol"; +import { GraphTallyCollector } from "@graphprotocol/horizon/contracts/payments/collectors/GraphTallyCollector.sol"; +import { PaymentsEscrow } from "@graphprotocol/horizon/contracts/payments/PaymentsEscrow.sol"; +import { UnsafeUpgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import { Constants } from "./utils/Constants.sol"; +import { DisputeManager } from "../contracts/DisputeManager.sol"; +import { SubgraphService } from "../contracts/SubgraphService.sol"; +import { Users } from "./utils/Users.sol"; +import { Utils } from "./utils/Utils.sol"; + +import { MockCuration } from "./mocks/MockCuration.sol"; +import { MockGRTToken } from "./mocks/MockGRTToken.sol"; +import { MockRewardsManager } from "./mocks/MockRewardsManager.sol"; +import { MockEpochManager } from "./mocks/MockEpochManager.sol"; + +abstract contract SubgraphBaseTest is Utils, Constants { + /* + * VARIABLES + */ + + /* Contracts */ + + GraphProxyAdmin proxyAdmin; + Controller controller; + SubgraphService subgraphService; + DisputeManager disputeManager; + IHorizonStaking staking; + GraphPayments graphPayments; + IPaymentsEscrow escrow; + GraphTallyCollector graphTallyCollector; + + HorizonStaking private stakingBase; + HorizonStakingExtension private stakingExtension; + + MockCuration curation; + MockGRTToken token; + MockRewardsManager rewardsManager; + MockEpochManager epochManager; + + /* Users */ + + Users internal users; + + /* + * SET UP + */ + + function setUp() public virtual { + token = new MockGRTToken(); + + // Setup Users + users = Users({ + governor: createUser("governor"), + deployer: createUser("deployer"), + indexer: createUser("indexer"), + operator: createUser("operator"), + gateway: createUser("gateway"), + verifier: createUser("verifier"), + delegator: createUser("delegator"), + arbitrator: createUser("arbitrator"), + fisherman: createUser("fisherman"), + rewardsDestination: createUser("rewardsDestination"), + pauseGuardian: createUser("pauseGuardian") + }); + + deployProtocolContracts(); + setupProtocol(); + unpauseProtocol(); + vm.stopPrank(); + } + + function deployProtocolContracts() private { + resetPrank(users.governor); + proxyAdmin = new GraphProxyAdmin(); + controller = new Controller(); + + resetPrank(users.deployer); + GraphProxy stakingProxy = new GraphProxy(address(0), address(proxyAdmin)); + rewardsManager = new MockRewardsManager(token, rewardsPerSignal, rewardsPerSubgraphAllocationUpdate); + curation = new MockCuration(); + epochManager = new MockEpochManager(); + + // GraphPayments predict address + bytes32 saltGraphPayments = keccak256("GraphPaymentsSalt"); + bytes32 paymentsHash = keccak256( + bytes.concat( + vm.getCode("GraphPayments.sol:GraphPayments"), + abi.encode(address(controller), protocolPaymentCut) + ) + ); + address predictedGraphPaymentsAddress = vm.computeCreate2Address( + saltGraphPayments, + paymentsHash, + users.deployer + ); + + // GraphEscrow predict address + bytes32 saltEscrow = keccak256("GraphEscrowSalt"); + bytes32 escrowHash = keccak256( + bytes.concat( + vm.getCode("PaymentsEscrow.sol:PaymentsEscrow"), + abi.encode(address(controller), withdrawEscrowThawingPeriod) + ) + ); + address predictedEscrowAddress = vm.computeCreate2Address(saltEscrow, escrowHash, users.deployer); + + resetPrank(users.governor); + controller.setContractProxy(keccak256("GraphToken"), address(token)); + controller.setContractProxy(keccak256("Staking"), address(stakingProxy)); + controller.setContractProxy(keccak256("RewardsManager"), address(rewardsManager)); + controller.setContractProxy(keccak256("GraphPayments"), predictedGraphPaymentsAddress); + controller.setContractProxy(keccak256("PaymentsEscrow"), predictedEscrowAddress); + controller.setContractProxy(keccak256("EpochManager"), address(epochManager)); + controller.setContractProxy(keccak256("GraphTokenGateway"), makeAddr("GraphTokenGateway")); + controller.setContractProxy(keccak256("GraphProxyAdmin"), makeAddr("GraphProxyAdmin")); + controller.setContractProxy(keccak256("Curation"), address(curation)); + + resetPrank(users.deployer); + address disputeManagerImplementation = address(new DisputeManager(address(controller))); + address disputeManagerProxy = UnsafeUpgrades.deployTransparentProxy( + disputeManagerImplementation, + users.governor, + abi.encodeCall( + DisputeManager.initialize, + ( + users.deployer, + users.arbitrator, + disputePeriod, + disputeDeposit, + fishermanRewardPercentage, + maxSlashingPercentage + ) + ) + ); + disputeManager = DisputeManager(disputeManagerProxy); + disputeManager.transferOwnership(users.governor); + + graphTallyCollector = new GraphTallyCollector( + "GraphTallyCollector", + "1", + address(controller), + revokeSignerThawingPeriod + ); + address subgraphServiceImplementation = address( + new SubgraphService( + address(controller), + address(disputeManager), + address(graphTallyCollector), + address(curation) + ) + ); + address subgraphServiceProxy = UnsafeUpgrades.deployTransparentProxy( + subgraphServiceImplementation, + users.governor, + abi.encodeCall( + SubgraphService.initialize, + (users.deployer, minimumProvisionTokens, delegationRatio, stakeToFeesRatio) + ) + ); + subgraphService = SubgraphService(subgraphServiceProxy); + + stakingExtension = new HorizonStakingExtension(address(controller), address(subgraphService)); + stakingBase = new HorizonStaking(address(controller), address(stakingExtension), address(subgraphService)); + + graphPayments = new GraphPayments{ salt: saltGraphPayments }(address(controller), protocolPaymentCut); + escrow = new PaymentsEscrow{ salt: saltEscrow }(address(controller), withdrawEscrowThawingPeriod); + + resetPrank(users.governor); + disputeManager.setSubgraphService(address(subgraphService)); + proxyAdmin.upgrade(stakingProxy, address(stakingBase)); + proxyAdmin.acceptProxy(stakingBase, stakingProxy); + staking = IHorizonStaking(address(stakingProxy)); + } + + function setupProtocol() private { + resetPrank(users.deployer); + subgraphService.transferOwnership(users.governor); + resetPrank(users.governor); + staking.setMaxThawingPeriod(MAX_WAIT_PERIOD); + epochManager.setEpochLength(EPOCH_LENGTH); + subgraphService.setMaxPOIStaleness(maxPOIStaleness); + subgraphService.setCurationCut(curationCut); + subgraphService.setPauseGuardian(users.pauseGuardian, true); + } + + function unpauseProtocol() private { + resetPrank(users.governor); + controller.setPaused(false); + } + + function createUser(string memory name) internal returns (address) { + address user = makeAddr(name); + vm.deal({ account: user, newBalance: 100 ether }); + deal({ token: address(token), to: user, give: 10_000_000_000 ether }); + vm.label({ account: user, newLabel: name }); + return user; + } + + function mint(address _address, uint256 amount) internal { + deal({ token: address(token), to: _address, give: amount }); + } + + function burn(address _from, uint256 amount) internal { + token.burnFrom(_from, amount); + } +} diff --git a/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol b/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol new file mode 100644 index 000000000..cfd5882b9 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol @@ -0,0 +1,736 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; +import { Attestation } from "../../contracts/libraries/Attestation.sol"; +import { Allocation } from "../../contracts/libraries/Allocation.sol"; +import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; + +import { SubgraphServiceSharedTest } from "../shared/SubgraphServiceShared.t.sol"; + +contract DisputeManagerTest is SubgraphServiceSharedTest { + using PPMMath for uint256; + + /* + * MODIFIERS + */ + + modifier useGovernor() { + vm.startPrank(users.governor); + _; + vm.stopPrank(); + } + + modifier useFisherman() { + vm.startPrank(users.fisherman); + _; + vm.stopPrank(); + } + + /* + * ACTIONS + */ + + function _setArbitrator(address _arbitrator) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.ArbitratorSet(_arbitrator); + disputeManager.setArbitrator(_arbitrator); + assertEq(disputeManager.arbitrator(), _arbitrator, "Arbitrator should be set."); + } + + function _setFishermanRewardCut(uint32 _fishermanRewardCut) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.FishermanRewardCutSet(_fishermanRewardCut); + disputeManager.setFishermanRewardCut(_fishermanRewardCut); + assertEq(disputeManager.fishermanRewardCut(), _fishermanRewardCut, "Fisherman reward cut should be set."); + } + + function _setMaxSlashingCut(uint32 _maxSlashingCut) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.MaxSlashingCutSet(_maxSlashingCut); + disputeManager.setMaxSlashingCut(_maxSlashingCut); + assertEq(disputeManager.maxSlashingCut(), _maxSlashingCut, "Max slashing cut should be set."); + } + + function _setDisputeDeposit(uint256 _disputeDeposit) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeDepositSet(_disputeDeposit); + disputeManager.setDisputeDeposit(_disputeDeposit); + assertEq(disputeManager.disputeDeposit(), _disputeDeposit, "Dispute deposit should be set."); + } + + function _setSubgraphService(address _subgraphService) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.SubgraphServiceSet(_subgraphService); + disputeManager.setSubgraphService(_subgraphService); + assertEq(address(disputeManager.subgraphService()), _subgraphService, "Subgraph service should be set."); + } + + function _createIndexingDispute(address _allocationId, bytes32 _poi) internal returns (bytes32) { + (, address fisherman, ) = vm.readCallers(); + bytes32 expectedDisputeId = keccak256(abi.encodePacked(_allocationId, _poi)); + uint256 disputeDeposit = disputeManager.disputeDeposit(); + uint256 beforeFishermanBalance = token.balanceOf(fisherman); + Allocation.State memory alloc = subgraphService.getAllocation(_allocationId); + uint256 stakeSnapshot = disputeManager.getStakeSnapshot(alloc.indexer); + + // Approve the dispute deposit + token.approve(address(disputeManager), disputeDeposit); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.IndexingDisputeCreated( + expectedDisputeId, + alloc.indexer, + fisherman, + disputeDeposit, + _allocationId, + _poi, + stakeSnapshot + ); + + // Create the indexing dispute + bytes32 _disputeId = disputeManager.createIndexingDispute(_allocationId, _poi); + + // Check that the dispute was created and that it has the correct ID + assertTrue(disputeManager.isDisputeCreated(_disputeId), "Dispute should be created."); + assertEq(expectedDisputeId, _disputeId, "Dispute ID should match"); + + // Check dispute values + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + assertEq(dispute.indexer, alloc.indexer, "Indexer should match"); + assertEq(dispute.fisherman, fisherman, "Fisherman should match"); + assertEq(dispute.deposit, disputeDeposit, "Deposit should match"); + assertEq(dispute.relatedDisputeId, bytes32(0), "Related dispute ID should be empty"); + assertEq( + uint8(dispute.disputeType), + uint8(IDisputeManager.DisputeType.IndexingDispute), + "Dispute type should be indexing" + ); + assertEq( + uint8(dispute.status), + uint8(IDisputeManager.DisputeStatus.Pending), + "Dispute status should be pending" + ); + assertEq(dispute.createdAt, block.timestamp, "Created at should match"); + assertEq(dispute.stakeSnapshot, stakeSnapshot, "Stake snapshot should match"); + + // Check that the fisherman was charged the dispute deposit + uint256 afterFishermanBalance = token.balanceOf(fisherman); + assertEq( + afterFishermanBalance, + beforeFishermanBalance - disputeDeposit, + "Fisherman should be charged the dispute deposit" + ); + + return _disputeId; + } + + function _createQueryDispute(bytes memory _attestationData) internal returns (bytes32) { + (, address fisherman, ) = vm.readCallers(); + Attestation.State memory attestation = Attestation.parse(_attestationData); + address indexer = disputeManager.getAttestationIndexer(attestation); + bytes32 expectedDisputeId = keccak256( + abi.encodePacked( + attestation.requestCID, + attestation.responseCID, + attestation.subgraphDeploymentId, + indexer, + fisherman + ) + ); + uint256 disputeDeposit = disputeManager.disputeDeposit(); + uint256 beforeFishermanBalance = token.balanceOf(fisherman); + uint256 stakeSnapshot = disputeManager.getStakeSnapshot(indexer); + + // Approve the dispute deposit + token.approve(address(disputeManager), disputeDeposit); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.QueryDisputeCreated( + expectedDisputeId, + indexer, + fisherman, + disputeDeposit, + attestation.subgraphDeploymentId, + _attestationData, + stakeSnapshot + ); + + bytes32 _disputeID = disputeManager.createQueryDispute(_attestationData); + + // Check that the dispute was created and that it has the correct ID + assertTrue(disputeManager.isDisputeCreated(_disputeID), "Dispute should be created."); + assertEq(expectedDisputeId, _disputeID, "Dispute ID should match"); + + // Check dispute values + IDisputeManager.Dispute memory dispute = _getDispute(_disputeID); + assertEq(dispute.indexer, indexer, "Indexer should match"); + assertEq(dispute.fisherman, fisherman, "Fisherman should match"); + assertEq(dispute.deposit, disputeDeposit, "Deposit should match"); + assertEq(dispute.relatedDisputeId, bytes32(0), "Related dispute ID should be empty"); + assertEq( + uint8(dispute.disputeType), + uint8(IDisputeManager.DisputeType.QueryDispute), + "Dispute type should be query" + ); + assertEq( + uint8(dispute.status), + uint8(IDisputeManager.DisputeStatus.Pending), + "Dispute status should be pending" + ); + assertEq(dispute.createdAt, block.timestamp, "Created at should match"); + assertEq(dispute.stakeSnapshot, stakeSnapshot, "Stake snapshot should match"); + + // Check that the fisherman was charged the dispute deposit + uint256 afterFishermanBalance = token.balanceOf(fisherman); + assertEq( + afterFishermanBalance, + beforeFishermanBalance - disputeDeposit, + "Fisherman should be charged the dispute deposit" + ); + + return _disputeID; + } + + struct BeforeValues_CreateQueryDisputeConflict { + Attestation.State attestation1; + Attestation.State attestation2; + address indexer1; + address indexer2; + uint256 stakeSnapshot1; + uint256 stakeSnapshot2; + } + + function _createQueryDisputeConflict( + bytes memory attestationData1, + bytes memory attestationData2 + ) internal returns (bytes32, bytes32) { + (, address fisherman, ) = vm.readCallers(); + + BeforeValues_CreateQueryDisputeConflict memory beforeValues; + beforeValues.attestation1 = Attestation.parse(attestationData1); + beforeValues.attestation2 = Attestation.parse(attestationData2); + beforeValues.indexer1 = disputeManager.getAttestationIndexer(beforeValues.attestation1); + beforeValues.indexer2 = disputeManager.getAttestationIndexer(beforeValues.attestation2); + beforeValues.stakeSnapshot1 = disputeManager.getStakeSnapshot(beforeValues.indexer1); + beforeValues.stakeSnapshot2 = disputeManager.getStakeSnapshot(beforeValues.indexer2); + + uint256 beforeFishermanBalance = token.balanceOf(fisherman); + + // Approve the dispute deposit + token.approve(address(disputeManager), disputeDeposit); + + bytes32 expectedDisputeId1 = keccak256( + abi.encodePacked( + beforeValues.attestation1.requestCID, + beforeValues.attestation1.responseCID, + beforeValues.attestation1.subgraphDeploymentId, + beforeValues.indexer1, + fisherman + ) + ); + bytes32 expectedDisputeId2 = keccak256( + abi.encodePacked( + beforeValues.attestation2.requestCID, + beforeValues.attestation2.responseCID, + beforeValues.attestation2.subgraphDeploymentId, + beforeValues.indexer2, + fisherman + ) + ); + + // createQueryDisputeConflict + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.QueryDisputeCreated( + expectedDisputeId1, + beforeValues.indexer1, + fisherman, + disputeDeposit / 2, + beforeValues.attestation1.subgraphDeploymentId, + attestationData1, + beforeValues.stakeSnapshot1 + ); + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.QueryDisputeCreated( + expectedDisputeId2, + beforeValues.indexer2, + fisherman, + disputeDeposit / 2, + beforeValues.attestation2.subgraphDeploymentId, + attestationData2, + beforeValues.stakeSnapshot2 + ); + + (bytes32 _disputeId1, bytes32 _disputeId2) = disputeManager.createQueryDisputeConflict( + attestationData1, + attestationData2 + ); + + // Check that the disputes were created and that they have the correct IDs + assertTrue(disputeManager.isDisputeCreated(_disputeId1), "Dispute 1 should be created."); + assertTrue(disputeManager.isDisputeCreated(_disputeId2), "Dispute 2 should be created."); + assertEq(expectedDisputeId1, _disputeId1, "Dispute 1 ID should match"); + assertEq(expectedDisputeId2, _disputeId2, "Dispute 2 ID should match"); + + // Check dispute values + IDisputeManager.Dispute memory dispute1 = _getDispute(_disputeId1); + assertEq(dispute1.indexer, beforeValues.indexer1, "Indexer 1 should match"); + assertEq(dispute1.fisherman, fisherman, "Fisherman 1 should match"); + assertEq(dispute1.deposit, disputeDeposit / 2, "Deposit 1 should match"); + assertEq(dispute1.relatedDisputeId, _disputeId2, "Related dispute ID 1 should be the id of the other dispute"); + assertEq( + uint8(dispute1.disputeType), + uint8(IDisputeManager.DisputeType.QueryDispute), + "Dispute type 1 should be query" + ); + assertEq( + uint8(dispute1.status), + uint8(IDisputeManager.DisputeStatus.Pending), + "Dispute status 1 should be pending" + ); + assertEq(dispute1.createdAt, block.timestamp, "Created at 1 should match"); + assertEq(dispute1.stakeSnapshot, beforeValues.stakeSnapshot1, "Stake snapshot 1 should match"); + + IDisputeManager.Dispute memory dispute2 = _getDispute(_disputeId2); + assertEq(dispute2.indexer, beforeValues.indexer2, "Indexer 2 should match"); + assertEq(dispute2.fisherman, fisherman, "Fisherman 2 should match"); + assertEq(dispute2.deposit, disputeDeposit / 2, "Deposit 2 should match"); + assertEq(dispute2.relatedDisputeId, _disputeId1, "Related dispute ID 2 should be the id of the other dispute"); + assertEq( + uint8(dispute2.disputeType), + uint8(IDisputeManager.DisputeType.QueryDispute), + "Dispute type 2 should be query" + ); + assertEq( + uint8(dispute2.status), + uint8(IDisputeManager.DisputeStatus.Pending), + "Dispute status 2 should be pending" + ); + assertEq(dispute2.createdAt, block.timestamp, "Created at 2 should match"); + assertEq(dispute2.stakeSnapshot, beforeValues.stakeSnapshot2, "Stake snapshot 2 should match"); + + // Check that the fisherman was charged the dispute deposit + uint256 afterFishermanBalance = token.balanceOf(fisherman); + assertEq( + afterFishermanBalance, + beforeFishermanBalance - disputeDeposit, + "Fisherman should be charged the dispute deposit" + ); + + return (_disputeId1, _disputeId2); + } + + function _acceptDispute(bytes32 _disputeId, uint256 _tokensSlash) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + address fisherman = dispute.fisherman; + uint256 fishermanPreviousBalance = token.balanceOf(fisherman); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + uint256 disputeDeposit = dispute.deposit; + uint256 fishermanRewardPercentage = disputeManager.fishermanRewardCut(); + uint256 fishermanReward = _tokensSlash.mulPPM(fishermanRewardPercentage); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeAccepted( + _disputeId, + dispute.indexer, + dispute.fisherman, + dispute.deposit + fishermanReward + ); + + // Accept the dispute + disputeManager.acceptDispute(_disputeId, _tokensSlash); + + // Check fisherman's got their reward and their deposit (if any) back + uint256 fishermanExpectedBalance = fishermanPreviousBalance + fishermanReward + disputeDeposit; + assertEq( + token.balanceOf(fisherman), + fishermanExpectedBalance, + "Fisherman should get their reward and deposit back" + ); + + // Check indexer was slashed by the correct amount + uint256 expectedIndexerTokensAvailable; + if (_tokensSlash > indexerTokensAvailable) { + expectedIndexerTokensAvailable = 0; + } else { + expectedIndexerTokensAvailable = indexerTokensAvailable - _tokensSlash; + } + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + expectedIndexerTokensAvailable, + "Indexer should be slashed by the correct amount" + ); + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq( + uint8(dispute.status), + uint8(IDisputeManager.DisputeStatus.Accepted), + "Dispute status should be accepted" + ); + } + + struct FishermanParams { + address fisherman; + uint256 previousBalance; + uint256 disputeDeposit; + uint256 relatedDisputeDeposit; + uint256 rewardPercentage; + uint256 rewardFirstDispute; + uint256 rewardRelatedDispute; + uint256 totalReward; + uint256 expectedBalance; + } + + function _acceptDisputeConflict( + bytes32 _disputeId, + uint256 _tokensSlash, + bool _acceptRelatedDispute, + uint256 _tokensRelatedSlash + ) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + IDisputeManager.Dispute memory relatedDispute = _getDispute(dispute.relatedDisputeId); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + uint256 relatedIndexerTokensAvailable = staking.getProviderTokensAvailable( + relatedDispute.indexer, + address(subgraphService) + ); + + FishermanParams memory params; + params.fisherman = dispute.fisherman; + params.previousBalance = token.balanceOf(params.fisherman); + params.disputeDeposit = dispute.deposit; + params.relatedDisputeDeposit = relatedDispute.deposit; + params.rewardPercentage = disputeManager.fishermanRewardCut(); + params.rewardFirstDispute = _tokensSlash.mulPPM(params.rewardPercentage); + params.rewardRelatedDispute = (_acceptRelatedDispute) ? _tokensRelatedSlash.mulPPM(params.rewardPercentage) : 0; + params.totalReward = params.rewardFirstDispute + params.rewardRelatedDispute; + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeAccepted( + _disputeId, + dispute.indexer, + params.fisherman, + params.disputeDeposit + params.rewardFirstDispute + ); + + if (_acceptRelatedDispute) { + emit IDisputeManager.DisputeAccepted( + dispute.relatedDisputeId, + relatedDispute.indexer, + relatedDispute.fisherman, + relatedDispute.deposit + params.rewardRelatedDispute + ); + } else { + emit IDisputeManager.DisputeDrawn( + dispute.relatedDisputeId, + relatedDispute.indexer, + relatedDispute.fisherman, + relatedDispute.deposit + ); + } + + // Accept the dispute + disputeManager.acceptDisputeConflict(_disputeId, _tokensSlash, _acceptRelatedDispute, _tokensRelatedSlash); + + // Check fisherman's got their reward and their deposit back + params.expectedBalance = + params.previousBalance + + params.totalReward + + params.disputeDeposit + + params.relatedDisputeDeposit; + assertEq( + token.balanceOf(params.fisherman), + params.expectedBalance, + "Fisherman should get their reward and deposit back" + ); + + // If both disputes are for the same indexer, check that the indexer was slashed by the correct amount + if (dispute.indexer == relatedDispute.indexer) { + uint256 tokensToSlash = (_acceptRelatedDispute) ? _tokensSlash + _tokensRelatedSlash : _tokensSlash; + uint256 expectedIndexerTokensAvailable; + if (tokensToSlash > indexerTokensAvailable) { + expectedIndexerTokensAvailable = 0; + } else { + expectedIndexerTokensAvailable = indexerTokensAvailable - tokensToSlash; + } + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + expectedIndexerTokensAvailable, + "Indexer should be slashed by the correct amount" + ); + } else { + // Check indexer for first dispute was slashed by the correct amount + uint256 expectedIndexerTokensAvailable; + uint256 tokensToSlash = (_acceptRelatedDispute) ? _tokensSlash : _tokensSlash; + if (tokensToSlash > indexerTokensAvailable) { + expectedIndexerTokensAvailable = 0; + } else { + expectedIndexerTokensAvailable = indexerTokensAvailable - tokensToSlash; + } + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + expectedIndexerTokensAvailable, + "Indexer should be slashed by the correct amount" + ); + + // Check indexer for related dispute was slashed by the correct amount if it was accepted + if (_acceptRelatedDispute) { + uint256 expectedRelatedIndexerTokensAvailable; + if (_tokensRelatedSlash > relatedIndexerTokensAvailable) { + expectedRelatedIndexerTokensAvailable = 0; + } else { + expectedRelatedIndexerTokensAvailable = relatedIndexerTokensAvailable - _tokensRelatedSlash; + } + assertEq( + staking.getProviderTokensAvailable(relatedDispute.indexer, address(subgraphService)), + expectedRelatedIndexerTokensAvailable, + "Indexer should be slashed by the correct amount" + ); + } + } + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq( + uint8(dispute.status), + uint8(IDisputeManager.DisputeStatus.Accepted), + "Dispute status should be accepted" + ); + + // If there's a related dispute, check it + relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq( + uint8(relatedDispute.status), + _acceptRelatedDispute + ? uint8(IDisputeManager.DisputeStatus.Accepted) + : uint8(IDisputeManager.DisputeStatus.Drawn), + "Related dispute status should be drawn" + ); + } + + function _drawDispute(bytes32 _disputeId) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + bool isConflictingDispute = dispute.relatedDisputeId != bytes32(0); + IDisputeManager.Dispute memory relatedDispute; + if (isConflictingDispute) relatedDispute = _getDispute(dispute.relatedDisputeId); + address fisherman = dispute.fisherman; + uint256 fishermanPreviousBalance = token.balanceOf(fisherman); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeDrawn(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); + + if (isConflictingDispute) { + emit IDisputeManager.DisputeDrawn( + dispute.relatedDisputeId, + relatedDispute.indexer, + relatedDispute.fisherman, + relatedDispute.deposit + ); + } + // Draw the dispute + disputeManager.drawDispute(_disputeId); + + // Check that the fisherman got their deposit back + uint256 fishermanExpectedBalance = fishermanPreviousBalance + + dispute.deposit + + (isConflictingDispute ? relatedDispute.deposit : 0); + assertEq(token.balanceOf(fisherman), fishermanExpectedBalance, "Fisherman should receive their deposit back."); + + // Check that indexer was not slashed + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + indexerTokensAvailable, + "Indexer should not be slashed" + ); + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq(uint8(dispute.status), uint8(IDisputeManager.DisputeStatus.Drawn), "Dispute status should be drawn"); + + // If there's a related dispute, check that it was drawn too + if (dispute.relatedDisputeId != bytes32(0)) { + relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq( + uint8(relatedDispute.status), + uint8(IDisputeManager.DisputeStatus.Drawn), + "Related dispute status should be drawn" + ); + } + } + + function _rejectDispute(bytes32 _disputeId) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + address fisherman = dispute.fisherman; + uint256 fishermanPreviousBalance = token.balanceOf(fisherman); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeRejected(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); + + // Reject the dispute + disputeManager.rejectDispute(_disputeId); + + // Check that the fisherman didn't get their deposit back + assertEq(token.balanceOf(users.fisherman), fishermanPreviousBalance, "Fisherman should lose the deposit."); + + // Check that indexer was not slashed + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + indexerTokensAvailable, + "Indexer should not be slashed" + ); + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq( + uint8(dispute.status), + uint8(IDisputeManager.DisputeStatus.Rejected), + "Dispute status should be rejected" + ); + // Checl related id is empty + assertEq(dispute.relatedDisputeId, bytes32(0), "Related dispute ID should be empty"); + } + + function _cancelDispute(bytes32 _disputeId) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + bool isDisputeInConflict = dispute.relatedDisputeId != bytes32(0); + IDisputeManager.Dispute memory relatedDispute; + if (isDisputeInConflict) relatedDispute = _getDispute(dispute.relatedDisputeId); + address fisherman = dispute.fisherman; + uint256 fishermanPreviousBalance = token.balanceOf(fisherman); + uint256 disputePeriod = disputeManager.disputePeriod(); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + + // skip to end of dispute period + skip(disputePeriod + 1); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeCancelled(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); + + if (isDisputeInConflict) { + emit IDisputeManager.DisputeCancelled( + dispute.relatedDisputeId, + relatedDispute.indexer, + relatedDispute.fisherman, + relatedDispute.deposit + ); + } + + // Cancel the dispute + disputeManager.cancelDispute(_disputeId); + + // Check that the fisherman got their deposit back + uint256 fishermanExpectedBalance = fishermanPreviousBalance + + dispute.deposit + + (isDisputeInConflict ? relatedDispute.deposit : 0); + assertEq( + token.balanceOf(users.fisherman), + fishermanExpectedBalance, + "Fisherman should receive their deposit back." + ); + + // Check that indexer was not slashed + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + indexerTokensAvailable, + "Indexer should not be slashed" + ); + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq( + uint8(dispute.status), + uint8(IDisputeManager.DisputeStatus.Cancelled), + "Dispute status should be cancelled" + ); + + if (isDisputeInConflict) { + relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq( + uint8(relatedDispute.status), + uint8(IDisputeManager.DisputeStatus.Cancelled), + "Related dispute status should be cancelled" + ); + } + } + + /* + * HELPERS + */ + + function _createAttestationReceipt( + bytes32 requestCID, + bytes32 responseCID, + bytes32 subgraphDeploymentId + ) internal pure returns (Attestation.Receipt memory receipt) { + return + Attestation.Receipt({ + requestCID: requestCID, + responseCID: responseCID, + subgraphDeploymentId: subgraphDeploymentId + }); + } + + function _createConflictingAttestations( + bytes32 requestCID, + bytes32 subgraphDeploymentId, + bytes32 responseCID1, + bytes32 responseCID2, + uint256 signer1, + uint256 signer2 + ) internal view returns (bytes memory attestationData1, bytes memory attestationData2) { + Attestation.Receipt memory receipt1 = _createAttestationReceipt(requestCID, responseCID1, subgraphDeploymentId); + Attestation.Receipt memory receipt2 = _createAttestationReceipt(requestCID, responseCID2, subgraphDeploymentId); + + bytes memory _attestationData1 = _createAtestationData(receipt1, signer1); + bytes memory _attestationData2 = _createAtestationData(receipt2, signer2); + return (_attestationData1, _attestationData2); + } + + function _createAtestationData( + Attestation.Receipt memory receipt, + uint256 signer + ) internal view returns (bytes memory attestationData) { + bytes32 digest = disputeManager.encodeReceipt(receipt); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signer, digest); + + return abi.encodePacked(receipt.requestCID, receipt.responseCID, receipt.subgraphDeploymentId, r, s, v); + } + + /* + * PRIVATE FUNCTIONS + */ + + function _getDispute(bytes32 _disputeId) internal view returns (IDisputeManager.Dispute memory) { + ( + address indexer, + address fisherman, + uint256 deposit, + bytes32 relatedDisputeId, + IDisputeManager.DisputeType disputeType, + IDisputeManager.DisputeStatus status, + uint256 createdAt, + uint256 stakeSnapshot + ) = disputeManager.disputes(_disputeId); + return + IDisputeManager.Dispute({ + indexer: indexer, + fisherman: fisherman, + deposit: deposit, + relatedDisputeId: relatedDisputeId, + disputeType: disputeType, + status: status, + createdAt: createdAt, + stakeSnapshot: stakeSnapshot + }); + } + + function _setStorage_SubgraphService(address _subgraphService) internal { + vm.store(address(disputeManager), bytes32(uint256(51)), bytes32(uint256(uint160(_subgraphService)))); + } +} diff --git a/packages/subgraph-service/test/disputeManager/constructor/constructor.t.sol b/packages/subgraph-service/test/disputeManager/constructor/constructor.t.sol new file mode 100644 index 000000000..ffdfc3179 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/constructor/constructor.t.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol"; +import { UnsafeUpgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import { DisputeManager } from "../../../contracts/DisputeManager.sol"; +import { DisputeManagerTest } from "../DisputeManager.t.sol"; +import { IDisputeManager } from "../../../contracts/interfaces/IDisputeManager.sol"; + +contract DisputeManagerConstructorTest is DisputeManagerTest { + using PPMMath for uint256; + + /* + * MODIFIERS + */ + + modifier useDeployer() { + vm.startPrank(users.deployer); + _; + vm.stopPrank(); + } + + /* + * HELPERS + */ + + function _initializeDisputeManager( + address implementation, + address arbitrator, + uint64 disputePeriod, + uint256 disputeDeposit, + uint32 fishermanRewardPercentage, + uint32 maxSlashingPercentage + ) private returns (address) { + return + UnsafeUpgrades.deployTransparentProxy( + implementation, + users.governor, + abi.encodeCall( + DisputeManager.initialize, + ( + users.deployer, + arbitrator, + disputePeriod, + disputeDeposit, + fishermanRewardPercentage, + maxSlashingPercentage + ) + ) + ); + } + + /* + * TESTS + */ + + function test_DisputeManager_Constructor( + uint32 fishermanRewardPercentage, + uint32 maxSlashingPercentage + ) public useDeployer { + vm.assume(fishermanRewardPercentage <= disputeManager.MAX_FISHERMAN_REWARD_CUT()); + vm.assume(maxSlashingPercentage <= PPMMath.MAX_PPM); + address disputeManagerImplementation = address(new DisputeManager(address(controller))); + address proxy = _initializeDisputeManager( + disputeManagerImplementation, + users.arbitrator, + disputePeriod, + disputeDeposit, + fishermanRewardPercentage, + maxSlashingPercentage + ); + + DisputeManager disputeManager = DisputeManager(proxy); + assertEq(disputeManager.arbitrator(), users.arbitrator); + assertEq(disputeManager.disputePeriod(), disputePeriod); + assertEq(disputeManager.disputeDeposit(), disputeDeposit); + assertEq(disputeManager.fishermanRewardCut(), fishermanRewardPercentage); + } + + function test_DisputeManager_Constructor_RevertIf_ControllerAddressIsZero() public useDeployer { + bytes memory expectedError = abi.encodeWithSelector( + GraphDirectory.GraphDirectoryInvalidZeroAddress.selector, + "Controller" + ); + vm.expectRevert(expectedError); + new DisputeManager(address(0)); + } + + function test_DisputeManager_Constructor_RevertIf_ArbitratorAddressIsZero() public useDeployer { + address disputeManagerImplementation = address(new DisputeManager(address(controller))); + bytes memory expectedError = abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidZeroAddress.selector); + vm.expectRevert(expectedError); + _initializeDisputeManager( + disputeManagerImplementation, + address(0), + disputePeriod, + disputeDeposit, + fishermanRewardPercentage, + maxSlashingPercentage + ); + } + + function test_DisputeManager_Constructor_RevertIf_InvalidDisputePeriod() public useDeployer { + address disputeManagerImplementation = address(new DisputeManager(address(controller))); + bytes memory expectedError = abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputePeriodZero.selector); + vm.expectRevert(expectedError); + _initializeDisputeManager( + disputeManagerImplementation, + users.arbitrator, + 0, + disputeDeposit, + fishermanRewardPercentage, + maxSlashingPercentage + ); + } + + function test_DisputeManager_Constructor_RevertIf_InvalidDisputeDeposit() public useDeployer { + address disputeManagerImplementation = address(new DisputeManager(address(controller))); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidDisputeDeposit.selector, + 0 + ); + vm.expectRevert(expectedError); + _initializeDisputeManager( + disputeManagerImplementation, + users.arbitrator, + disputePeriod, + 0, + fishermanRewardPercentage, + maxSlashingPercentage + ); + } + + function test_DisputeManager_Constructor_RevertIf_InvalidFishermanRewardPercentage( + uint32 _fishermanRewardPercentage + ) public useDeployer { + vm.assume(_fishermanRewardPercentage > disputeManager.MAX_FISHERMAN_REWARD_CUT()); + address disputeManagerImplementation = address(new DisputeManager(address(controller))); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidFishermanReward.selector, + _fishermanRewardPercentage + ); + vm.expectRevert(expectedError); + _initializeDisputeManager( + disputeManagerImplementation, + users.arbitrator, + disputePeriod, + disputeDeposit, + _fishermanRewardPercentage, + maxSlashingPercentage + ); + } + + function test_DisputeManager_Constructor_RevertIf_InvalidMaxSlashingPercentage( + uint32 _maxSlashingPercentage + ) public useDeployer { + vm.assume(_maxSlashingPercentage > PPMMath.MAX_PPM); + address disputeManagerImplementation = address(new DisputeManager(address(controller))); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidMaxSlashingCut.selector, + _maxSlashingPercentage + ); + vm.expectRevert(expectedError); + _initializeDisputeManager( + disputeManagerImplementation, + users.arbitrator, + disputePeriod, + disputeDeposit, + fishermanRewardPercentage, + _maxSlashingPercentage + ); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/disputes.t.sol b/packages/subgraph-service/test/disputeManager/disputes/disputes.t.sol new file mode 100644 index 000000000..efcf49bc4 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/disputes.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../DisputeManager.t.sol"; + +contract DisputeManagerDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + /* + * TESTS + */ + + function test_Dispute_Accept_RevertIf_DisputeDoesNotExist( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + bytes32 disputeID = bytes32("0x0"); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidDispute.selector, disputeID)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Dispute_Accept_RevertIf_SlashZeroTokens(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI101")); + + // attempt to accept dispute with 0 tokens slashed + resetPrank(users.arbitrator); + uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(tokens); + vm.expectRevert( + abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidTokensSlash.selector, 0, maxTokensToSlash) + ); + disputeManager.acceptDispute(disputeID, 0); + } + + function test_Dispute_Reject_RevertIf_DisputeDoesNotExist(uint256 tokens) public useIndexer useAllocation(tokens) { + bytes32 disputeID = bytes32("0x0"); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidDispute.selector, disputeID)); + disputeManager.rejectDispute(disputeID); + } + + function test_Dispute_Draw_RevertIf_DisputeDoesNotExist(uint256 tokens) public useIndexer useAllocation(tokens) { + bytes32 disputeID = bytes32("0x0"); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidDispute.selector, disputeID)); + disputeManager.drawDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol new file mode 100644 index 000000000..901508bda --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerIndexingAcceptDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + /* + * TESTS + */ + + function test_Indexing_Accept_Dispute(uint256 tokens, uint256 tokensSlash) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + resetPrank(users.arbitrator); + _acceptDispute(disputeID, tokensSlash); + } + + function test_Indexing_Accept_Dispute_RevertWhen_SubgraphServiceNotSet( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + resetPrank(users.arbitrator); + // clear subgraph service address from storage + _setStorage_SubgraphService(address(0)); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerSubgraphServiceNotSet.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Indexing_Accept_Dispute_OptParam( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + resetPrank(users.arbitrator); + _acceptDispute(disputeID, tokensSlash); + } + + function test_Indexing_Accept_RevertIf_CallerIsNotArbitrator( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + // attempt to accept dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Indexing_Accept_RevertWhen_SlashingOverMaxSlashPercentage( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + tokensSlash = bound(tokensSlash, uint256(maxSlashingPercentage).mulPPM(tokens) + 1, type(uint256).max); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI101")); + + // max slashing percentage is 50% + resetPrank(users.arbitrator); + uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidTokensSlash.selector, + tokensSlash, + maxTokensToSlash + ); + vm.expectRevert(expectedError); + disputeManager.acceptDispute(disputeID, tokensSlash); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/cancel.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/cancel.t.sol new file mode 100644 index 000000000..e2aaaa541 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/cancel.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerIndexingCancelDisputeTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Indexing_Cancel_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + _cancelDispute(disputeID); + } + + function test_Indexing_Cancel_RevertIf_CallerIsNotFisherman( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotFisherman.selector)); + disputeManager.cancelDispute(disputeID); + } + + function test_Indexing_Cancel_RevertIf_DisputePeriodNotOver( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputePeriodNotFinished.selector)); + disputeManager.cancelDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol new file mode 100644 index 000000000..3b1df5011 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Indexing_Create_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + _createIndexingDispute(allocationID, bytes32("POI1")); + } + + function test_Indexing_Create_Dispute_RevertWhen_SubgraphServiceNotSet( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + + // clear subgraph service address from storage + _setStorage_SubgraphService(address(0)); + + // // Approve the dispute deposit + token.approve(address(disputeManager), disputeDeposit); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerSubgraphServiceNotSet.selector)); + disputeManager.createIndexingDispute(allocationID, bytes32("POI2")); + } + + function test_Indexing_Create_MultipleDisputes() public { + uint256 tokens = 10000 ether; + uint8 numIndexers = 10; + uint256[] memory allocationIDPrivateKeys = new uint256[](numIndexers); + for (uint i = 0; i < numIndexers; i++) { + string memory indexerName = string(abi.encodePacked("Indexer ", i)); + address indexer = createUser(indexerName); + vm.assume(indexer != address(0)); + + resetPrank(indexer); + mint(indexer, tokens); + _createProvision(indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(indexer, abi.encode("url", "geoHash", address(0))); + uint256 allocationIDPrivateKey = uint256(keccak256(abi.encodePacked(i))); + bytes memory data = _createSubgraphAllocationData( + indexer, + subgraphDeployment, + allocationIDPrivateKey, + tokens + ); + _startService(indexer, data); + allocationIDPrivateKeys[i] = allocationIDPrivateKey; + } + + resetPrank(users.fisherman); + for (uint i = 0; i < allocationIDPrivateKeys.length; i++) { + _createIndexingDispute(vm.addr(allocationIDPrivateKeys[i]), bytes32("POI1")); + } + } + + function test_Indexing_Create_RevertWhen_DisputeAlreadyCreated( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + // Create another dispute with different fisherman + address otherFisherman = makeAddr("otherFisherman"); + resetPrank(otherFisherman); + mint(otherFisherman, disputeDeposit); + token.approve(address(disputeManager), disputeDeposit); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerDisputeAlreadyCreated.selector, + disputeID + ); + vm.expectRevert(expectedError); + disputeManager.createIndexingDispute(allocationID, bytes32("POI1")); + vm.stopPrank(); + } + + function test_Indexing_Create_RevertIf_DepositUnderMinimum(uint256 tokensDeposit) public useFisherman { + tokensDeposit = bound(tokensDeposit, 0, disputeDeposit - 1); + token.approve(address(disputeManager), tokensDeposit); + bytes memory expectedError = abi.encodeWithSignature( + "ERC20InsufficientAllowance(address,uint256,uint256)", + address(disputeManager), + tokensDeposit, + disputeDeposit + ); + vm.expectRevert(expectedError); + disputeManager.createIndexingDispute(allocationID, bytes32("POI3")); + vm.stopPrank(); + } + + function test_Indexing_Create_RevertIf_AllocationDoesNotExist(uint256 tokens) public useFisherman { + tokens = bound(tokens, disputeDeposit, 10_000_000_000 ether); + token.approve(address(disputeManager), tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerIndexerNotFound.selector, + allocationID + ); + vm.expectRevert(expectedError); + disputeManager.createIndexingDispute(allocationID, bytes32("POI4")); + vm.stopPrank(); + } + + function test_Indexing_Create_RevertIf_IndexerIsBelowStake(uint256 tokens) public useIndexer useAllocation(tokens) { + // Close allocation + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); + // Thaw, deprovision and unstake + address subgraphDataServiceAddress = address(subgraphService); + _thawDeprovisionAndUnstake(users.indexer, subgraphDataServiceAddress, tokens); + + // Attempt to create dispute + resetPrank(users.fisherman); + token.approve(address(disputeManager), tokens); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerZeroTokens.selector)); + disputeManager.createIndexingDispute(allocationID, bytes32("POI1")); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/draw.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/draw.t.sol new file mode 100644 index 000000000..0f7dc6699 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/draw.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerIndexingDrawDisputeTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Indexing_Draw_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI32")); + + resetPrank(users.arbitrator); + _drawDispute(disputeID); + } + + function test_Indexing_Draw_RevertIf_CallerIsNotArbitrator(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + // attempt to draw dispute as fisherman + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.drawDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/reject.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/reject.t.sol new file mode 100644 index 000000000..ed91c70c0 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/reject.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerIndexingRejectDisputeTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Indexing_Reject_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + resetPrank(users.arbitrator); + _rejectDispute(disputeID); + } + + function test_Indexing_Reject_RevertIf_CallerIsNotArbitrator( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + // attempt to accept dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.rejectDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol new file mode 100644 index 000000000..9cfbaae10 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryAcceptDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Accept_Dispute(uint256 tokens, uint256 tokensSlash) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + _acceptDispute(disputeID, tokensSlash); + } + + function test_Query_Accept_Dispute_RevertWhen_SubgraphServiceNotSet( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + // clear subgraph service address from storage + _setStorage_SubgraphService(address(0)); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerSubgraphServiceNotSet.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Query_Accept_Dispute_OptParam( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + _acceptDispute(disputeID, tokensSlash); + } + + function test_Query_Accept_RevertIf_CallerIsNotArbitrator( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + // attempt to accept dispute as fisherman + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Query_Accept_RevertWhen_SlashingOverMaxSlashPercentage( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, uint256(maxSlashingPercentage).mulPPM(tokens) + 1, type(uint256).max); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + // max slashing percentage is 50% + resetPrank(users.arbitrator); + uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidTokensSlash.selector, + tokensSlash, + maxTokensToSlash + ); + vm.expectRevert(expectedError); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Query_Accept_RevertWhen_UsingConflictAccept( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputeNotInConflict.selector, disputeID)); + disputeManager.acceptDisputeConflict(disputeID, tokensSlash, true, 0); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/cancel.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/cancel.t.sol new file mode 100644 index 000000000..471d1620a --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/cancel.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryCancelDisputeTest is DisputeManagerTest { + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Cancel_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + _cancelDispute(disputeID); + } + + function test_Query_Cancel_RevertIf_CallerIsNotFisherman(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotFisherman.selector)); + disputeManager.cancelDispute(disputeID); + } + + function test_Query_Cancel_RevertIf_DisputePeriodNotOver(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputePeriodNotFinished.selector)); + disputeManager.cancelDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol new file mode 100644 index 000000000..843318995 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryCreateDisputeTest is DisputeManagerTest { + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Create_Dispute_Only(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + _createQueryDispute(attestationData); + } + + function test_Query_Create_Dispute_RevertWhen_SubgraphServiceNotSet( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + + // clear subgraph service address from storage + _setStorage_SubgraphService(address(0)); + + // // Approve the dispute deposit + token.approve(address(disputeManager), disputeDeposit); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerSubgraphServiceNotSet.selector)); + disputeManager.createQueryDispute(attestationData); + } + + function test_Query_Create_MultipleDisputes_DifferentFisherman( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + _createQueryDispute(attestationData); + + // Create another dispute with different fisherman + address otherFisherman = makeAddr("otherFisherman"); + resetPrank(otherFisherman); + mint(otherFisherman, MAX_TOKENS); + Attestation.Receipt memory otherFishermanReceipt = _createAttestationReceipt( + requestCID, + responseCID, + subgraphDeploymentId + ); + bytes memory otherFishermanAttestationData = _createAtestationData( + otherFishermanReceipt, + allocationIDPrivateKey + ); + _createQueryDispute(otherFishermanAttestationData); + } + + function test_Query_Create_MultipleDisputes_DifferentIndexer( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Create first dispute for indexer + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + _createQueryDispute(attestationData); + + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + uint256 newAllocationIDKey = uint256(keccak256(abi.encodePacked("newAllocationID"))); + mint(newIndexer, tokens); + resetPrank(newIndexer); + _createProvision(newIndexer, tokens, maxSlashingPercentage, disputePeriod); + _register(newIndexer, abi.encode("url", "geoHash", 0x0)); + bytes memory data = _createSubgraphAllocationData(newIndexer, subgraphDeployment, newAllocationIDKey, tokens); + _startService(newIndexer, data); + + // Create another dispute with same receipt but different indexer + resetPrank(users.fisherman); + bytes memory attestationData2 = _createAtestationData(receipt, newAllocationIDKey); + _createQueryDispute(attestationData2); + } + + function test_Query_Create_RevertIf_Duplicate(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + Attestation.Receipt memory newReceipt = _createAttestationReceipt( + requestCID, + responseCID, + subgraphDeploymentId + ); + bytes memory newAttestationData = _createAtestationData(newReceipt, allocationIDPrivateKey); + token.approve(address(disputeManager), disputeDeposit); + vm.expectRevert( + abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputeAlreadyCreated.selector, disputeID) + ); + disputeManager.createQueryDispute(newAttestationData); + } + + function test_Query_Create_RevertIf_DepositUnderMinimum(uint256 tokensDispute) public useFisherman { + tokensDispute = bound(tokensDispute, 0, disputeDeposit - 1); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + + token.approve(address(disputeManager), tokensDispute); + bytes memory expectedError = abi.encodeWithSignature( + "ERC20InsufficientAllowance(address,uint256,uint256)", + address(disputeManager), + tokensDispute, + disputeDeposit + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDispute(attestationData); + } + + function test_Query_Create_RevertIf_AllocationDoesNotExist(uint256 tokens) public useFisherman { + tokens = bound(tokens, disputeDeposit, 10_000_000_000 ether); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + token.approve(address(disputeManager), tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerIndexerNotFound.selector, + allocationID + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDispute(attestationData); + vm.stopPrank(); + } + + function test_Query_Create_RevertIf_IndexerIsBelowStake(uint256 tokens) public useIndexer useAllocation(tokens) { + // Close allocation + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); + + // Thaw, deprovision and unstake + address subgraphDataServiceAddress = address(subgraphService); + _thawDeprovisionAndUnstake(users.indexer, subgraphDataServiceAddress, tokens); + + // Atempt to create dispute + resetPrank(users.fisherman); + token.approve(address(disputeManager), tokens); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerZeroTokens.selector)); + disputeManager.createQueryDispute(attestationData); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/draw.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/draw.t.sol new file mode 100644 index 000000000..59eaee991 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/draw.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryDrawDisputeTest is DisputeManagerTest { + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Draw_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + _drawDispute(disputeID); + } + + function test_Query_Draw_RevertIf_CallerIsNotArbitrator(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + // attempt to draw dispute as fisherman + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.drawDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/reject.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/reject.t.sol new file mode 100644 index 000000000..4752ed423 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/reject.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryRejectDisputeTest is DisputeManagerTest { + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Reject_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + _rejectDispute(disputeID); + } + + function test_Query_Reject_RevertIf_CallerIsNotArbitrator(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + // attempt to accept dispute as fisherman + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.rejectDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol new file mode 100644 index 000000000..f145277af --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryConflictAcceptDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 private responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + /* + * TESTS + */ + + function test_Query_Conflict_Accept_Dispute_Draw_Other( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + uint256 fishermanBalanceBefore = token.balanceOf(users.fisherman); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + _acceptDisputeConflict(disputeID1, tokensSlash, false, 0); + + uint256 fishermanRewardPercentage = disputeManager.fishermanRewardCut(); + uint256 fishermanReward = tokensSlash.mulPPM(fishermanRewardPercentage); + uint256 fishermanBalanceAfter = token.balanceOf(users.fisherman); + + assertEq(fishermanBalanceAfter, fishermanBalanceBefore + fishermanReward); + } + + function test_Query_Conflict_Accept_Dispute_Accept_Other( + uint256 tokens, + uint256 tokensSlash, + uint256 tokensSlashRelatedDispute + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + tokensSlashRelatedDispute = bound(tokensSlashRelatedDispute, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + uint256 fishermanBalanceBefore = token.balanceOf(users.fisherman); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + _acceptDisputeConflict(disputeID1, tokensSlash, true, tokensSlashRelatedDispute); + + uint256 fishermanRewardPercentage = disputeManager.fishermanRewardCut(); + uint256 fishermanRewardFirstDispute = tokensSlash.mulPPM(fishermanRewardPercentage); + uint256 fishermanRewardRelatedDispute = tokensSlashRelatedDispute.mulPPM(fishermanRewardPercentage); + uint256 fishermanReward = fishermanRewardFirstDispute + fishermanRewardRelatedDispute; + uint256 fishermanBalanceAfter = token.balanceOf(users.fisherman); + + assertEq(fishermanBalanceAfter, fishermanBalanceBefore + fishermanReward); + } + + function test_Query_Conflict_Accept_RevertIf_CallerIsNotArbitrator( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + // attempt to accept dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.acceptDisputeConflict(disputeID1, tokensSlash, false, 0); + } + + function test_Query_Conflict_Accept_RevertWhen_SlashingOverMaxSlashPercentage( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, uint256(maxSlashingPercentage).mulPPM(tokens) + 1, type(uint256).max); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + // max slashing percentage is 50% + resetPrank(users.arbitrator); + uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidTokensSlash.selector, + tokensSlash, + maxTokensToSlash + ); + vm.expectRevert(expectedError); + disputeManager.acceptDisputeConflict(disputeID1, tokensSlash, false, 0); + } + + function test_Query_Conflict_Accept_AcceptRelated_DifferentIndexer( + uint256 tokensFirstIndexer, + uint256 tokensSecondIndexer, + uint256 tokensSlash, + uint256 tokensSlashRelatedDispute + ) public useIndexer useAllocation(tokensFirstIndexer) { + tokensSecondIndexer = bound(tokensSecondIndexer, minimumProvisionTokens, 10_000_000_000 ether); + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokensFirstIndexer)); + + // Setup different indexer for related dispute + address differentIndexer = makeAddr("DifferentIndexer"); + mint(differentIndexer, tokensSecondIndexer); + uint256 differentIndexerAllocationIDPrivateKey = uint256(keccak256(abi.encodePacked(differentIndexer))); + resetPrank(differentIndexer); + _createProvision(differentIndexer, tokensSecondIndexer, maxSlashingPercentage, disputePeriod); + _register(differentIndexer, abi.encode("url", "geoHash", address(0))); + bytes memory data = _createSubgraphAllocationData( + differentIndexer, + subgraphDeployment, + differentIndexerAllocationIDPrivateKey, + tokensSecondIndexer + ); + _startService(differentIndexer, data); + tokensSlashRelatedDispute = bound( + tokensSlashRelatedDispute, + 1, + uint256(maxSlashingPercentage).mulPPM(tokensSecondIndexer) + ); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + differentIndexerAllocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + _acceptDisputeConflict(disputeID1, tokensSlash, true, tokensSlashRelatedDispute); + } + + function test_Query_Conflict_Accept_RevertWhen_UsingSingleAccept( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputeInConflict.selector, disputeID1)); + disputeManager.acceptDispute(disputeID1, tokensSlash); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/cancel.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/cancel.t.sol new file mode 100644 index 000000000..a368fc443 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/cancel.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryConflictCancelDisputeTest is DisputeManagerTest { + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 private responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + /* + * TESTS + */ + + function test_Query_Conflict_Cancel_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + _cancelDispute(disputeID1); + } + + function test_Query_Conflict_Cancel_RevertIf_CallerIsNotFisherman( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.indexer); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotFisherman.selector)); + disputeManager.cancelDispute(disputeID1); + } + + function test_Query_Conflict_Cancel_RevertIf_DisputePeriodNotOver( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputePeriodNotFinished.selector)); + disputeManager.cancelDispute(disputeID1); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/create.t.sol new file mode 100644 index 000000000..de08a7de4 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/create.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryConflictCreateDisputeTest is DisputeManagerTest { + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 private responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + /* + * TESTS + */ + + function test_Query_Conflict_Create_DisputeAttestation(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + _createQueryDisputeConflict(attestationData1, attestationData2); + } + + function test_Query_Conflict_Create_DisputeAttestationDifferentIndexers( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + uint256 newAllocationIDKey = uint256(keccak256(abi.encodePacked("newAllocationID"))); + mint(newIndexer, tokens); + resetPrank(newIndexer); + _createProvision(newIndexer, tokens, maxSlashingPercentage, disputePeriod); + _register(newIndexer, abi.encode("url", "geoHash", 0x0)); + bytes memory data = _createSubgraphAllocationData(newIndexer, subgraphDeployment, newAllocationIDKey, tokens); + _startService(newIndexer, data); + + // Create query conflict dispute + resetPrank(users.fisherman); + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + newAllocationIDKey + ); + + _createQueryDisputeConflict(attestationData1, attestationData2); + } + + function test_Query_Conflict_Create_RevertIf_AttestationsResponsesAreTheSame() public useFisherman { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID1, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerNonConflictingAttestations.selector, + requestCID, + responseCID1, + subgraphDeployment, + requestCID, + responseCID1, + subgraphDeployment + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); + } + + function test_Query_Conflict_Create_RevertIf_AttestationsHaveDifferentSubgraph() public useFisherman { + bytes32 subgraphDeploymentId2 = keccak256(abi.encodePacked("Subgraph Deployment ID 2")); + + Attestation.Receipt memory receipt1 = _createAttestationReceipt(requestCID, responseCID1, subgraphDeployment); + bytes memory attestationData1 = _createAtestationData(receipt1, allocationIDPrivateKey); + + Attestation.Receipt memory receipt2 = _createAttestationReceipt( + requestCID, + responseCID2, + subgraphDeploymentId2 + ); + bytes memory attestationData2 = _createAtestationData(receipt2, allocationIDPrivateKey); + + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerNonConflictingAttestations.selector, + requestCID, + responseCID1, + subgraphDeployment, + requestCID, + responseCID2, + subgraphDeploymentId2 + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/draw.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/draw.t.sol new file mode 100644 index 000000000..35875408b --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/draw.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryConflictDrawDisputeTest is DisputeManagerTest { + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 private responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + /* + * TESTS + */ + + function test_Query_Conflict_Draw_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + _drawDispute(disputeID1); + } + + function test_Query_Conflict_Draw_RevertIf_CallerIsNotArbitrator( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + // attempt to draw dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.drawDispute(disputeID1); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol new file mode 100644 index 000000000..3b56a05d8 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryConflictRejectDisputeTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Query_Conflict_Reject_Revert(uint256 tokens) public useIndexer useAllocation(tokens) { + bytes32 requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputeInConflict.selector, disputeID1)); + disputeManager.rejectDispute(disputeID1); + } +} diff --git a/packages/subgraph-service/test/disputeManager/governance/arbitrator.t.sol b/packages/subgraph-service/test/disputeManager/governance/arbitrator.t.sol new file mode 100644 index 000000000..34b1f1322 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/arbitrator.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../DisputeManager.t.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract DisputeManagerGovernanceArbitratorTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Governance_SetArbitrator() public useGovernor { + address arbitrator = makeAddr("newArbitrator"); + _setArbitrator(arbitrator); + } + + function test_Governance_RevertWhen_ZeroAddress() public useGovernor { + address arbitrator = address(0); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidZeroAddress.selector)); + disputeManager.setArbitrator(arbitrator); + } + + function test_Governance_RevertWhen_NotGovernor() public useFisherman { + address arbitrator = makeAddr("newArbitrator"); + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.fisherman) + ); + disputeManager.setArbitrator(arbitrator); + } +} diff --git a/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol b/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol new file mode 100644 index 000000000..c7221e690 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../DisputeManager.t.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract DisputeManagerGovernanceDisputeDepositTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Governance_SetDisputeDeposit(uint256 disputeDeposit) public useGovernor { + vm.assume(disputeDeposit >= MIN_DISPUTE_DEPOSIT); + _setDisputeDeposit(disputeDeposit); + } + + function test_Governance_RevertWhen_DepositTooLow(uint256 disputeDeposit) public useGovernor { + vm.assume(disputeDeposit < MIN_DISPUTE_DEPOSIT); + vm.expectRevert( + abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidDisputeDeposit.selector, disputeDeposit) + ); + disputeManager.setDisputeDeposit(disputeDeposit); + } + + function test_Governance_RevertWhen_NotGovernor() public useFisherman { + uint256 disputeDeposit = 100 ether; + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.fisherman) + ); + disputeManager.setDisputeDeposit(disputeDeposit); + } +} diff --git a/packages/subgraph-service/test/disputeManager/governance/fishermanRewardCut.t.sol b/packages/subgraph-service/test/disputeManager/governance/fishermanRewardCut.t.sol new file mode 100644 index 000000000..b274ff6df --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/fishermanRewardCut.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../DisputeManager.t.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract DisputeManagerGovernanceFishermanRewardCutTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Governance_SetFishermanRewardCut(uint32 fishermanRewardCut) public useGovernor { + vm.assume(fishermanRewardCut <= disputeManager.MAX_FISHERMAN_REWARD_CUT()); + _setFishermanRewardCut(fishermanRewardCut); + } + + function test_Governance_RevertWhen_OverMaximumValue(uint32 fishermanRewardCut) public useGovernor { + vm.assume(fishermanRewardCut > disputeManager.MAX_FISHERMAN_REWARD_CUT()); + vm.expectRevert( + abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidFishermanReward.selector, fishermanRewardCut) + ); + disputeManager.setFishermanRewardCut(fishermanRewardCut); + } + + function test_Governance_RevertWhen_NotGovernor() public useFisherman { + uint32 fishermanRewardCut = 1000; + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.fisherman) + ); + disputeManager.setFishermanRewardCut(fishermanRewardCut); + } +} diff --git a/packages/subgraph-service/test/disputeManager/governance/maxSlashingCut.t.sol b/packages/subgraph-service/test/disputeManager/governance/maxSlashingCut.t.sol new file mode 100644 index 000000000..5f0820241 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/maxSlashingCut.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../DisputeManager.t.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract DisputeManagerGovernanceMaxSlashingCutTest is DisputeManagerTest { + /* + * TESTS + */ + + function test_Governance_SetMaxSlashingCut(uint32 maxSlashingCut) public useGovernor { + vm.assume(maxSlashingCut <= MAX_PPM); + _setMaxSlashingCut(maxSlashingCut); + } + + function test_Governance_RevertWhen_NotPPM(uint32 maxSlashingCut) public useGovernor { + vm.assume(maxSlashingCut > MAX_PPM); + vm.expectRevert( + abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidMaxSlashingCut.selector, maxSlashingCut) + ); + disputeManager.setMaxSlashingCut(maxSlashingCut); + } + + function test_Governance_RevertWhen_NotGovernor() public useFisherman { + uint32 maxSlashingCut = 1000; + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.fisherman) + ); + disputeManager.setMaxSlashingCut(maxSlashingCut); + } +} diff --git a/packages/subgraph-service/test/disputeManager/governance/subgraphService.t.sol b/packages/subgraph-service/test/disputeManager/governance/subgraphService.t.sol new file mode 100644 index 000000000..2934af577 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/subgraphService.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../DisputeManager.t.sol"; + +contract DisputeManagerGovernanceSubgraphService is DisputeManagerTest { + /* + * TESTS + */ + + function test_Governance_SetSubgraphService(address subgraphService) public useGovernor { + vm.assume(subgraphService != address(0)); + _setSubgraphService(subgraphService); + } + + function test_Governance_SetSubgraphService_RevertWhenZero() public useGovernor { + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidZeroAddress.selector)); + disputeManager.setSubgraphService(address(0)); + } +} diff --git a/packages/subgraph-service/test/mocks/MockCuration.sol b/packages/subgraph-service/test/mocks/MockCuration.sol new file mode 100644 index 000000000..0fb417c6d --- /dev/null +++ b/packages/subgraph-service/test/mocks/MockCuration.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +contract MockCuration { + function isCurated(bytes32) public pure returns (bool) { + return true; + } + + function collect(bytes32, uint256) external {} +} diff --git a/packages/subgraph-service/test/mocks/MockEpochManager.sol b/packages/subgraph-service/test/mocks/MockEpochManager.sol new file mode 100644 index 000000000..060a92e21 --- /dev/null +++ b/packages/subgraph-service/test/mocks/MockEpochManager.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.27; + +import { IEpochManager } from "@graphprotocol/contracts/contracts/epochs/IEpochManager.sol"; + +contract MockEpochManager is IEpochManager { + // -- Variables -- + + uint256 public epochLength; + uint256 public lastRunEpoch; + uint256 public lastLengthUpdateEpoch; + uint256 public lastLengthUpdateBlock; + + // -- Configuration -- + + function setEpochLength(uint256 _epochLength) public { + lastLengthUpdateEpoch = 1; + lastLengthUpdateBlock = blockNum(); + epochLength = _epochLength; + } + + // -- Epochs + + function runEpoch() public { + lastRunEpoch = currentEpoch(); + } + + // -- Getters -- + + function isCurrentEpochRun() public view returns (bool) { + return lastRunEpoch == currentEpoch(); + } + + function blockNum() public view returns (uint256) { + return block.number; + } + + function blockHash(uint256 _block) public view returns (bytes32) { + return blockhash(_block); + } + + function currentEpoch() public view returns (uint256) { + return lastLengthUpdateEpoch + epochsSinceUpdate(); + } + + function currentEpochBlock() public view returns (uint256) { + return lastLengthUpdateBlock + (epochsSinceUpdate() * epochLength); + } + + function currentEpochBlockSinceStart() public view returns (uint256) { + return blockNum() - currentEpochBlock(); + } + + function epochsSince(uint256 _epoch) public view returns (uint256) { + uint256 epoch = currentEpoch(); + return _epoch < epoch ? (epoch - _epoch) : 0; + } + + function epochsSinceUpdate() public view returns (uint256) { + return (blockNum() - lastLengthUpdateBlock) / epochLength; + } +} diff --git a/packages/subgraph-service/test/mocks/MockGRTToken.sol b/packages/subgraph-service/test/mocks/MockGRTToken.sol new file mode 100644 index 000000000..cce7f717c --- /dev/null +++ b/packages/subgraph-service/test/mocks/MockGRTToken.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; + +contract MockGRTToken is ERC20, IGraphToken { + constructor() ERC20("Graph Token", "GRT") {} + + function burn(uint256 amount) external { + _burn(msg.sender, amount); + } + + function burnFrom(address _from, uint256 amount) external { + _burn(_from, amount); + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + + // -- Mint Admin -- + + function addMinter(address _account) external {} + + function removeMinter(address _account) external {} + + function renounceMinter() external {} + + function isMinter(address _account) external view returns (bool) {} + + // -- Permit -- + + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external {} + + // -- Allowance -- + + function increaseAllowance(address spender, uint256 addedValue) external returns (bool) {} + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) {} +} diff --git a/packages/subgraph-service/test/mocks/MockRewardsManager.sol b/packages/subgraph-service/test/mocks/MockRewardsManager.sol new file mode 100644 index 000000000..dd94d6a2c --- /dev/null +++ b/packages/subgraph-service/test/mocks/MockRewardsManager.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IRewardsManager } from "@graphprotocol/contracts/contracts/rewards/IRewardsManager.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; + +import { MockGRTToken } from "./MockGRTToken.sol"; + +interface IRewardsIssuer { + function getAllocationData( + address allocationId + ) + external + view + returns (address indexer, bytes32 subgraphDeploymentId, uint256 tokens, uint256 accRewardsPerAllocatedToken); +} + +contract MockRewardsManager is IRewardsManager { + using PPMMath for uint256; + + MockGRTToken public token; + uint256 public rewardsPerSignal; + uint256 public rewardsPerSubgraphAllocationUpdate; + mapping(bytes32 => bool) public subgraphs; + + uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; + + constructor(MockGRTToken _token, uint256 _rewardsPerSignal, uint256 _rewardsPerSubgraphAllocationUpdate) { + token = _token; + rewardsPerSignal = _rewardsPerSignal; + rewardsPerSubgraphAllocationUpdate = _rewardsPerSubgraphAllocationUpdate; + } + + // -- Config -- + + function setIssuancePerBlock(uint256) external {} + + function setMinimumSubgraphSignal(uint256) external {} + + function setSubgraphService(address) external {} + + // -- Denylist -- + + function setSubgraphAvailabilityOracle(address) external {} + + function setDenied(bytes32, bool) external {} + + function setDeniedMany(bytes32[] calldata, bool[] calldata) external {} + + function isDenied(bytes32) external view returns (bool) {} + + // -- Getters -- + + function getNewRewardsPerSignal() external view returns (uint256) {} + + function getAccRewardsPerSignal() external view returns (uint256) {} + + function getAccRewardsForSubgraph(bytes32) external view returns (uint256) {} + + function getAccRewardsPerAllocatedToken(bytes32) external view returns (uint256, uint256) {} + + function getRewards(address, address) external view returns (uint256) {} + + function calcRewards(uint256, uint256) external pure returns (uint256) {} + + // -- Updates -- + + function updateAccRewardsPerSignal() external returns (uint256) {} + + function takeRewards(address _allocationID) external returns (uint256) { + address rewardsIssuer = msg.sender; + (, , uint256 tokens, uint256 accRewardsPerAllocatedToken) = IRewardsIssuer(rewardsIssuer).getAllocationData( + _allocationID + ); + + uint256 accRewardsPerTokens = tokens.mulPPM(rewardsPerSignal); + uint256 rewards = accRewardsPerTokens - accRewardsPerAllocatedToken; + token.mint(rewardsIssuer, rewards); + return rewards; + } + + // -- Hooks -- + + function onSubgraphSignalUpdate(bytes32) external pure returns (uint256) {} + + function onSubgraphAllocationUpdate(bytes32 _subgraphDeploymentID) external returns (uint256) { + if (subgraphs[_subgraphDeploymentID]) { + return rewardsPerSubgraphAllocationUpdate; + } + + subgraphs[_subgraphDeploymentID] = true; + return 0; + } +} diff --git a/packages/subgraph-service/test/shared/HorizonStakingShared.t.sol b/packages/subgraph-service/test/shared/HorizonStakingShared.t.sol new file mode 100644 index 000000000..66915d043 --- /dev/null +++ b/packages/subgraph-service/test/shared/HorizonStakingShared.t.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { IHorizonStakingExtension } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingExtension.sol"; + +import { SubgraphBaseTest } from "../SubgraphBaseTest.t.sol"; + +abstract contract HorizonStakingSharedTest is SubgraphBaseTest { + /* + * HELPERS + */ + + function _createProvision( + address _indexer, + uint256 _tokens, + uint32 _maxSlashingPercentage, + uint64 _disputePeriod + ) internal { + _stakeTo(_indexer, _tokens); + staking.provision(_indexer, address(subgraphService), _tokens, _maxSlashingPercentage, _disputePeriod); + } + + function _addToProvision(address _indexer, uint256 _tokens) internal { + _stakeTo(_indexer, _tokens); + staking.addToProvision(_indexer, address(subgraphService), _tokens); + } + + function _delegate(address _indexer, address _verifier, uint256 _tokens, uint256 _minSharesOut) internal { + staking.delegate(_indexer, _verifier, _tokens, _minSharesOut); + } + + function _undelegate(address _indexer, address _verifier, uint256 _shares) internal { + staking.undelegate(_indexer, _verifier, _shares); + } + + function _setDelegationFeeCut( + address _indexer, + address _verifier, + IGraphPayments.PaymentTypes _paymentType, + uint256 _cut + ) internal { + staking.setDelegationFeeCut(_indexer, _verifier, _paymentType, _cut); + } + + function _thawDeprovisionAndUnstake(address _indexer, address _verifier, uint256 _tokens) internal { + // Initiate thaw request + staking.thaw(_indexer, _verifier, _tokens); + + // Skip thawing period + IHorizonStakingTypes.Provision memory provision = staking.getProvision(_indexer, _verifier); + skip(provision.thawingPeriod + 1); + + // Deprovision and unstake + staking.deprovision(_indexer, _verifier, 0); + staking.unstake(_tokens); + } + + function _setProvisionParameters( + address _indexer, + address _verifier, + uint32 _maxVerifierCut, + uint64 _thawingPeriod + ) internal { + staking.setProvisionParameters(_indexer, _verifier, _maxVerifierCut, _thawingPeriod); + } + + function _setStorage_allocation_hardcoded(address indexer, address allocationId, uint256 tokens) internal { + IHorizonStakingExtension.Allocation memory allocation = IHorizonStakingExtension.Allocation({ + indexer: indexer, + subgraphDeploymentID: bytes32("0x12344321"), + tokens: tokens, + createdAtEpoch: 1234, + closedAtEpoch: 1235, + collectedFees: 1234, + __DEPRECATED_effectiveAllocation: 1222234, + accRewardsPerAllocatedToken: 1233334, + distributedRebates: 1244434 + }); + + // __DEPRECATED_allocations + uint256 allocationsSlot = 15; + bytes32 allocationBaseSlot = keccak256(abi.encode(allocationId, allocationsSlot)); + vm.store(address(staking), allocationBaseSlot, bytes32(uint256(uint160(allocation.indexer)))); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 1), allocation.subgraphDeploymentID); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 2), bytes32(tokens)); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 3), bytes32(allocation.createdAtEpoch)); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 4), bytes32(allocation.closedAtEpoch)); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 5), bytes32(allocation.collectedFees)); + vm.store( + address(staking), + bytes32(uint256(allocationBaseSlot) + 6), + bytes32(allocation.__DEPRECATED_effectiveAllocation) + ); + vm.store( + address(staking), + bytes32(uint256(allocationBaseSlot) + 7), + bytes32(allocation.accRewardsPerAllocatedToken) + ); + vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 8), bytes32(allocation.distributedRebates)); + + // _serviceProviders + uint256 serviceProviderSlot = 14; + bytes32 serviceProviderBaseSlot = keccak256(abi.encode(allocation.indexer, serviceProviderSlot)); + uint256 currentTokensStaked = uint256(vm.load(address(staking), serviceProviderBaseSlot)); + uint256 currentTokensProvisioned = uint256( + vm.load(address(staking), bytes32(uint256(serviceProviderBaseSlot) + 1)) + ); + vm.store( + address(staking), + bytes32(uint256(serviceProviderBaseSlot) + 0), + bytes32(currentTokensStaked + tokens) + ); + vm.store( + address(staking), + bytes32(uint256(serviceProviderBaseSlot) + 1), + bytes32(currentTokensProvisioned + tokens) + ); + + // __DEPRECATED_subgraphAllocations + uint256 subgraphsAllocationsSlot = 16; + bytes32 subgraphAllocationsBaseSlot = keccak256( + abi.encode(allocation.subgraphDeploymentID, subgraphsAllocationsSlot) + ); + uint256 currentAllocatedTokens = uint256(vm.load(address(staking), subgraphAllocationsBaseSlot)); + vm.store(address(staking), subgraphAllocationsBaseSlot, bytes32(currentAllocatedTokens + tokens)); + } + + /* + * PRIVATE + */ + + function _stakeTo(address _indexer, uint256 _tokens) private { + token.approve(address(staking), _tokens); + staking.stakeTo(_indexer, _tokens); + } +} diff --git a/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol b/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol new file mode 100644 index 000000000..8a51d3443 --- /dev/null +++ b/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { Allocation } from "../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../contracts/utilities/AllocationManager.sol"; +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ISubgraphService } from "../../contracts/interfaces/ISubgraphService.sol"; + +import { HorizonStakingSharedTest } from "./HorizonStakingShared.t.sol"; + +abstract contract SubgraphServiceSharedTest is HorizonStakingSharedTest { + using Allocation for Allocation.State; + + /* + * VARIABLES + */ + + uint256 allocationIDPrivateKey; + address allocationID; + bytes32 subgraphDeployment; + + /* + * MODIFIERS + */ + + modifier useIndexer() { + vm.startPrank(users.indexer); + _; + vm.stopPrank(); + } + + modifier useAllocation(uint256 tokens) { + vm.assume(tokens >= minimumProvisionTokens); + vm.assume(tokens < 10_000_000_000 ether); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + bytes memory data = _createSubgraphAllocationData( + users.indexer, + subgraphDeployment, + allocationIDPrivateKey, + tokens + ); + _startService(users.indexer, data); + _; + } + + modifier useDelegation(uint256 tokens) { + vm.assume(tokens > MIN_DELEGATION); + vm.assume(tokens < 10_000_000_000 ether); + (, address msgSender, ) = vm.readCallers(); + resetPrank(users.delegator); + token.approve(address(staking), tokens); + _delegate(users.indexer, address(subgraphService), tokens, 0); + resetPrank(msgSender); + _; + } + + /* + * SET UP + */ + + function setUp() public virtual override { + super.setUp(); + (allocationID, allocationIDPrivateKey) = makeAddrAndKey("allocationId"); + subgraphDeployment = keccak256(abi.encodePacked("Subgraph Deployment ID")); + } + + /* + * ACTIONS + */ + + function _register(address _indexer, bytes memory _data) internal { + (string memory url, string memory geohash, address rewardsDestination) = abi.decode( + _data, + (string, string, address) + ); + + vm.expectEmit(address(subgraphService)); + emit IDataService.ServiceProviderRegistered(_indexer, _data); + + // Register indexer + subgraphService.register(_indexer, _data); + + // Check registered indexer data + ISubgraphService.Indexer memory indexer = _getIndexer(_indexer); + assertEq(indexer.registeredAt, block.timestamp); + assertEq(indexer.url, url); + assertEq(indexer.geoHash, geohash); + + // Check rewards destination + assertEq(subgraphService.rewardsDestination(_indexer), rewardsDestination); + } + + function _startService(address _indexer, bytes memory _data) internal { + (bytes32 subgraphDeploymentId, uint256 tokens, address allocationId, ) = abi.decode( + _data, + (bytes32, uint256, address, bytes) + ); + uint256 previousSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeploymentId); + uint256 currentEpoch = epochManager.currentEpoch(); + + vm.expectEmit(address(subgraphService)); + emit IDataService.ServiceStarted(_indexer, _data); + emit AllocationManager.AllocationCreated(_indexer, allocationId, subgraphDeploymentId, tokens, currentEpoch); + + // TODO: improve this + uint256 accRewardsPerAllocatedToken = 0; + if (rewardsManager.subgraphs(subgraphDeploymentId)) { + accRewardsPerAllocatedToken = rewardsManager.rewardsPerSubgraphAllocationUpdate(); + } + + // Start service + subgraphService.startService(_indexer, _data); + + // Check allocation data + Allocation.State memory allocation = subgraphService.getAllocation(allocationId); + assertEq(allocation.tokens, tokens); + assertEq(allocation.indexer, _indexer); + assertEq(allocation.subgraphDeploymentId, subgraphDeploymentId); + assertEq(allocation.createdAt, block.timestamp); + assertEq(allocation.closedAt, 0); + assertEq(allocation.lastPOIPresentedAt, 0); + assertEq(allocation.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken); + assertEq(allocation.accRewardsPending, 0); + assertEq(allocation.createdAtEpoch, currentEpoch); + + // Check subgraph deployment allocated tokens + uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeploymentId); + assertEq(subgraphAllocatedTokens, previousSubgraphAllocatedTokens + tokens); + } + + function _stopService(address _indexer, bytes memory _data) internal { + address allocationId = abi.decode(_data, (address)); + + Allocation.State memory allocation = subgraphService.getAllocation(allocationId); + assertTrue(allocation.isOpen()); + uint256 previousSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens( + allocation.subgraphDeploymentId + ); + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.AllocationClosed( + _indexer, + allocationId, + allocation.subgraphDeploymentId, + allocation.tokens + ); + emit IDataService.ServiceStopped(_indexer, _data); + + // stop allocation + subgraphService.stopService(_indexer, _data); + + // update allocation + allocation = subgraphService.getAllocation(allocationId); + + // check allocation + assertEq(allocation.closedAt, block.timestamp); + + // check subgraph deployment allocated tokens + uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeployment); + assertEq(subgraphAllocatedTokens, previousSubgraphAllocatedTokens - allocation.tokens); + } + + /* + * HELPERS + */ + + function _createSubgraphAllocationData( + address _indexer, + bytes32 _subgraphDeployment, + uint256 _allocationIdPrivateKey, + uint256 _tokens + ) internal view returns (bytes memory) { + address allocationId = vm.addr(_allocationIdPrivateKey); + bytes32 digest = subgraphService.encodeAllocationProof(_indexer, allocationId); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_allocationIdPrivateKey, digest); + + return abi.encode(_subgraphDeployment, _tokens, allocationId, abi.encodePacked(r, s, v)); + } + + function _delegate(uint256 tokens) internal { + token.approve(address(staking), tokens); + staking.delegate(users.indexer, address(subgraphService), tokens, 0); + } + + /* + * PRIVATE FUNCTIONS + */ + + function _getIndexer(address _indexer) private view returns (ISubgraphService.Indexer memory) { + (uint256 registeredAt, string memory url, string memory geoHash) = subgraphService.indexers(_indexer); + return ISubgraphService.Indexer({ registeredAt: registeredAt, url: url, geoHash: geoHash }); + } +} diff --git a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol new file mode 100644 index 000000000..2053083c8 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { IGraphTallyCollector } from "@graphprotocol/horizon/contracts/interfaces/IGraphTallyCollector.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { LinkedList } from "@graphprotocol/horizon/contracts/libraries/LinkedList.sol"; +import { IDataServiceFees } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataServiceFees.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; + +import { Allocation } from "../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../contracts/utilities/AllocationManager.sol"; +import { ISubgraphService } from "../../contracts/interfaces/ISubgraphService.sol"; +import { LegacyAllocation } from "../../contracts/libraries/LegacyAllocation.sol"; +import { SubgraphServiceSharedTest } from "../shared/SubgraphServiceShared.t.sol"; + +contract SubgraphServiceTest is SubgraphServiceSharedTest { + using PPMMath for uint256; + using Allocation for Allocation.State; + using LinkedList for LinkedList.List; + + /* + * MODIFIERS + */ + + modifier useGovernor() { + vm.startPrank(users.governor); + _; + vm.stopPrank(); + } + + modifier useOperator() { + resetPrank(users.indexer); + staking.setOperator(address(subgraphService), users.operator, true); + resetPrank(users.operator); + _; + vm.stopPrank(); + } + + modifier useRewardsDestination() { + _setRewardsDestination(users.rewardsDestination); + _; + } + + /* + * SET UP + */ + + function setUp() public virtual override { + super.setUp(); + } + + /* + * ACTIONS + */ + + function _setRewardsDestination(address _rewardsDestination) internal { + (, address indexer, ) = vm.readCallers(); + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.RewardsDestinationSet(indexer, _rewardsDestination); + + // Set rewards destination + subgraphService.setRewardsDestination(_rewardsDestination); + + // Check rewards destination + assertEq(subgraphService.rewardsDestination(indexer), _rewardsDestination); + } + + function _acceptProvision(address _indexer, bytes memory _data) internal { + IHorizonStakingTypes.Provision memory provision = staking.getProvision(_indexer, address(subgraphService)); + uint32 maxVerifierCut = provision.maxVerifierCut; + uint64 thawingPeriod = provision.thawingPeriod; + uint32 maxVerifierCutPending = provision.maxVerifierCutPending; + uint64 thawingPeriodPending = provision.thawingPeriodPending; + + vm.expectEmit(address(subgraphService)); + emit IDataService.ProvisionPendingParametersAccepted(_indexer); + + // Accept provision + subgraphService.acceptProvisionPendingParameters(_indexer, _data); + + // Update provision after acceptance + provision = staking.getProvision(_indexer, address(subgraphService)); + + // Check that max verifier cut updated to pending value if needed + if (maxVerifierCut != maxVerifierCutPending) { + assertEq(provision.maxVerifierCut, maxVerifierCutPending); + } + + // Check that thawing period updated to pending value if needed + if (thawingPeriod != thawingPeriodPending) { + assertEq(provision.thawingPeriod, thawingPeriodPending); + } + } + + function _resizeAllocation(address _indexer, address _allocationId, uint256 _tokens) internal { + // before state + Allocation.State memory beforeAllocation = subgraphService.getAllocation(_allocationId); + bytes32 subgraphDeploymentId = beforeAllocation.subgraphDeploymentId; + uint256 beforeSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeploymentId); + uint256 beforeAllocatedTokens = subgraphService.allocationProvisionTracker(_indexer); + + uint256 allocatedTokensDelta; + if (_tokens > beforeAllocation.tokens) { + allocatedTokensDelta = _tokens - beforeAllocation.tokens; + } else { + allocatedTokensDelta = beforeAllocation.tokens - _tokens; + } + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.AllocationResized( + _indexer, + _allocationId, + subgraphDeploymentId, + _tokens, + beforeSubgraphAllocatedTokens + ); + + // resize allocation + subgraphService.resizeAllocation(_indexer, _allocationId, _tokens); + + // after state + uint256 afterSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeploymentId); + uint256 afterAllocatedTokens = subgraphService.allocationProvisionTracker(_indexer); + Allocation.State memory afterAllocation = subgraphService.getAllocation(_allocationId); + uint256 accRewardsPerAllocatedTokenDelta = afterAllocation.accRewardsPerAllocatedToken - + beforeAllocation.accRewardsPerAllocatedToken; + uint256 afterAccRewardsPending = rewardsManager.calcRewards( + beforeAllocation.tokens, + accRewardsPerAllocatedTokenDelta + ); + + // check state + if (_tokens > beforeAllocation.tokens) { + assertEq(afterAllocatedTokens, beforeAllocatedTokens + allocatedTokensDelta); + } else { + assertEq(afterAllocatedTokens, beforeAllocatedTokens - allocatedTokensDelta); + } + assertEq(afterAllocation.tokens, _tokens); + assertEq(afterAllocation.accRewardsPerAllocatedToken, rewardsPerSubgraphAllocationUpdate); + assertEq(afterAllocation.accRewardsPending, afterAccRewardsPending); + assertEq(afterSubgraphAllocatedTokens, _tokens); + } + + function _closeStaleAllocation(address _allocationId) internal { + Allocation.State memory allocation = subgraphService.getAllocation(_allocationId); + assertTrue(allocation.isOpen()); + uint256 previousSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens( + allocation.subgraphDeploymentId + ); + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.AllocationClosed( + allocation.indexer, + _allocationId, + allocation.subgraphDeploymentId, + allocation.tokens + ); + + // close stale allocation + subgraphService.closeStaleAllocation(_allocationId); + + // update allocation + allocation = subgraphService.getAllocation(_allocationId); + + // check allocation + assertEq(allocation.closedAt, block.timestamp); + + // check subgraph deployment allocated tokens + uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeployment); + assertEq(subgraphAllocatedTokens, previousSubgraphAllocatedTokens - allocation.tokens); + } + + struct IndexingRewardsData { + bytes32 poi; + uint256 tokensIndexerRewards; + uint256 tokensDelegationRewards; + } + + struct QueryFeeData { + uint256 curationCut; + uint256 protocolPaymentCut; + } + + struct CollectPaymentData { + uint256 rewardsDestinationBalance; + uint256 indexerProvisionBalance; + uint256 delegationPoolBalance; + uint256 indexerBalance; + uint256 curationBalance; + uint256 lockedTokens; + } + + function _collect(address _indexer, IGraphPayments.PaymentTypes _paymentType, bytes memory _data) internal { + // Reset storage variables + uint256 paymentCollected = 0; + address allocationId; + IndexingRewardsData memory indexingRewardsData; + CollectPaymentData memory collectPaymentDataBefore = _collectPaymentDataBefore(_indexer); + + if (_paymentType == IGraphPayments.PaymentTypes.QueryFee) { + paymentCollected = _handleQueryFeeCollection(_indexer, _data); + } else if (_paymentType == IGraphPayments.PaymentTypes.IndexingRewards) { + (paymentCollected, allocationId, indexingRewardsData) = _handleIndexingRewardsCollection(_data); + } + + vm.expectEmit(address(subgraphService)); + emit IDataService.ServicePaymentCollected(_indexer, _paymentType, paymentCollected); + + // collect rewards + subgraphService.collect(_indexer, _paymentType, _data); + + CollectPaymentData memory collectPaymentDataAfter = _collectPaymentDataAfter(_indexer); + + if (_paymentType == IGraphPayments.PaymentTypes.QueryFee) { + _verifyQueryFeeCollection( + _indexer, + paymentCollected, + _data, + collectPaymentDataBefore, + collectPaymentDataAfter + ); + } else if (_paymentType == IGraphPayments.PaymentTypes.IndexingRewards) { + _verifyIndexingRewardsCollection( + _indexer, + allocationId, + indexingRewardsData, + collectPaymentDataBefore, + collectPaymentDataAfter + ); + } + } + + function _collectPaymentDataBefore(address _indexer) private view returns (CollectPaymentData memory) { + address rewardsDestination = subgraphService.rewardsDestination(_indexer); + CollectPaymentData memory collectPaymentDataBefore; + collectPaymentDataBefore.rewardsDestinationBalance = token.balanceOf(rewardsDestination); + collectPaymentDataBefore.indexerProvisionBalance = staking.getProviderTokensAvailable( + _indexer, + address(subgraphService) + ); + collectPaymentDataBefore.delegationPoolBalance = staking.getDelegatedTokensAvailable( + _indexer, + address(subgraphService) + ); + collectPaymentDataBefore.indexerBalance = token.balanceOf(_indexer); + collectPaymentDataBefore.curationBalance = token.balanceOf(address(curation)); + collectPaymentDataBefore.lockedTokens = subgraphService.feesProvisionTracker(_indexer); + return collectPaymentDataBefore; + } + + function _collectPaymentDataAfter(address _indexer) private view returns (CollectPaymentData memory) { + CollectPaymentData memory collectPaymentDataAfter; + address rewardsDestination = subgraphService.rewardsDestination(_indexer); + collectPaymentDataAfter.rewardsDestinationBalance = token.balanceOf(rewardsDestination); + collectPaymentDataAfter.indexerProvisionBalance = staking.getProviderTokensAvailable( + _indexer, + address(subgraphService) + ); + collectPaymentDataAfter.delegationPoolBalance = staking.getDelegatedTokensAvailable( + _indexer, + address(subgraphService) + ); + collectPaymentDataAfter.indexerBalance = token.balanceOf(_indexer); + collectPaymentDataAfter.curationBalance = token.balanceOf(address(curation)); + collectPaymentDataAfter.lockedTokens = subgraphService.feesProvisionTracker(_indexer); + return collectPaymentDataAfter; + } + + function _handleQueryFeeCollection( + address _indexer, + bytes memory _data + ) private returns (uint256 paymentCollected) { + IGraphTallyCollector.SignedRAV memory signedRav = abi.decode(_data, (IGraphTallyCollector.SignedRAV)); + Allocation.State memory allocation = subgraphService.getAllocation( + address(uint160(uint256(signedRav.rav.collectionId))) + ); + address payer = graphTallyCollector.isAuthorized(signedRav.rav.payer, _recoverRAVSigner(signedRav)) + ? signedRav.rav.payer + : address(0); + + uint256 tokensCollected = graphTallyCollector.tokensCollected( + address(subgraphService), + signedRav.rav.collectionId, + _indexer, + payer + ); + paymentCollected = signedRav.rav.valueAggregate - tokensCollected; + + QueryFeeData memory queryFeeData = _queryFeeData(allocation.subgraphDeploymentId); + uint256 tokensProtocol = paymentCollected.mulPPMRoundUp(queryFeeData.protocolPaymentCut); + uint256 tokensCurators = (paymentCollected - tokensProtocol).mulPPMRoundUp(queryFeeData.curationCut); + + vm.expectEmit(address(subgraphService)); + emit ISubgraphService.QueryFeesCollected(_indexer, payer, paymentCollected, tokensCurators); + + return paymentCollected; + } + + function _queryFeeData(bytes32 _subgraphDeploymentId) private view returns (QueryFeeData memory) { + QueryFeeData memory queryFeeData; + queryFeeData.protocolPaymentCut = graphPayments.PROTOCOL_PAYMENT_CUT(); + uint256 curationFeesCut = subgraphService.curationFeesCut(); + queryFeeData.curationCut = curation.isCurated(_subgraphDeploymentId) ? curationFeesCut : 0; + return queryFeeData; + } + + function _handleIndexingRewardsCollection( + bytes memory _data + ) private returns (uint256 paymentCollected, address allocationId, IndexingRewardsData memory indexingRewardsData) { + (allocationId, indexingRewardsData.poi) = abi.decode(_data, (address, bytes32)); + Allocation.State memory allocation = subgraphService.getAllocation(allocationId); + + // Calculate accumulated tokens, this depends on the rewards manager which we have mocked + uint256 accRewardsPerTokens = allocation.tokens.mulPPM(rewardsManager.rewardsPerSignal()); + // Calculate the payment collected by the indexer for this transaction + paymentCollected = accRewardsPerTokens - allocation.accRewardsPerAllocatedToken; + + uint256 currentEpoch = epochManager.currentEpoch(); + paymentCollected = currentEpoch > allocation.createdAtEpoch ? paymentCollected : 0; + + uint256 delegatorCut = staking.getDelegationFeeCut( + allocation.indexer, + address(subgraphService), + IGraphPayments.PaymentTypes.IndexingRewards + ); + IHorizonStakingTypes.DelegationPool memory delegationPool = staking.getDelegationPool( + allocation.indexer, + address(subgraphService) + ); + indexingRewardsData.tokensDelegationRewards = delegationPool.shares > 0 + ? paymentCollected.mulPPM(delegatorCut) + : 0; + indexingRewardsData.tokensIndexerRewards = paymentCollected - indexingRewardsData.tokensDelegationRewards; + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.IndexingRewardsCollected( + allocation.indexer, + allocationId, + allocation.subgraphDeploymentId, + paymentCollected, + indexingRewardsData.tokensIndexerRewards, + indexingRewardsData.tokensDelegationRewards, + indexingRewardsData.poi, + epochManager.currentEpoch() + ); + + return (paymentCollected, allocationId, indexingRewardsData); + } + + function _verifyQueryFeeCollection( + address _indexer, + uint256 _paymentCollected, + bytes memory _data, + CollectPaymentData memory collectPaymentDataBefore, + CollectPaymentData memory collectPaymentDataAfter + ) private view { + IGraphTallyCollector.SignedRAV memory signedRav = abi.decode(_data, (IGraphTallyCollector.SignedRAV)); + Allocation.State memory allocation = subgraphService.getAllocation( + address(uint160(uint256(signedRav.rav.collectionId))) + ); + QueryFeeData memory queryFeeData = _queryFeeData(allocation.subgraphDeploymentId); + uint256 tokensProtocol = _paymentCollected.mulPPMRoundUp(queryFeeData.protocolPaymentCut); + uint256 curationTokens = (_paymentCollected - tokensProtocol).mulPPMRoundUp(queryFeeData.curationCut); + uint256 expectedIndexerTokensPayment = _paymentCollected - tokensProtocol - curationTokens; + + assertEq( + collectPaymentDataAfter.indexerBalance, + collectPaymentDataBefore.indexerBalance + expectedIndexerTokensPayment + ); + assertEq(collectPaymentDataAfter.curationBalance, collectPaymentDataBefore.curationBalance + curationTokens); + + // Check locked tokens + uint256 tokensToLock = _paymentCollected * subgraphService.stakeToFeesRatio(); + assertEq(collectPaymentDataAfter.lockedTokens, collectPaymentDataBefore.lockedTokens + tokensToLock); + + // Check the stake claim + LinkedList.List memory claimsList = _getClaimList(_indexer); + bytes32 claimId = _buildStakeClaimId(_indexer, claimsList.nonce - 1); + IDataServiceFees.StakeClaim memory stakeClaim = _getStakeClaim(claimId); + uint64 disputePeriod = disputeManager.getDisputePeriod(); + assertEq(stakeClaim.tokens, tokensToLock); + assertEq(stakeClaim.createdAt, block.timestamp); + assertEq(stakeClaim.releasableAt, block.timestamp + disputePeriod); + assertEq(stakeClaim.nextClaim, bytes32(0)); + } + + function _verifyIndexingRewardsCollection( + address _indexer, + address allocationId, + IndexingRewardsData memory indexingRewardsData, + CollectPaymentData memory collectPaymentDataBefore, + CollectPaymentData memory collectPaymentDataAfter + ) private { + Allocation.State memory allocation = subgraphService.getAllocation(allocationId); + + // Check allocation state + assertEq(allocation.accRewardsPending, 0); + uint256 accRewardsPerAllocatedToken = rewardsManager.onSubgraphAllocationUpdate( + allocation.subgraphDeploymentId + ); + assertEq(allocation.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken); + assertEq(allocation.lastPOIPresentedAt, block.timestamp); + + // Check indexer got paid the correct amount + address rewardsDestination = subgraphService.rewardsDestination(_indexer); + if (rewardsDestination == address(0)) { + // If rewards destination is address zero indexer should get paid to their provision balance + assertEq( + collectPaymentDataAfter.indexerProvisionBalance, + collectPaymentDataBefore.indexerProvisionBalance + indexingRewardsData.tokensIndexerRewards + ); + } else { + // If rewards destination is set indexer should get paid to the rewards destination address + assertEq( + collectPaymentDataAfter.rewardsDestinationBalance, + collectPaymentDataBefore.rewardsDestinationBalance + indexingRewardsData.tokensIndexerRewards + ); + } + + // Check delegation pool got paid the correct amount + assertEq( + collectPaymentDataAfter.delegationPoolBalance, + collectPaymentDataBefore.delegationPoolBalance + indexingRewardsData.tokensDelegationRewards + ); + + // If after collecting indexing rewards the indexer is over allocated the allcation should close + uint256 tokensAvailable = staking.getTokensAvailable( + _indexer, + address(subgraphService), + subgraphService.getDelegationRatio() + ); + if (allocation.tokens <= tokensAvailable) { + // Indexer isn't over allocated so allocation should still be open + assertTrue(allocation.isOpen()); + } else { + // Indexer is over allocated so allocation should be closed + assertFalse(allocation.isOpen()); + } + } + + function _migrateLegacyAllocation(address _indexer, address _allocationId, bytes32 _subgraphDeploymentID) internal { + vm.expectEmit(address(subgraphService)); + emit AllocationManager.LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentID); + + subgraphService.migrateLegacyAllocation(_indexer, _allocationId, _subgraphDeploymentID); + + LegacyAllocation.State memory afterLegacyAllocation = subgraphService.getLegacyAllocation(_allocationId); + assertEq(afterLegacyAllocation.indexer, _indexer); + assertEq(afterLegacyAllocation.subgraphDeploymentId, _subgraphDeploymentID); + } + + /* + * HELPERS + */ + + function _createAndStartAllocation(address _indexer, uint256 _tokens) internal { + mint(_indexer, _tokens); + + resetPrank(_indexer); + token.approve(address(staking), _tokens); + staking.stakeTo(_indexer, _tokens); + staking.provision(_indexer, address(subgraphService), _tokens, maxSlashingPercentage, disputePeriod); + _register(_indexer, abi.encode("url", "geoHash", address(0))); + + (address newIndexerAllocationId, uint256 newIndexerAllocationKey) = makeAddrAndKey("newIndexerAllocationId"); + bytes32 digest = subgraphService.encodeAllocationProof(_indexer, newIndexerAllocationId); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(newIndexerAllocationKey, digest); + + bytes memory data = abi.encode(subgraphDeployment, _tokens, newIndexerAllocationId, abi.encodePacked(r, s, v)); + _startService(_indexer, data); + } + + /* + * PRIVATE FUNCTIONS + */ + + function _recoverRAVSigner(IGraphTallyCollector.SignedRAV memory _signedRAV) private view returns (address) { + bytes32 messageHash = graphTallyCollector.encodeRAV(_signedRAV.rav); + return ECDSA.recover(messageHash, _signedRAV.signature); + } + + function _getClaimList(address _indexer) private view returns (LinkedList.List memory) { + (bytes32 head, bytes32 tail, uint256 nonce, uint256 count) = subgraphService.claimsLists(_indexer); + return LinkedList.List(head, tail, nonce, count); + } + + function _buildStakeClaimId(address _indexer, uint256 _nonce) private view returns (bytes32) { + return keccak256(abi.encodePacked(address(subgraphService), _indexer, _nonce)); + } + + function _getStakeClaim(bytes32 _claimId) private view returns (IDataServiceFees.StakeClaim memory) { + (uint256 tokens, uint256 createdAt, uint256 releasableAt, bytes32 nextClaim) = subgraphService.claims(_claimId); + return IDataServiceFees.StakeClaim(tokens, createdAt, releasableAt, nextClaim); + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocation/forceClose.t.sol b/packages/subgraph-service/test/subgraphService/allocation/forceClose.t.sol new file mode 100644 index 000000000..1ac1383ab --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocation/forceClose.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; + +import { Allocation } from "../../../contracts/libraries/Allocation.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceAllocationForceCloseTest is SubgraphServiceTest { + address private permissionlessBob = makeAddr("permissionlessBob"); + + /* + * TESTS + */ + + function test_SubgraphService_Allocation_ForceClose_Stale(uint256 tokens) public useIndexer useAllocation(tokens) { + // Skip forward + skip(maxPOIStaleness + 1); + + resetPrank(permissionlessBob); + _closeStaleAllocation(allocationID); + } + + function test_SubgraphService_Allocation_ForceClose_Stale_AfterCollecting( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Simulate POIs being submitted + uint8 numberOfPOIs = 5; + uint256 timeBetweenPOIs = 5 days; + + for (uint8 i = 0; i < numberOfPOIs; i++) { + // Skip forward + skip(timeBetweenPOIs); + + bytes memory data = abi.encode(allocationID, bytes32("POI1")); + _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); + } + + // Skip forward so that the allocation is stale + skip(maxPOIStaleness + 1); + + // Close the stale allocation + resetPrank(permissionlessBob); + _closeStaleAllocation(allocationID); + } + + function test_SubgraphService_Allocation_ForceClose_RevertIf_NotStale( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Simulate POIs being submitted + uint8 numberOfPOIs = 20; + uint256 timeBetweenPOIs = (maxPOIStaleness - 1) / numberOfPOIs; + + for (uint8 i = 0; i < numberOfPOIs; i++) { + // Skip forward + skip(timeBetweenPOIs); + + resetPrank(users.indexer); + + bytes memory data = abi.encode(allocationID, bytes32("POI1")); + _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); + + resetPrank(permissionlessBob); + vm.expectRevert( + abi.encodeWithSelector( + ISubgraphService.SubgraphServiceCannotForceCloseAllocation.selector, + allocationID + ) + ); + subgraphService.closeStaleAllocation(allocationID); + } + } + + function test_SubgraphService_Allocation_ForceClose_RevertIf_Altruistic(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _createSubgraphAllocationData(users.indexer, subgraphDeployment, allocationIDPrivateKey, 0); + _startService(users.indexer, data); + + skip(maxPOIStaleness + 1); + + resetPrank(permissionlessBob); + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceAllocationIsAltruistic.selector, allocationID) + ); + subgraphService.closeStaleAllocation(allocationID); + } + + function test_SubgraphService_Allocation_ForceClose_RevertIf_Paused() public useIndexer useAllocation(1000 ether) { + resetPrank(users.pauseGuardian); + subgraphService.pause(); + + resetPrank(permissionlessBob); + vm.expectRevert(abi.encodeWithSelector(PausableUpgradeable.EnforcedPause.selector)); + subgraphService.closeStaleAllocation(allocationID); + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocation/overDelegated.t.sol b/packages/subgraph-service/test/subgraphService/allocation/overDelegated.t.sol new file mode 100644 index 000000000..d1f2f5ff0 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocation/overDelegated.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; + +import { Allocation } from "../../../contracts/libraries/Allocation.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceAllocationOverDelegatedTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Allocation_OverDelegated_NotOverAllocatedAfterUndelegation( + uint256 delegationTokens, + uint256 undelegationTokens + ) public useIndexer { + // Use minimum provision tokens + uint256 indexerTokens = minimumProvisionTokens; + uint256 allocationTokens = indexerTokens * delegationRatio; + // Bound delegation tokens to be over delegated + delegationTokens = bound(delegationTokens, allocationTokens, MAX_TOKENS); + // Assume undelegation tokens to still leave indexer over delegated + vm.assume(undelegationTokens > 1); + vm.assume(undelegationTokens < delegationTokens - allocationTokens); + + // Create provision + token.approve(address(staking), indexerTokens); + _createProvision(users.indexer, indexerTokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + // Delegate so that indexer is over allocated + resetPrank(users.delegator); + token.approve(address(staking), delegationTokens); + _delegate(users.indexer, address(subgraphService), delegationTokens, 0); + + // Create allocation + resetPrank(users.indexer); + bytes memory data = _createSubgraphAllocationData( + users.indexer, + subgraphDeployment, + allocationIDPrivateKey, + allocationTokens + ); + _startService(users.indexer, data); + + // Undelegate + resetPrank(users.delegator); + _undelegate(users.indexer, address(subgraphService), undelegationTokens); + + // Check that indexer is not over allocated + assertFalse(subgraphService.isOverAllocated(users.indexer)); + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol b/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol new file mode 100644 index 000000000..9667309a1 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { Allocation } from "../../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; + +contract SubgraphServiceAllocationResizeTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Allocation_Resize( + uint256 tokens, + uint256 resizeTokens + ) public useIndexer useAllocation(tokens) { + resizeTokens = bound(resizeTokens, 1, MAX_TOKENS); + vm.assume(resizeTokens != tokens); + + mint(users.indexer, resizeTokens); + _addToProvision(users.indexer, resizeTokens); + _resizeAllocation(users.indexer, allocationID, resizeTokens); + } + + function test_SubgraphService_Allocation_Resize_AfterCollectingIndexingRewards( + uint256 tokens, + uint256 resizeTokens + ) public useIndexer useAllocation(tokens) { + resizeTokens = bound(resizeTokens, 1, MAX_TOKENS); + vm.assume(resizeTokens != tokens); + + mint(users.indexer, resizeTokens); + + // skip time to ensure allocation gets rewards + vm.roll(block.number + EPOCH_LENGTH); + + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI1")); + _collect(users.indexer, paymentType, data); + _addToProvision(users.indexer, resizeTokens); + _resizeAllocation(users.indexer, allocationID, resizeTokens); + } + + function test_SubgraphService_Allocation_Resize_SecondTime( + uint256 tokens, + uint256 firstResizeTokens, + uint256 secondResizeTokens + ) public useIndexer useAllocation(tokens) { + firstResizeTokens = bound(firstResizeTokens, 1, MAX_TOKENS); + secondResizeTokens = bound(secondResizeTokens, 1, MAX_TOKENS); + vm.assume(firstResizeTokens != tokens); + vm.assume(secondResizeTokens != firstResizeTokens); + + mint(users.indexer, firstResizeTokens); + _addToProvision(users.indexer, firstResizeTokens); + _resizeAllocation(users.indexer, allocationID, firstResizeTokens); + + mint(users.indexer, secondResizeTokens); + _addToProvision(users.indexer, secondResizeTokens); + _resizeAllocation(users.indexer, allocationID, secondResizeTokens); + } + + function test_SubgraphService_Allocation_Resize_RevertWhen_NotAuthorized( + uint256 tokens, + uint256 resizeTokens + ) public useIndexer useAllocation(tokens) { + resizeTokens = bound(resizeTokens, tokens + 1, MAX_TOKENS); + + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + vm.expectRevert( + abi.encodeWithSelector( + ISubgraphService.SubgraphServiceAllocationNotAuthorized.selector, + newIndexer, + allocationID + ) + ); + subgraphService.resizeAllocation(newIndexer, allocationID, resizeTokens); + } + + function test_SubgraphService_Allocation_Resize_RevertWhen_SameSize( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + vm.expectRevert( + abi.encodeWithSelector(AllocationManager.AllocationManagerAllocationSameSize.selector, allocationID, tokens) + ); + subgraphService.resizeAllocation(users.indexer, allocationID, tokens); + } + + function test_SubgraphService_Allocation_Resize_RevertIf_AllocationIsClosed( + uint256 tokens, + uint256 resizeTokens + ) public useIndexer useAllocation(tokens) { + resizeTokens = bound(resizeTokens, tokens + 1, MAX_TOKENS); + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); + vm.expectRevert( + abi.encodeWithSelector(AllocationManager.AllocationManagerAllocationClosed.selector, allocationID) + ); + subgraphService.resizeAllocation(users.indexer, allocationID, resizeTokens); + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocation/start.t.sol b/packages/subgraph-service/test/subgraphService/allocation/start.t.sol new file mode 100644 index 000000000..15e48dc66 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocation/start.t.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; + +import { Allocation } from "../../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { LegacyAllocation } from "../../../contracts/libraries/LegacyAllocation.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceAllocationStartTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Allocation_Start(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(tokens); + _startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_AllowsZeroTokens(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(0); + _startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_ByOperator(uint256 tokens) public useOperator { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(tokens); + _startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + resetPrank(users.operator); + bytes memory data = _generateData(tokens); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.indexer, + users.operator + ) + ); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_NoValidProvision(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + bytes memory data = _generateData(tokens); + vm.expectRevert( + abi.encodeWithSelector(ProvisionManager.ProvisionManagerProvisionNotFound.selector, users.indexer) + ); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_NotRegistered(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + + bytes memory data = _generateData(tokens); + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, users.indexer) + ); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_ZeroAllocationId(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes32 digest = subgraphService.encodeAllocationProof(users.indexer, address(0)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationIDPrivateKey, digest); + bytes memory data = abi.encode(subgraphDeployment, tokens, address(0), abi.encodePacked(r, s, v)); + vm.expectRevert(abi.encodeWithSelector(AllocationManager.AllocationManagerInvalidZeroAllocationId.selector)); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_InvalidSignature(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + (address signer, uint256 signerPrivateKey) = makeAddrAndKey("invalidSigner"); + bytes32 digest = subgraphService.encodeAllocationProof(users.indexer, allocationID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest); + bytes memory data = abi.encode(subgraphDeployment, tokens, allocationID, abi.encodePacked(r, s, v)); + vm.expectRevert( + abi.encodeWithSelector( + AllocationManager.AllocationManagerInvalidAllocationProof.selector, + signer, + allocationID + ) + ); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_InvalidData(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = abi.encode(subgraphDeployment, tokens, allocationID, _generateRandomHexBytes(32)); + vm.expectRevert(abi.encodeWithSelector(ECDSA.ECDSAInvalidSignatureLength.selector, 32)); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_AlreadyExists_SubgraphService( + uint256 tokens + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(tokens); + _startService(users.indexer, data); + + vm.expectRevert(abi.encodeWithSelector(Allocation.AllocationAlreadyExists.selector, allocationID)); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_AlreadyExists_Migrated(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + resetPrank(users.governor); + _migrateLegacyAllocation(users.indexer, allocationID, subgraphDeployment); + + resetPrank(users.indexer); + bytes memory data = _generateData(tokens); + vm.expectRevert(abi.encodeWithSelector(LegacyAllocation.LegacyAllocationAlreadyExists.selector, allocationID)); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_AlreadyExists_Staking(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + // create dummy allo in staking contract + _setStorage_allocation_hardcoded(users.indexer, allocationID, tokens); + + bytes memory data = _generateData(tokens); + vm.expectRevert(abi.encodeWithSelector(LegacyAllocation.LegacyAllocationAlreadyExists.selector, allocationID)); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_NotEnoughTokens( + uint256 tokens, + uint256 lockTokens + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS - 1); + lockTokens = bound(lockTokens, tokens + 1, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(lockTokens); + vm.expectRevert( + abi.encodeWithSelector(ProvisionTracker.ProvisionTrackerInsufficientTokens.selector, tokens, lockTokens) + ); + subgraphService.startService(users.indexer, data); + } + + /* + * PRIVATE FUNCTIONS + */ + + function _generateData(uint256 tokens) private view returns (bytes memory) { + return _createSubgraphAllocationData(users.indexer, subgraphDeployment, allocationIDPrivateKey, tokens); + } + + function _generateRandomHexBytes(uint256 length) private view returns (bytes memory) { + bytes memory randomBytes = new bytes(length); + for (uint256 i = 0; i < length; i++) { + randomBytes[i] = bytes1( + uint8(uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, i))) % 256) + ); + } + return randomBytes; + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocation/stop.t.sol b/packages/subgraph-service/test/subgraphService/allocation/stop.t.sol new file mode 100644 index 000000000..d861371e5 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocation/stop.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; + +import { Allocation } from "../../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { LegacyAllocation } from "../../../contracts/libraries/LegacyAllocation.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceAllocationStopTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Allocation_Stop(uint256 tokens) public useIndexer useAllocation(tokens) { + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Stop_RevertWhen_IndexerIsNotTheAllocationOwner( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + + // Attempt to close other indexer's allocation + bytes memory data = abi.encode(allocationID); + vm.expectRevert( + abi.encodeWithSelector( + ISubgraphService.SubgraphServiceAllocationNotAuthorized.selector, + newIndexer, + allocationID + ) + ); + subgraphService.stopService(newIndexer, data); + } + + function test_SubgraphService_Allocation_Stop_RevertWhen_NotAuthorized( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.operator); + bytes memory data = abi.encode(allocationID); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.indexer, + users.operator + ) + ); + subgraphService.stopService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Stop_RevertWhen_NotRegistered() public useIndexer { + bytes memory data = abi.encode(allocationID); + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, users.indexer) + ); + subgraphService.stopService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Stop_RevertWhen_NotOpen( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); + vm.expectRevert(abi.encodeWithSelector(Allocation.AllocationClosed.selector, allocationID, block.timestamp)); + subgraphService.stopService(users.indexer, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/collect/collect.t.sol b/packages/subgraph-service/test/subgraphService/collect/collect.t.sol new file mode 100644 index 000000000..33ba00ff6 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/collect/collect.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; + +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceCollectTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Collect_RevertWhen_InvalidPayment( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes invalidPaymentType = IGraphPayments.PaymentTypes.IndexingFee; + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceInvalidPaymentType.selector, invalidPaymentType) + ); + subgraphService.collect(users.indexer, invalidPaymentType, ""); + } +} diff --git a/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol b/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol new file mode 100644 index 000000000..a35f37ecc --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; + +import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../../SubgraphService.t.sol"; + +contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Collect_Indexing(uint256 tokens) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI")); + + // skip time to ensure allocation gets rewards + vm.roll(block.number + EPOCH_LENGTH); + + _collect(users.indexer, paymentType, data); + } + + function test_SubgraphService_Collect_Indexing_WithDelegation( + uint256 tokens, + uint256 delegationTokens, + uint256 delegationFeeCut + ) public useIndexer useAllocation(tokens) useDelegation(delegationTokens) { + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); + _setDelegationFeeCut( + users.indexer, + address(subgraphService), + IGraphPayments.PaymentTypes.IndexingRewards, + delegationFeeCut + ); + + // skip time to ensure allocation gets rewards + vm.roll(block.number + EPOCH_LENGTH); + + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, paymentType, data); + } + + function test_SubgraphService_Collect_Indexing_AfterUndelegate( + uint256 tokens, + uint256 delegationTokens, + uint256 delegationFeeCut + ) public useIndexer useAllocation(tokens) useDelegation(delegationTokens) { + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); + _setDelegationFeeCut( + users.indexer, + address(subgraphService), + IGraphPayments.PaymentTypes.IndexingRewards, + delegationFeeCut + ); + // Undelegate + resetPrank(users.delegator); + staking.undelegate(users.indexer, address(subgraphService), delegationTokens); + + // skip time to ensure allocation gets rewards + vm.roll(block.number + EPOCH_LENGTH); + + resetPrank(users.indexer); + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, paymentType, data); + } + + function test_SubgraphService_Collect_Indexing_RewardsDestination( + uint256 tokens + ) public useIndexer useAllocation(tokens) useRewardsDestination { + // skip time to ensure allocation gets rewards + vm.roll(block.number + EPOCH_LENGTH); + + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, paymentType, data); + } + + function test_subgraphService_Collect_Indexing_MultipleOverTime( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + uint8 numberOfPOIs = 20; + uint256 timeBetweenPOIs = 5 days; + + for (uint8 i = 0; i < numberOfPOIs; i++) { + // Skip forward + skip(timeBetweenPOIs); + + resetPrank(users.indexer); + + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); + } + } + + function test_subgraphService_Collect_Indexing_MultipleOverTime_WithDelegation( + uint256 tokens, + uint256 delegationTokens, + uint256 delegationFeeCut + ) public useIndexer useAllocation(tokens) useDelegation(delegationTokens) { + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); + _setDelegationFeeCut( + users.indexer, + address(subgraphService), + IGraphPayments.PaymentTypes.IndexingRewards, + delegationFeeCut + ); + + uint8 numberOfPOIs = 20; + uint256 timeBetweenPOIs = 5 days; + for (uint8 i = 0; i < numberOfPOIs; i++) { + // Skip forward + skip(timeBetweenPOIs); + + resetPrank(users.indexer); + + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); + } + } + + function test_SubgraphService_Collect_Indexing_OverAllocated(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens * 2, 10_000_000_000 ether); + + // setup allocation + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + bytes memory data = _createSubgraphAllocationData( + users.indexer, + subgraphDeployment, + allocationIDPrivateKey, + tokens + ); + _startService(users.indexer, data); + + // thaw some tokens to become over allocated + staking.thaw(users.indexer, address(subgraphService), tokens / 2); + + // skip time to ensure allocation gets rewards + vm.roll(block.number + EPOCH_LENGTH); + + // this collection should close the allocation + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory collectData = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, paymentType, collectData); + } + + function test_SubgraphService_Collect_Indexing_RevertWhen_IndexerIsNotAllocationOwner( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + bytes memory data = abi.encode(allocationID, bytes32("POI")); + + // skip time to ensure allocation gets rewards + vm.roll(block.number + EPOCH_LENGTH); + + // Attempt to collect from other indexer's allocation + vm.expectRevert( + abi.encodeWithSelector( + ISubgraphService.SubgraphServiceAllocationNotAuthorized.selector, + newIndexer, + allocationID + ) + ); + subgraphService.collect(newIndexer, paymentType, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol new file mode 100644 index 000000000..28cc677cf --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IGraphTallyCollector } from "@graphprotocol/horizon/contracts/interfaces/IGraphTallyCollector.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../../SubgraphService.t.sol"; + +contract SubgraphServiceRegisterTest is SubgraphServiceTest { + using PPMMath for uint128; + using PPMMath for uint256; + + address signer; + uint256 signerPrivateKey; + + /* + * HELPERS + */ + + function _getSignerProof(uint256 _proofDeadline, uint256 _signer) private returns (bytes memory) { + (, address msgSender, ) = vm.readCallers(); + bytes32 messageHash = keccak256( + abi.encodePacked( + block.chainid, + address(graphTallyCollector), + "authorizeSignerProof", + _proofDeadline, + msgSender + ) + ); + bytes32 proofToDigest = MessageHashUtils.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signer, proofToDigest); + return abi.encodePacked(r, s, v); + } + + function _getQueryFeeEncodedData(address indexer, uint128 tokens) private view returns (bytes memory) { + IGraphTallyCollector.ReceiptAggregateVoucher memory rav = _getRAV( + indexer, + bytes32(uint256(uint160(allocationID))), + tokens + ); + bytes32 messageHash = graphTallyCollector.encodeRAV(rav); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + IGraphTallyCollector.SignedRAV memory signedRAV = IGraphTallyCollector.SignedRAV(rav, signature); + return abi.encode(signedRAV); + } + + function _getRAV( + address indexer, + bytes32 collectionId, + uint128 tokens + ) private view returns (IGraphTallyCollector.ReceiptAggregateVoucher memory rav) { + return + IGraphTallyCollector.ReceiptAggregateVoucher({ + collectionId: collectionId, + payer: users.gateway, + serviceProvider: indexer, + dataService: address(subgraphService), + timestampNs: 0, + valueAggregate: tokens, + metadata: "" + }); + } + + function _deposit(uint256 tokens) private { + token.approve(address(escrow), tokens); + escrow.deposit(address(graphTallyCollector), users.indexer, tokens); + } + + function _authorizeSigner() private { + uint256 proofDeadline = block.timestamp + 1; + bytes memory proof = _getSignerProof(proofDeadline, signerPrivateKey); + graphTallyCollector.authorizeSigner(signer, proofDeadline, proof); + } + + /* + * SET UP + */ + + function setUp() public virtual override { + super.setUp(); + (signer, signerPrivateKey) = makeAddrAndKey("signer"); + vm.label({ account: signer, newLabel: "signer" }); + } + + /* + * TESTS + */ + + function testCollect_QueryFees( + uint256 tokensAllocated, + uint256 tokensPayment + ) public useIndexer useAllocation(tokensAllocated) { + vm.assume(tokensAllocated > minimumProvisionTokens * stakeToFeesRatio); + uint256 maxTokensPayment = tokensAllocated / stakeToFeesRatio > type(uint128).max + ? type(uint128).max + : tokensAllocated / stakeToFeesRatio; + tokensPayment = bound(tokensPayment, minimumProvisionTokens, maxTokensPayment); + + resetPrank(users.gateway); + _deposit(tokensPayment); + _authorizeSigner(); + + resetPrank(users.indexer); + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokensPayment)); + _collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testCollect_MultipleQueryFees( + uint256 tokensAllocated, + uint8 numPayments + ) public useIndexer useAllocation(tokensAllocated) { + vm.assume(tokensAllocated > minimumProvisionTokens * stakeToFeesRatio); + numPayments = uint8(bound(numPayments, 2, 10)); + uint256 tokensPayment = tokensAllocated / stakeToFeesRatio / numPayments; + + resetPrank(users.gateway); + _deposit(tokensAllocated); + _authorizeSigner(); + + resetPrank(users.indexer); + uint256 accTokensPayment = 0; + for (uint i = 0; i < numPayments; i++) { + accTokensPayment = accTokensPayment + tokensPayment; + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(accTokensPayment)); + _collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); + } + } + + function testCollect_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokens)); + resetPrank(users.operator); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.indexer, + users.operator + ) + ); + subgraphService.collect(users.indexer, paymentType, data); + } + + function testCollect_QueryFees_RevertWhen_IndexerIsNotAllocationOwner( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + + // This data is for user.indexer allocationId + bytes memory data = _getQueryFeeEncodedData(newIndexer, uint128(tokens)); + + resetPrank(newIndexer); + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceInvalidRAV.selector, newIndexer, users.indexer) + ); + subgraphService.collect(newIndexer, paymentType, data); + } + + function testCollect_QueryFees_RevertWhen_CollectingOtherIndexersFees( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokens)); + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerMismatch.selector, users.indexer, newIndexer) + ); + subgraphService.collect(newIndexer, paymentType, data); + } + + function testCollect_QueryFees_RevertWhen_CollectionIdTooLarge() public useIndexer useAllocation(1000 ether) { + bytes32 collectionId = keccak256(abi.encodePacked("Large collection id, longer than 160 bits")); + IGraphTallyCollector.ReceiptAggregateVoucher memory rav = _getRAV(users.indexer, collectionId, 1000 ether); + bytes32 messageHash = graphTallyCollector.encodeRAV(rav); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + IGraphTallyCollector.SignedRAV memory signedRAV = IGraphTallyCollector.SignedRAV(rav, signature); + bytes memory data = abi.encode(signedRAV); + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceInvalidCollectionId.selector, collectionId) + ); + subgraphService.collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/governance/legacy.t.sol b/packages/subgraph-service/test/subgraphService/governance/legacy.t.sol new file mode 100644 index 000000000..d1b5dd124 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/governance/legacy.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceLegacyAllocation is SubgraphServiceTest { + /* + * TESTS + */ + + function test_MigrateAllocation() public useGovernor { + _migrateLegacyAllocation(users.indexer, allocationID, subgraphDeployment); + } + + function test_MigrateAllocation_WhenNotGovernor() public useIndexer { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.indexer)); + subgraphService.migrateLegacyAllocation(users.indexer, allocationID, subgraphDeployment); + } +} diff --git a/packages/subgraph-service/test/subgraphService/governance/stakeToFeesRatio.t.sol b/packages/subgraph-service/test/subgraphService/governance/stakeToFeesRatio.t.sol new file mode 100644 index 000000000..d1aca8155 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/governance/stakeToFeesRatio.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract DisputeManagerGovernanceArbitratorTest is SubgraphServiceTest { + /** + * ACTIONS + */ + + function _setStakeToFeesRatio(uint256 _stakeToFeesRatio) internal { + vm.expectEmit(address(subgraphService)); + emit ISubgraphService.StakeToFeesRatioSet(_stakeToFeesRatio); + subgraphService.setStakeToFeesRatio(_stakeToFeesRatio); + assertEq(subgraphService.stakeToFeesRatio(), _stakeToFeesRatio); + } + + /* + * TESTS + */ + + function test_Governance_SetStakeToFeesRatio(uint256 stakeToFeesRatio) public useGovernor { + vm.assume(stakeToFeesRatio > 0); + _setStakeToFeesRatio(stakeToFeesRatio); + } + + function test_Governance_RevertWhen_ZeroValue() public useGovernor { + uint256 stakeToFeesRatio = 0; + vm.expectRevert(abi.encodeWithSelector(ISubgraphService.SubgraphServiceInvalidZeroStakeToFeesRatio.selector)); + subgraphService.setStakeToFeesRatio(stakeToFeesRatio); + } + + function test_Governance_RevertWhen_NotGovernor() public useIndexer { + uint256 stakeToFeesRatio = 2; + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.indexer)); + subgraphService.setStakeToFeesRatio(stakeToFeesRatio); + } +} diff --git a/packages/subgraph-service/test/subgraphService/provider/register.t.sol b/packages/subgraph-service/test/subgraphService/provider/register.t.sol new file mode 100644 index 000000000..942f2e16a --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/provider/register.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceProviderRegisterTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Provider_Register(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + bytes memory data = abi.encode("url", "geoHash", users.rewardsDestination); + _register(users.indexer, data); + } + + function test_SubgraphService_Provider_Register_RevertIf_AlreadyRegistered( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + vm.expectRevert(abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerAlreadyRegistered.selector)); + bytes memory data = abi.encode("url", "geoHash", users.rewardsDestination); + subgraphService.register(users.indexer, data); + } + + function test_SubgraphService_Provider_Register_RevertWhen_InvalidProvision() public useIndexer { + vm.expectRevert( + abi.encodeWithSelector(ProvisionManager.ProvisionManagerProvisionNotFound.selector, users.indexer) + ); + bytes memory data = abi.encode("url", "geoHash", users.rewardsDestination); + subgraphService.register(users.indexer, data); + } + + function test_SubgraphService_Provider_Register_RevertWhen_NotAuthorized() public { + resetPrank(users.operator); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.indexer, + users.operator + ) + ); + bytes memory data = abi.encode("url", "geoHash", users.rewardsDestination); + subgraphService.register(users.indexer, data); + } + + function test_SubgraphService_Provider_Register_RevertWhen_InvalidProvisionValues( + uint256 tokens + ) public useIndexer { + tokens = bound(tokens, 1, minimumProvisionTokens - 1); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "tokens", + tokens, + minimumProvisionTokens, + maximumProvisionTokens + ) + ); + subgraphService.register(users.indexer, abi.encode("url", "geoHash", address(0))); + } + + function test_SubgraphService_Provider_Register_RevertIf_EmptyUrl(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + bytes memory data = abi.encode("", "geoHash", users.rewardsDestination); + vm.expectRevert(abi.encodeWithSelector(ISubgraphService.SubgraphServiceEmptyUrl.selector)); + subgraphService.register(users.indexer, data); + } + + function test_SubgraphService_Provider_Register_RevertIf_EmptyGeohash(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + bytes memory data = abi.encode("url", "", users.rewardsDestination); + vm.expectRevert(abi.encodeWithSelector(ISubgraphService.SubgraphServiceEmptyGeohash.selector)); + subgraphService.register(users.indexer, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/provider/rewardsDestination.t.sol b/packages/subgraph-service/test/subgraphService/provider/rewardsDestination.t.sol new file mode 100644 index 000000000..600f655b2 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/provider/rewardsDestination.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceProviderRewardsDestinationTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Provider_RewardsDestination_Set( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Should be able to use new address + _setRewardsDestination(users.rewardsDestination); + + // Should be able to set back to address zero + _setRewardsDestination(address(0)); + } +} diff --git a/packages/subgraph-service/test/subgraphService/provision/accept.t.sol b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol new file mode 100644 index 000000000..4d421b979 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceProvisionAcceptTest is SubgraphServiceTest { + /* + * TESTS + */ + + function test_SubgraphService_Provision_Accept( + uint256 tokens, + uint32 newVerifierCut, + uint64 newDisputePeriod + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + vm.assume(newVerifierCut >= fishermanRewardPercentage); + vm.assume(newVerifierCut <= MAX_PPM); + newDisputePeriod = uint64(bound(newDisputePeriod, disputePeriod, MAX_WAIT_PERIOD)); + + // Setup indexer + _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + // Update parameters with new values + _setProvisionParameters(users.indexer, address(subgraphService), newVerifierCut, newDisputePeriod); + + // Accept provision and check parameters + _acceptProvision(users.indexer, ""); + } + + function test_SubgraphService_Provision_Accept_RevertWhen_NotRegistered() public useIndexer { + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, users.indexer) + ); + subgraphService.acceptProvisionPendingParameters(users.indexer, ""); + } + + function test_SubgraphService_Provision_Accept_RevertWhen_NotAuthorized() public { + resetPrank(users.operator); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.indexer, + users.operator + ) + ); + subgraphService.acceptProvisionPendingParameters(users.indexer, ""); + } + + function test_SubgraphService_Provision_Accept_RevertIf_InvalidVerifierCut( + uint256 tokens, + uint32 newVerifierCut + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + vm.assume(newVerifierCut < maxSlashingPercentage); + + // Setup indexer + _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + // Update parameters with new values + _setProvisionParameters(users.indexer, address(subgraphService), newVerifierCut, disputePeriod); + + // Should revert since newVerifierCut is invalid + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "maxVerifierCut", + newVerifierCut, + fishermanRewardPercentage, + MAX_PPM + ) + ); + subgraphService.acceptProvisionPendingParameters(users.indexer, ""); + } + + function test_SubgraphService_Provision_Accept_RevertIf_InvalidDisputePeriod( + uint256 tokens, + uint64 newDisputePeriod + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + vm.assume(newDisputePeriod < disputePeriod); + + // Setup indexer + _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + // Update parameters with new values + _setProvisionParameters(users.indexer, address(subgraphService), fishermanRewardPercentage, newDisputePeriod); + + // Should revert since newDisputePeriod is invalid + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "thawingPeriod", + newDisputePeriod, + disputePeriod, + type(uint64).max + ) + ); + subgraphService.acceptProvisionPendingParameters(users.indexer, ""); + } +} diff --git a/packages/subgraph-service/test/utils/Constants.sol b/packages/subgraph-service/test/utils/Constants.sol new file mode 100644 index 000000000..34be9ac74 --- /dev/null +++ b/packages/subgraph-service/test/utils/Constants.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +abstract contract Constants { + uint256 internal constant MAX_TOKENS = 10_000_000_000 ether; + uint256 internal constant MAX_PPM = 1_000_000; + uint256 internal constant EPOCH_LENGTH = 1; + // Dispute Manager + uint64 internal constant disputePeriod = 7 days; + uint256 internal constant MIN_DISPUTE_DEPOSIT = 1 ether; // 1 GRT + uint256 internal constant disputeDeposit = 100 ether; // 100 GRT + uint32 internal constant fishermanRewardPercentage = 500000; // 50% + uint32 internal constant maxSlashingPercentage = 500000; // 50% + // Subgraph Service + uint256 internal constant minimumProvisionTokens = 1000 ether; + uint256 internal constant maximumProvisionTokens = type(uint256).max; + uint32 internal constant delegationRatio = 16; + uint256 public constant stakeToFeesRatio = 2; + uint256 public constant maxPOIStaleness = 28 days; + uint256 public constant curationCut = 10000; + // Staking + uint64 internal constant MAX_WAIT_PERIOD = 28 days; + uint256 internal constant MIN_DELEGATION = 1 ether; + // GraphEscrow parameters + uint256 internal constant withdrawEscrowThawingPeriod = 60; + // GraphPayments parameters + uint256 internal constant protocolPaymentCut = 10000; + // RewardsMananger parameters + uint256 public constant rewardsPerSignal = 10000; + uint256 public constant rewardsPerSubgraphAllocationUpdate = 1000; + // GraphTallyCollector parameters + uint256 public constant revokeSignerThawingPeriod = 7 days; +} diff --git a/packages/subgraph-service/test/utils/Users.sol b/packages/subgraph-service/test/utils/Users.sol new file mode 100644 index 000000000..e0c142fe1 --- /dev/null +++ b/packages/subgraph-service/test/utils/Users.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +struct Users { + address governor; + address deployer; + address indexer; + address operator; + address gateway; + address verifier; + address delegator; + address arbitrator; + address fisherman; + address rewardsDestination; + address pauseGuardian; +} diff --git a/packages/subgraph-service/test/utils/Utils.sol b/packages/subgraph-service/test/utils/Utils.sol new file mode 100644 index 000000000..be42f269f --- /dev/null +++ b/packages/subgraph-service/test/utils/Utils.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +abstract contract Utils is Test { + /// @dev Stops the active prank and sets a new one. + function resetPrank(address msgSender) internal { + vm.stopPrank(); + vm.startPrank(msgSender); + } +} diff --git a/packages/subgraph-service/tsconfig.json b/packages/subgraph-service/tsconfig.json new file mode 100644 index 000000000..5f32ebc8c --- /dev/null +++ b/packages/subgraph-service/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "node16", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist", + "moduleResolution": "node16" + }, + "include": [ + "hardhat.config.ts", + "types/**/*.ts", + "scripts/**/*.ts", + "tasks/**/*.ts", + "test/**/*.ts", + "ignition/**/*.ts", + "eslint.config.js", + "prettier.config.js", + "natspec-smells.config.js" + ] +} diff --git a/packages/subgraph-service/types/hardhat-graph-protocol.d.ts b/packages/subgraph-service/types/hardhat-graph-protocol.d.ts new file mode 100644 index 000000000..8b5985269 --- /dev/null +++ b/packages/subgraph-service/types/hardhat-graph-protocol.d.ts @@ -0,0 +1,45 @@ +// TypeScript does not resolve correctly the type extensions when they are symlinked from the same monorepo. +// So we need to re-type it... this file should be a copy of hardhat-graph-protocol/src/type-extensions.ts +import 'hardhat/types/config' +import 'hardhat/types/runtime' +import type { GraphDeployments, GraphRuntimeEnvironment, GraphRuntimeEnvironmentOptions } from 'hardhat-graph-protocol' + +declare module 'hardhat/types/runtime' { + interface HardhatRuntimeEnvironment { + graph: (opts?: GraphRuntimeEnvironmentOptions) => GraphRuntimeEnvironment + } +} + +declare module 'hardhat/types/config' { + interface HardhatConfig { + graph: GraphRuntimeEnvironmentOptions + } + + interface HardhatUserConfig { + graph: GraphRuntimeEnvironmentOptions + } + + interface HardhatNetworkConfig { + deployments?: GraphDeployments + } + + interface HardhatNetworkUserConfig { + deployments?: GraphDeployments + } + + interface HttpNetworkConfig { + deployments?: GraphDeployments + } + + interface HttpNetworkUserConfig { + deployments?: GraphDeployments + } + + interface ProjectPathsConfig { + graph?: string + } + + interface ProjectPathsUserConfig { + graph?: string + } +} diff --git a/packages/token-distribution/CHANGELOG.md b/packages/token-distribution/CHANGELOG.md new file mode 100644 index 000000000..2809305b6 --- /dev/null +++ b/packages/token-distribution/CHANGELOG.md @@ -0,0 +1,7 @@ +# @graphprotocol/token-distribution + +## 1.2.1 + +### Patch Changes + +- Bump contracts dependency diff --git a/packages/token-distribution/package.json b/packages/token-distribution/package.json index 37069fc41..097bc13e0 100644 --- a/packages/token-distribution/package.json +++ b/packages/token-distribution/package.json @@ -1,6 +1,6 @@ { "name": "@graphprotocol/token-distribution", - "version": "1.2.0", + "version": "1.2.1", "description": "Graph Token Distribution", "main": "index.js", "scripts": { @@ -31,7 +31,7 @@ "devDependencies": { "@ethersproject/experimental": "^5.0.7", "@graphprotocol/client-cli": "^2.0.2", - "@graphprotocol/contracts": "^5.0.0", + "@graphprotocol/contracts": "workspace:^7.0.0", "@nomiclabs/hardhat-ethers": "^2.0.0", "@nomiclabs/hardhat-etherscan": "^3.1.7", "@nomiclabs/hardhat-waffle": "^2.0.0", @@ -65,6 +65,6 @@ "solhint-plugin-prettier": "^0.1.0", "ts-node": "^10.9.1", "typechain": "^5.0.0", - "typescript": "^4.0.2" + "typescript": "^5.2.2" } } diff --git a/packages/token-distribution/scripts/build b/packages/token-distribution/scripts/build index 7805eacb4..5f63fc2ee 100755 --- a/packages/token-distribution/scripts/build +++ b/packages/token-distribution/scripts/build @@ -2,5 +2,10 @@ set -eo pipefail +if [ -z "${STUDIO_API_KEY}" ]; then + echo "Warning: STUDIO_API_KEY is not set. Skipping build steps. Some functionality may be limited." + exit 0 +fi + yarn graphclient build yarn run compile \ No newline at end of file diff --git a/scripts/build b/scripts/build new file mode 100755 index 000000000..e07906ec3 --- /dev/null +++ b/scripts/build @@ -0,0 +1,46 @@ +#!/bin/bash + +BUILD_HORIZON_ONLY=${BUILD_HORIZON_ONLY:-false} + +# List packages to build - order matters! +horizon_packages=( + "packages/contracts" + "packages/horizon" + "packages/subgraph-service" +) + +all_packages=( + "packages/eslint-graph-config" + # "packages/solhint-graph-config" -- disabled since it doesn't have a build script + # "packages/solhint-plugin-graph" -- disabled since it doesn't have a build script + "packages/contracts" + "packages/horizon" + "packages/subgraph-service" + "packages/hardhat-graph-protocol" + "packages/data-edge" + "packages/sdk" + "packages/token-distribution" +) + +if [ "$BUILD_HORIZON_ONLY" = "true" ]; then + packages=("${horizon_packages[@]}") +else + packages=("${all_packages[@]}") +fi + +for package in "${packages[@]}"; do + echo -e "\n\n==== Building $package..." + + cd "$package" || { echo "Failed to navigate to $package"; exit 1; } + + if BUILD_RUN=true yarn build; then + echo "Successfully built $package" + else + echo "Build failed for $package" >&2 + exit 1 + fi + + cd - > /dev/null +done + +echo "All packages built successfully!" diff --git a/yarn.lock b/yarn.lock index ff0f7312d..5badedf56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,32 +5,32 @@ __metadata: version: 8 cacheKey: 10c0 -"@0no-co/graphql.web@npm:^1.0.1": - version: 1.0.4 - resolution: "@0no-co/graphql.web@npm:1.0.4" +"@0no-co/graphql.web@npm:^1.0.5": + version: 1.0.7 + resolution: "@0no-co/graphql.web@npm:1.0.7" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 peerDependenciesMeta: graphql: optional: true - checksum: bf63cb5b017063363c9a9e06dc17532abc1c2da402c7ebcbc7b5ab2a0601ec93b02de93af9e50d9daffb3b747eddcf0b1e5418a46d1182c5b8087b7d7a1768ad + checksum: 4744a6c327e0a2d564c8f4720ef08dcece6c3b8373f52208ff29f7086f90e18d211c59cc222c229f1e3241abd1fc6e30377dc1dadac491bbb25706f29dea626a languageName: node linkType: hard -"@aashutoshrathi/word-wrap@npm:^1.2.3": - version: 1.2.6 - resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" - checksum: 53c2b231a61a46792b39a0d43bc4f4f776bb4542aa57ee04930676802e5501282c2fc8aac14e4cd1f1120ff8b52616b6ff5ab539ad30aa2277d726444b71619f +"@adraffy/ens-normalize@npm:1.10.1": + version: 1.10.1 + resolution: "@adraffy/ens-normalize@npm:1.10.1" + checksum: fdd647604e8fac6204921888aaf5a6bc65eabf0d2921bc5f93b64d01f4bc33ead167c1445f7de05468d05cd92ac31b74c68d2be840c62b79d73693308f885c06 languageName: node linkType: hard "@ampproject/remapping@npm:^2.2.0": - version: 2.2.1 - resolution: "@ampproject/remapping@npm:2.2.1" + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 92ce5915f8901d8c7cd4f4e6e2fe7b9fd335a29955b400caa52e0e5b12ca3796ada7c2f10e78c9c5b0f9c2539dff0ffea7b19850a56e1487aa083531e1e46d43 + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 81d63cca5443e0f0c72ae18b544cc28c7c0ec2cea46e7cb888bb0e0f411a1191d0d6b7af798d54e30777d8d1488b2ec0732aac2be342d3d7d3ffd271c6f489ed languageName: node linkType: hard @@ -122,12 +122,12 @@ __metadata: linkType: hard "@aws-sdk/types@npm:^3.1.0": - version: 3.515.0 - resolution: "@aws-sdk/types@npm:3.515.0" + version: 3.577.0 + resolution: "@aws-sdk/types@npm:3.577.0" dependencies: - "@smithy/types": "npm:^2.9.1" - tslib: "npm:^2.5.0" - checksum: 47afecf060dec7e7db5073dfec7d6815582f66266cfb31111af4a80fc10bc56bf5c7a5dc096780849efa982a9b097e69b1b3bebf23d82521763b04ae17cc7202 + "@smithy/types": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: ae31757b05c2445f52b3f3268d7e9cbae765cae24f088afb8f967c8e3a268b425794a8e99fab3e0428dc6491ccca99b6c57ab5ca69e2d1cc2878ec85ff9643f7 languageName: node linkType: hard @@ -140,55 +140,55 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/code-frame@npm:7.23.5" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.2": + version: 7.24.2 + resolution: "@babel/code-frame@npm:7.24.2" dependencies: - "@babel/highlight": "npm:^7.23.4" - chalk: "npm:^2.4.2" - checksum: a10e843595ddd9f97faa99917414813c06214f4d9205294013e20c70fbdf4f943760da37dec1d998bf3e6fc20fa2918a47c0e987a7e458663feb7698063ad7c6 + "@babel/highlight": "npm:^7.24.2" + picocolors: "npm:^1.0.0" + checksum: d1d4cba89475ab6aab7a88242e1fd73b15ecb9f30c109b69752956434d10a26a52cbd37727c4eca104b6d45227bd1dfce39a6a6f4a14c9b2f07f871e968cf406 languageName: node linkType: hard "@babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/compat-data@npm:7.23.5" - checksum: 081278ed46131a890ad566a59c61600a5f9557bd8ee5e535890c8548192532ea92590742fd74bd9db83d74c669ef8a04a7e1c85cdea27f960233e3b83c3a957c + version: 7.24.4 + resolution: "@babel/compat-data@npm:7.24.4" + checksum: 9cd8a9cd28a5ca6db5d0e27417d609f95a8762b655e8c9c97fd2de08997043ae99f0139007083c5e607601c6122e8432c85fe391731b19bf26ad458fa0c60dd3 languageName: node linkType: hard "@babel/core@npm:^7.14.0": - version: 7.23.9 - resolution: "@babel/core@npm:7.23.9" + version: 7.24.5 + resolution: "@babel/core@npm:7.24.5" dependencies: "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.23.5" - "@babel/generator": "npm:^7.23.6" + "@babel/code-frame": "npm:^7.24.2" + "@babel/generator": "npm:^7.24.5" "@babel/helper-compilation-targets": "npm:^7.23.6" - "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helpers": "npm:^7.23.9" - "@babel/parser": "npm:^7.23.9" - "@babel/template": "npm:^7.23.9" - "@babel/traverse": "npm:^7.23.9" - "@babel/types": "npm:^7.23.9" + "@babel/helper-module-transforms": "npm:^7.24.5" + "@babel/helpers": "npm:^7.24.5" + "@babel/parser": "npm:^7.24.5" + "@babel/template": "npm:^7.24.0" + "@babel/traverse": "npm:^7.24.5" + "@babel/types": "npm:^7.24.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 03883300bf1252ab4c9ba5b52f161232dd52873dbe5cde9289bb2bb26e935c42682493acbac9194a59a3b6cbd17f4c4c84030db8d6d482588afe64531532ff9b + checksum: e26ba810a77bc8e21579a12fc36c79a0a60554404dc9447f2d64eb1f26d181c48d3b97d39d9f158e9911ec7162a8280acfaf2b4b210e975f0dd4bd4dbb1ee159 languageName: node linkType: hard -"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.23.6": - version: 7.23.6 - resolution: "@babel/generator@npm:7.23.6" +"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/generator@npm:7.24.5" dependencies: - "@babel/types": "npm:^7.23.6" - "@jridgewell/gen-mapping": "npm:^0.3.2" - "@jridgewell/trace-mapping": "npm:^0.3.17" + "@babel/types": "npm:^7.24.5" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^2.5.1" - checksum: 53540e905cd10db05d9aee0a5304e36927f455ce66f95d1253bb8a179f286b88fa7062ea0db354c566fe27f8bb96567566084ffd259f8feaae1de5eccc8afbda + checksum: 0d64f880150e7dfb92ceff2b4ac865f36aa1e295120920246492ffd0146562dabf79ba8699af1c8833f8a7954818d4d146b7b02f808df4d6024fb99f98b2f78d languageName: node linkType: hard @@ -201,7 +201,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.22.15, @babel/helper-compilation-targets@npm:^7.23.6": +"@babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.23.6": version: 7.23.6 resolution: "@babel/helper-compilation-targets@npm:7.23.6" dependencies: @@ -215,21 +215,21 @@ __metadata: linkType: hard "@babel/helper-create-class-features-plugin@npm:^7.18.6": - version: 7.23.10 - resolution: "@babel/helper-create-class-features-plugin@npm:7.23.10" + version: 7.24.5 + resolution: "@babel/helper-create-class-features-plugin@npm:7.24.5" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-member-expression-to-functions": "npm:^7.23.0" + "@babel/helper-member-expression-to-functions": "npm:^7.24.5" "@babel/helper-optimise-call-expression": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-replace-supers": "npm:^7.24.1" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/helper-split-export-declaration": "npm:^7.24.5" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: f30437aa16f3585cc3382ea630f24457ef622c22f5e4eccffbc03f6a81efbef0b6714fb5a78baa64c838884ba7e1427e3280d7b27481b9f587bc8fbbed05dd36 + checksum: afc72e8075a249663f8024ef1760de4c0b9252bdde16419ac955fa7e15b8d4096ca1e01f796df4fa8cfdb056708886f60b631ad492242a8e47307974fc305920 languageName: node linkType: hard @@ -259,36 +259,36 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.22.15, @babel/helper-member-expression-to-functions@npm:^7.23.0": - version: 7.23.0 - resolution: "@babel/helper-member-expression-to-functions@npm:7.23.0" +"@babel/helper-member-expression-to-functions@npm:^7.23.0, @babel/helper-member-expression-to-functions@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/helper-member-expression-to-functions@npm:7.24.5" dependencies: - "@babel/types": "npm:^7.23.0" - checksum: b810daddf093ffd0802f1429052349ed9ea08ef7d0c56da34ffbcdecbdafac86f95bdea2fe30e0e0e629febc7dd41b56cb5eacc10d1a44336d37b755dac31fa4 + "@babel/types": "npm:^7.24.5" + checksum: a3c0276a1ede8648a0e6fd86ad846cd57421d05eddfa29446b8b5a013db650462022b9ec1e65ea32c747d0542d729c80866830697f94fb12d603e87c51f080a5 languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-module-imports@npm:7.22.15" +"@babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.3": + version: 7.24.3 + resolution: "@babel/helper-module-imports@npm:7.24.3" dependencies: - "@babel/types": "npm:^7.22.15" - checksum: 4e0d7fc36d02c1b8c8b3006dfbfeedf7a367d3334a04934255de5128115ea0bafdeb3e5736a2559917f0653e4e437400d54542da0468e08d3cbc86d3bbfa8f30 + "@babel/types": "npm:^7.24.0" + checksum: 052c188adcd100f5e8b6ff0c9643ddaabc58b6700d3bbbc26804141ad68375a9f97d9d173658d373d31853019e65f62610239e3295cdd58e573bdcb2fded188d languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/helper-module-transforms@npm:7.23.3" +"@babel/helper-module-transforms@npm:^7.23.3, @babel/helper-module-transforms@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/helper-module-transforms@npm:7.24.5" dependencies: "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-module-imports": "npm:^7.22.15" - "@babel/helper-simple-access": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-module-imports": "npm:^7.24.3" + "@babel/helper-simple-access": "npm:^7.24.5" + "@babel/helper-split-export-declaration": "npm:^7.24.5" + "@babel/helper-validator-identifier": "npm:^7.24.5" peerDependencies: "@babel/core": ^7.0.0 - checksum: 211e1399d0c4993671e8e5c2b25383f08bee40004ace5404ed4065f0e9258cc85d99c1b82fd456c030ce5cfd4d8f310355b54ef35de9924eabfc3dff1331d946 + checksum: 6e77d72f62b7e87abaea800ea0bccd4d54cde26485750969f5f493c032eb63251eb50c3522cace557781565d51c1d0c4bcc866407d24becfb109c18fb92c978d languageName: node linkType: hard @@ -301,32 +301,32 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0": - version: 7.22.5 - resolution: "@babel/helper-plugin-utils@npm:7.22.5" - checksum: d2c4bfe2fa91058bcdee4f4e57a3f4933aed7af843acfd169cd6179fab8d13c1d636474ecabb2af107dc77462c7e893199aa26632bac1c6d7e025a17cbb9d20d +"@babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.0, @babel/helper-plugin-utils@npm:^7.24.5, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.24.5 + resolution: "@babel/helper-plugin-utils@npm:7.24.5" + checksum: 4ae40094e6a2f183281213344f4df60c66b16b19a2bc38d2bb11810a6dc0a0e7ec638957d0e433ff8b615775b8f3cd1b7edbf59440d1b50e73c389fc22913377 languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-replace-supers@npm:7.22.20" +"@babel/helper-replace-supers@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-replace-supers@npm:7.24.1" dependencies: "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-member-expression-to-functions": "npm:^7.22.15" + "@babel/helper-member-expression-to-functions": "npm:^7.23.0" "@babel/helper-optimise-call-expression": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0 - checksum: 6b0858811ad46873817c90c805015d63300e003c5a85c147a17d9845fa2558a02047c3cc1f07767af59014b2dd0fa75b503e5bc36e917f360e9b67bb6f1e79f4 + checksum: d39a3df7892b7c3c0e307fb229646168a9bd35e26a72080c2530729322600e8cff5f738f44a14860a2358faffa741b6a6a0d6749f113387b03ddbfa0ec10e1a0 languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-simple-access@npm:7.22.5" +"@babel/helper-simple-access@npm:^7.22.5, @babel/helper-simple-access@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/helper-simple-access@npm:7.24.5" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: f0cf81a30ba3d09a625fd50e5a9069e575c5b6719234e04ee74247057f8104beca89ed03e9217b6e9b0493434cedc18c5ecca4cea6244990836f1f893e140369 + "@babel/types": "npm:^7.24.5" + checksum: d96a0ab790a400f6c2dcbd9457b9ca74b9ba6d0f67ff9cd5bcc73792c8fbbd0847322a0dddbd8987dd98610ee1637c680938c7d83d3ffce7d06d7519d823d996 languageName: node linkType: hard @@ -339,26 +339,26 @@ __metadata: languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.22.6": - version: 7.22.6 - resolution: "@babel/helper-split-export-declaration@npm:7.22.6" +"@babel/helper-split-export-declaration@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/helper-split-export-declaration@npm:7.24.5" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: d83e4b623eaa9622c267d3c83583b72f3aac567dc393dda18e559d79187961cb29ae9c57b2664137fc3d19508370b12ec6a81d28af73a50e0846819cb21c6e44 + "@babel/types": "npm:^7.24.5" + checksum: d7a812d67d031a348f3fb0e6263ce2dbe6038f81536ba7fb16db385383bcd6542b71833194303bf6d3d0e4f7b6b584c9c8fae8772122e2ce68fc9bdf07f4135d languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/helper-string-parser@npm:7.23.4" - checksum: f348d5637ad70b6b54b026d6544bd9040f78d24e7ec245a0fc42293968181f6ae9879c22d89744730d246ce8ec53588f716f102addd4df8bbc79b73ea10004ac +"@babel/helper-string-parser@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-string-parser@npm:7.24.1" + checksum: 2f9bfcf8d2f9f083785df0501dbab92770111ece2f90d120352fda6dd2a7d47db11b807d111e6f32aa1ba6d763fe2dc6603d153068d672a5d0ad33ca802632b2 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-validator-identifier@npm:7.22.20" - checksum: dcad63db345fb110e032de46c3688384b0008a42a4845180ce7cd62b1a9c0507a1bed727c4d1060ed1a03ae57b4d918570259f81724aaac1a5b776056f37504e +"@babel/helper-validator-identifier@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/helper-validator-identifier@npm:7.24.5" + checksum: 05f957229d89ce95a137d04e27f7d0680d84ae48b6ad830e399db0779341f7d30290f863a93351b4b3bde2166737f73a286ea42856bb07c8ddaa95600d38645c languageName: node linkType: hard @@ -369,34 +369,35 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/helpers@npm:7.23.9" +"@babel/helpers@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/helpers@npm:7.24.5" dependencies: - "@babel/template": "npm:^7.23.9" - "@babel/traverse": "npm:^7.23.9" - "@babel/types": "npm:^7.23.9" - checksum: f69fd0aca96a6fb8bd6dd044cd8a5c0f1851072d4ce23355345b9493c4032e76d1217f86b70df795e127553cf7f3fcd1587ede9d1b03b95e8b62681ca2165b87 + "@babel/template": "npm:^7.24.0" + "@babel/traverse": "npm:^7.24.5" + "@babel/types": "npm:^7.24.5" + checksum: 0630b0223c3a9a34027ddc05b3bac54d68d5957f84e92d2d4814b00448a76e12f9188f9c85cfce2011696d82a8ffcbd8189da097c0af0181d32eb27eca34185e languageName: node linkType: hard -"@babel/highlight@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/highlight@npm:7.23.4" +"@babel/highlight@npm:^7.24.2": + version: 7.24.5 + resolution: "@babel/highlight@npm:7.24.5" dependencies: - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-validator-identifier": "npm:^7.24.5" chalk: "npm:^2.4.2" js-tokens: "npm:^4.0.0" - checksum: fbff9fcb2f5539289c3c097d130e852afd10d89a3a08ac0b5ebebbc055cc84a4bcc3dcfed463d488cde12dd0902ef1858279e31d7349b2e8cee43913744bda33 + picocolors: "npm:^1.0.0" + checksum: e98047d3ad24608bfa596d000c861a2cc875af897427f2833b91a4e0d4cead07301a7ec15fa26093dcd61e036e2eed2db338ae54f93016fe0dc785fadc4159db languageName: node linkType: hard -"@babel/parser@npm:^7.14.0, @babel/parser@npm:^7.16.8, @babel/parser@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/parser@npm:7.23.9" +"@babel/parser@npm:^7.14.0, @babel/parser@npm:^7.16.8, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/parser@npm:7.24.5" bin: parser: ./bin/babel-parser.js - checksum: 7df97386431366d4810538db4b9ec538f4377096f720c0591c7587a16f6810e62747e9fbbfa1ff99257fd4330035e4fb1b5b77c7bd3b97ce0d2e3780a6618975 + checksum: 8333a6ad5328bad34fa0e12bcee147c3345ea9a438c0909e7c68c6cfbea43c464834ffd7eabd1cbc1c62df0a558e22ffade9f5b29440833ba7b33d96a71f88c0 languageName: node linkType: hard @@ -438,36 +439,36 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-flow@npm:^7.0.0, @babel/plugin-syntax-flow@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-syntax-flow@npm:7.23.3" +"@babel/plugin-syntax-flow@npm:^7.0.0, @babel/plugin-syntax-flow@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-flow@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8a5e1e8b6a3728a2c8fe6d70c09a43642e737d9c0485e1b041cd3a6021ef05376ec3c9137be3b118c622ba09b5770d26fdc525473f8d06d4ab9e46de2783dd0a + checksum: 618de04360a96111408abdaafaba2efbaef0d90faad029d50e0281eaad5d7c7bd2ce4420bbac0ee27ad84c2b7bbc3e48f782064f81ed5bc40c398637991004c7 languageName: node linkType: hard "@babel/plugin-syntax-import-assertions@npm:^7.20.0": - version: 7.23.3 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 7db8b59f75667bada2293353bb66b9d5651a673b22c72f47da9f5c46e719142481601b745f9822212fd7522f92e26e8576af37116f85dae1b5e5967f80d0faab + checksum: 72f0340d73e037f0702c61670054e0af66ece7282c5c2f4ba8de059390fee502de282defdf15959cd9f71aa18dc5c5e4e7a0fde317799a0600c6c4e0a656d82b languageName: node linkType: hard "@babel/plugin-syntax-jsx@npm:^7.0.0, @babel/plugin-syntax-jsx@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-syntax-jsx@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-syntax-jsx@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 563bb7599b868773f1c7c1d441ecc9bc53aeb7832775da36752c926fc402a1fa5421505b39e724f71eb217c13e4b93117e081cac39723b0e11dac4c897f33c3e + checksum: 6cec76fbfe6ca81c9345c2904d8d9a8a0df222f9269f0962ed6eb2eb8f3f10c2f15e993d1ef09dbaf97726bf1792b5851cf5bd9a769f966a19448df6be95d19a languageName: node linkType: hard @@ -483,193 +484,193 @@ __metadata: linkType: hard "@babel/plugin-transform-arrow-functions@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-arrow-functions@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b128315c058f5728d29b0b78723659b11de88247ea4d0388f0b935cddf60a80c40b9067acf45cbbe055bd796928faef152a09d9e4a0695465aca4394d9f109ca + checksum: f44bfacf087dc21b422bab99f4e9344ee7b695b05c947dacae66de05c723ab9d91800be7edc1fa016185e8c819f3aca2b4a5f66d8a4d1e47d9bad80b8fa55b8e languageName: node linkType: hard "@babel/plugin-transform-block-scoped-functions@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 82c12a11277528184a979163de7189ceb00129f60dd930b0d5313454310bf71205f302fb2bf0430247161c8a22aaa9fb9eec1459f9f7468206422c191978fd59 + checksum: 6fbaa85f5204f34845dfc0bebf62fdd3ac5a286241c85651e59d426001e7a1785ac501f154e093e0b8ee49e1f51e3f8b06575a5ae8d4a9406d43e4816bf18c37 languageName: node linkType: hard "@babel/plugin-transform-block-scoping@npm:^7.0.0": - version: 7.23.4 - resolution: "@babel/plugin-transform-block-scoping@npm:7.23.4" + version: 7.24.5 + resolution: "@babel/plugin-transform-block-scoping@npm:7.24.5" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 83006804dddf980ab1bcd6d67bc381e24b58c776507c34f990468f820d0da71dba3697355ca4856532fa2eeb2a1e3e73c780f03760b5507a511cbedb0308e276 + checksum: 85997fc8179b7d26e8af30865aeb91789f3bc1f0cd5643ed25f25891ff9c071460ec1220599b19070b424a3b902422f682e9b02e515872540173eae2e25f760c languageName: node linkType: hard "@babel/plugin-transform-classes@npm:^7.0.0": - version: 7.23.8 - resolution: "@babel/plugin-transform-classes@npm:7.23.8" + version: 7.24.5 + resolution: "@babel/plugin-transform-classes@npm:7.24.5" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-compilation-targets": "npm:^7.23.6" "@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.22.20" - "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/helper-plugin-utils": "npm:^7.24.5" + "@babel/helper-replace-supers": "npm:^7.24.1" + "@babel/helper-split-export-declaration": "npm:^7.24.5" globals: "npm:^11.1.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 227ac5166501e04d9e7fbd5eda6869b084ffa4af6830ac12544ac6ea14953ca00eb1762b0df9349c0f6c8d2a799385910f558066cd0fb85b9ca437b1131a6043 + checksum: 4affcbb7cb01fa4764c7a4b534c30fd24a4b68e680a2d6e242dd7ca8726490f0f1426c44797deff84a38a162e0629718900c68d28daffe2b12adf5b4194156a7 languageName: node linkType: hard "@babel/plugin-transform-computed-properties@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-computed-properties@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-computed-properties@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/template": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/template": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3ca8a006f8e652b58c21ecb84df1d01a73f0a96b1d216fd09a890b235dd90cb966b152b603b88f7e850ae238644b1636ce5c30b7c029c0934b43383932372e4a + checksum: 8292c508b656b7722e2c2ca0f6f31339852e3ed2b9b80f6e068a4010e961b431ca109ecd467fc906283f4b1574c1e7b1cb68d35a4dea12079d386c15ff7e0eac languageName: node linkType: hard "@babel/plugin-transform-destructuring@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-destructuring@npm:7.23.3" + version: 7.24.5 + resolution: "@babel/plugin-transform-destructuring@npm:7.24.5" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 717e9a62c1b0c93c507f87b4eaf839ec08d3c3147f14d74ae240d8749488d9762a8b3950132be620a069bde70f4b3e4ee9867b226c973fcc40f3cdec975cde71 + checksum: 6a37953a95f04b335bf3e2118fb93f50dd9593c658d1b2f8918a380a2ee30f1b420139eccf7ec3873c86a8208527895fcf6b7e21c0e734a6ad6e5d5042eace4d languageName: node linkType: hard "@babel/plugin-transform-flow-strip-types@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-flow-strip-types@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/plugin-syntax-flow": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/plugin-syntax-flow": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 9ab627f9668fc1f95564b26bffd6706f86205960d9ccc168236752fbef65dbe10aa0ce74faae12f48bb3b72ec7f38ef2a78b4874c222c1e85754e981639f3b33 + checksum: e6aa9cbad0441867598d390d4df65bc8c6b797574673e4eedbdae0cc528e81e00f4b2cd38f7d138b0f04bcdd2540384a9812d5d76af5abfa06aee1c7fc20ca58 languageName: node linkType: hard "@babel/plugin-transform-for-of@npm:^7.0.0": - version: 7.23.6 - resolution: "@babel/plugin-transform-for-of@npm:7.23.6" + version: 7.24.1 + resolution: "@babel/plugin-transform-for-of@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 46681b6ab10f3ca2d961f50d4096b62ab5d551e1adad84e64be1ee23e72eb2f26a1e30e617e853c74f1349fffe4af68d33921a128543b6f24b6d46c09a3e2aec + checksum: e4bc92b1f334246e62d4bde079938df940794db564742034f6597f2e38bd426e11ae8c5670448e15dd6e45c462f2a9ab3fa87259bddf7c08553ffd9457fc2b2c languageName: node linkType: hard "@babel/plugin-transform-function-name@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-function-name@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-function-name@npm:7.24.1" dependencies: - "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-compilation-targets": "npm:^7.23.6" "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 89cb9747802118048115cf92a8f310752f02030549b26f008904990cbdc86c3d4a68e07ca3b5c46de8a46ed4df2cb576ac222c74c56de67253d2a3ddc2956083 + checksum: 65c1735ec3b5e43db9b5aebf3c16171c04b3050c92396b9e22dda0d2aaf51f43fdcf147f70a40678fd9a4ee2272a5acec4826e9c21bcf968762f4c184897ad75 languageName: node linkType: hard "@babel/plugin-transform-literals@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-literals@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-literals@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8292106b106201464c2bfdd5c014fe6a9ca1c0256eb0a8031deb20081e21906fe68b156186f77d993c23eeab6d8d6f5f66e8895eec7ed97ce6de5dbcafbcd7f4 + checksum: a27cc7d565ee57b5a2bf136fa889c5c2f5988545ae7b3b2c83a7afe5dd37dfac80dca88b1c633c65851ce6af7d2095c04c01228657ce0198f918e64b5ccd01fa languageName: node linkType: hard "@babel/plugin-transform-member-expression-literals@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-member-expression-literals@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 687f24f3ec60b627fef6e87b9e2770df77f76727b9d5f54fa4c84a495bb24eb4a20f1a6240fa22d339d45aac5eaeb1b39882e941bfd00cf498f9c53478d1ec88 + checksum: 2af731d02aa4c757ef80c46df42264128cbe45bfd15e1812d1a595265b690a44ad036041c406a73411733540e1c4256d8174705ae6b8cfaf757fc175613993fd languageName: node linkType: hard "@babel/plugin-transform-modules-commonjs@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.1" dependencies: "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-simple-access": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 5c8840c5c9ecba39367ae17c973ed13dbc43234147b77ae780eec65010e2a9993c5d717721b23e8179f7cf49decdd325c509b241d69cfbf92aa647a1d8d5a37d + checksum: efb3ea2047604a7eb44a9289311ebb29842fe6510ff8b66a77a60440448c65e1312a60dc48191ed98246bdbd163b5b6f3348a0669bcc0e3809e69c7c776b20fa languageName: node linkType: hard "@babel/plugin-transform-object-super@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-object-super@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-object-super@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-replace-supers": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: a6856fd8c0afbe5b3318c344d4d201d009f4051e2f6ff6237ff2660593e93c5997a58772b13d639077c3e29ced3440247b29c496cd77b13af1e7559a70009775 + checksum: d30e6b9e59a707efd7ed524fc0a8deeea046011a6990250f2e9280516683138e2d13d9c52daf41d78407bdab0378aef7478326f2a15305b773d851cb6e106157 languageName: node linkType: hard "@babel/plugin-transform-parameters@npm:^7.0.0, @babel/plugin-transform-parameters@npm:^7.20.7": - version: 7.23.3 - resolution: "@babel/plugin-transform-parameters@npm:7.23.3" + version: 7.24.5 + resolution: "@babel/plugin-transform-parameters@npm:7.24.5" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: a8d4cbe0f6ba68d158f5b4215c63004fc37a1fdc539036eb388a9792017c8496ea970a1932ccb929308f61e53dc56676ed01d8df6f42bc0a85c7fd5ba82482b7 + checksum: e08b8c46a24b1b21dde7783cb0aeb56ffe9ef6d6f1795649ce76273657158d3bfa5370c6594200ed7d371983b599c8e194b76108dffed9ab5746fe630ef2e8f5 languageName: node linkType: hard "@babel/plugin-transform-property-literals@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-property-literals@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-property-literals@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b2549f23f90cf276c2e3058c2225c3711c2ad1c417e336d3391199445a9776dd791b83be47b2b9a7ae374b40652d74b822387e31fa5267a37bf49c122e1a9747 + checksum: 3bf3e01f7bb8215a8b6d0081b6f86fea23e3a4543b619e059a264ede028bc58cdfb0acb2c43271271915a74917effa547bc280ac636a9901fa9f2fb45623f87e languageName: node linkType: hard "@babel/plugin-transform-react-display-name@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-react-display-name@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-react-display-name@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3aed142af7bd1aed1df2bdad91ed33ba1cdd5c3c67ce6eafba821ff72f129162a197ffb55f1eb1775af276abd5545934489a8257fef6c6665ddf253a4f39a939 + checksum: adf1a3cb0df8134533a558a9072a67e34127fd489dfe431c3348a86dd41f3e74861d5d5134bbb68f61a9cdb3f7e79b2acea1346be94ce4d3328a64e5a9e09be1 languageName: node linkType: hard @@ -689,85 +690,85 @@ __metadata: linkType: hard "@babel/plugin-transform-shorthand-properties@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-shorthand-properties@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: c423c66fec0b6503f50561741754c84366ef9e9818442c8881fbaa90cc363fd137084b9431cdc00ed2f1fd8c8a1a5982c4a7e1f2af3769db4caf2ac7ea55d4f0 + checksum: 8273347621183aada3cf1f3019d8d5f29467ba13a75b72cb405bc7f23b7e05fd85f4edb1e4d9f0103153dddb61826a42dc24d466480d707f8932c1923a4c25fa languageName: node linkType: hard "@babel/plugin-transform-spread@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-spread@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-spread@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: a348e4ae47e4ceeceb760506ec7bf835ccc18a2cf70ec74ebfbe41bc172fa2412b05b7d1b86836f8aee375e41a04ff20486074778d0e2d19d668b33dc52e9dbb + checksum: 50a0302e344546d57e5c9f4dea575f88e084352eeac4e9a3e238c41739eef2df1daf4a7ebbb3ccb7acd3447f6a5ce9938405f98bf5f5583deceb8257f5a673c9 languageName: node linkType: hard "@babel/plugin-transform-template-literals@npm:^7.0.0": - version: 7.23.3 - resolution: "@babel/plugin-transform-template-literals@npm:7.23.3" + version: 7.24.1 + resolution: "@babel/plugin-transform-template-literals@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 9b5f43788b9ffcb8f2b445a16b1aa40fcf23cb0446a4649445f098ec6b4cb751f243a535da623d59fefe48f4c40552f5621187a61811779076bab26863e3373d + checksum: f73bcda5488eb81c6e7a876498d9e6b72be32fca5a4d9db9053491a2d1300cd27b889b463fd2558f3cd5826a85ed00f61d81b234aa55cb5a0abf1b6fa1bd5026 languageName: node linkType: hard "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.5.5": - version: 7.23.9 - resolution: "@babel/runtime@npm:7.23.9" + version: 7.24.5 + resolution: "@babel/runtime@npm:7.24.5" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: e71205fdd7082b2656512cc98e647d9ea7e222e4fe5c36e9e5adc026446fcc3ba7b3cdff8b0b694a0b78bb85db83e7b1e3d4c56ef90726682b74f13249cf952d + checksum: 05730e43e8ba6550eae9fd4fb5e7d9d3cb91140379425abcb2a1ff9cebad518a280d82c4c4b0f57ada26a863106ac54a748d90c775790c0e2cd0ddd85ccdf346 languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/template@npm:7.23.9" +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/template@npm:7.24.0" dependencies: "@babel/code-frame": "npm:^7.23.5" - "@babel/parser": "npm:^7.23.9" - "@babel/types": "npm:^7.23.9" - checksum: 0e8b60119433787742bc08ae762bbd8d6755611c4cabbcb7627b292ec901a55af65d93d1c88572326069efb64136ef151ec91ffb74b2df7689bbab237030833a + "@babel/parser": "npm:^7.24.0" + "@babel/types": "npm:^7.24.0" + checksum: 9d3dd8d22fe1c36bc3bdef6118af1f4b030aaf6d7d2619f5da203efa818a2185d717523486c111de8d99a8649ddf4bbf6b2a7a64962d8411cf6a8fa89f010e54 languageName: node linkType: hard -"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/traverse@npm:7.23.9" +"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.24.5": + version: 7.24.5 + resolution: "@babel/traverse@npm:7.24.5" dependencies: - "@babel/code-frame": "npm:^7.23.5" - "@babel/generator": "npm:^7.23.6" + "@babel/code-frame": "npm:^7.24.2" + "@babel/generator": "npm:^7.24.5" "@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-function-name": "npm:^7.23.0" "@babel/helper-hoist-variables": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/parser": "npm:^7.23.9" - "@babel/types": "npm:^7.23.9" + "@babel/helper-split-export-declaration": "npm:^7.24.5" + "@babel/parser": "npm:^7.24.5" + "@babel/types": "npm:^7.24.5" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: d1615d1d02f04d47111a7ea4446a1a6275668ca39082f31d51f08380de9502e19862be434eaa34b022ce9a17dbb8f9e2b73a746c654d9575f3a680a7ffdf5630 + checksum: 3f22534bc2b2ed9208e55ef48af3b32939032b23cb9dc4037447cb108640df70bbb0b9fea86e9c58648949fdc2cb14e89aa79ffa3c62a5dd43459a52fe8c01d1 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.8.3": - version: 7.23.9 - resolution: "@babel/types@npm:7.23.9" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.5, @babel/types@npm:^7.8.3": + version: 7.24.5 + resolution: "@babel/types@npm:7.24.5" dependencies: - "@babel/helper-string-parser": "npm:^7.23.4" - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-string-parser": "npm:^7.24.1" + "@babel/helper-validator-identifier": "npm:^7.24.5" to-fast-properties: "npm:^2.0.0" - checksum: edc7bb180ce7e4d2aea10c6972fb10474341ac39ba8fdc4a27ffb328368dfdfbf40fca18e441bbe7c483774500d5c05e222cec276c242e952853dcaf4eb884f7 + checksum: e1284eb046c5e0451b80220d1200e2327e0a8544a2fe45bb62c952e5fdef7099c603d2336b17b6eac3cc046b7a69bfbce67fe56e1c0ea48cd37c65cb88638f2a languageName: node linkType: hard @@ -817,9 +818,9 @@ __metadata: languageName: node linkType: hard -"@changesets/apply-release-plan@npm:^7.0.0": - version: 7.0.0 - resolution: "@changesets/apply-release-plan@npm:7.0.0" +"@changesets/apply-release-plan@npm:^7.0.1": + version: 7.0.1 + resolution: "@changesets/apply-release-plan@npm:7.0.1" dependencies: "@babel/runtime": "npm:^7.20.1" "@changesets/config": "npm:^3.0.0" @@ -834,7 +835,7 @@ __metadata: prettier: "npm:^2.7.1" resolve-from: "npm:^5.0.0" semver: "npm:^7.5.3" - checksum: 5f4c2d6b500d0ade51b31bc03b2475dd0bcaf3a31995f2ad953a6c3b05d3fb588568470bad3093d052f351ecdc6f8e2124d38941210361692b81bf62afbba7d7 + checksum: ca41f84a22a1fd25af4b195956afd393000139581a84528b6d21871e4ce417abf884ad2ef8a8ed7abb49031fed7cbb69226a158df36050aa70ed295636a42369 languageName: node linkType: hard @@ -862,11 +863,11 @@ __metadata: linkType: hard "@changesets/cli@npm:^2.27.1": - version: 2.27.1 - resolution: "@changesets/cli@npm:2.27.1" + version: 2.27.3 + resolution: "@changesets/cli@npm:2.27.3" dependencies: "@babel/runtime": "npm:^7.20.1" - "@changesets/apply-release-plan": "npm:^7.0.0" + "@changesets/apply-release-plan": "npm:^7.0.1" "@changesets/assemble-release-plan": "npm:^6.0.0" "@changesets/changelog-git": "npm:^0.2.0" "@changesets/config": "npm:^3.0.0" @@ -878,7 +879,7 @@ __metadata: "@changesets/pre": "npm:^2.0.0" "@changesets/read": "npm:^0.6.0" "@changesets/types": "npm:^6.0.0" - "@changesets/write": "npm:^0.3.0" + "@changesets/write": "npm:^0.3.1" "@manypkg/get-packages": "npm:^1.1.3" "@types/semver": "npm:^7.5.0" ansi-colors: "npm:^4.1.3" @@ -899,7 +900,7 @@ __metadata: tty-table: "npm:^4.1.5" bin: changeset: bin.js - checksum: c7adc35f22983be9b0f6a8e4c3bc7013208ddf341b637530b88267e78469f0b7af9e36b138bea9f2fe29bb7b44294cd08aa0301a5cba0c6a928824f11d024e04 + checksum: fee10ae204926dbf4847111bf49ca93dc5b43a55b48635e2e08e71ab8442cf414c8165ae11174d6366ef12305274ea1b75beb14ca74b9a766d82b8c32148199b languageName: node linkType: hard @@ -1039,16 +1040,16 @@ __metadata: languageName: node linkType: hard -"@changesets/write@npm:^0.3.0": - version: 0.3.0 - resolution: "@changesets/write@npm:0.3.0" +"@changesets/write@npm:^0.3.1": + version: 0.3.1 + resolution: "@changesets/write@npm:0.3.1" dependencies: "@babel/runtime": "npm:^7.20.1" "@changesets/types": "npm:^6.0.0" fs-extra: "npm:^7.0.1" human-id: "npm:^1.0.2" prettier: "npm:^2.7.1" - checksum: 537f419d854946cce5694696b6a48ffee0ea1f7b5c97c5246836931886db18153c42a7dea1e74b0e8bf571fcded527e2f443ab362fdb1e4129bd95a61b2d0fe5 + checksum: 6c6ef4c12f93ae10706eea96fae73ab05fddeaa1870102681106a29e4e92c37be9643f214c56187141ab5cf3a4cccb4e8a59212d0fa6c7c26083c5d613878c9a languageName: node linkType: hard @@ -1115,12 +1116,12 @@ __metadata: linkType: hard "@commitlint/config-conventional@npm:^18.4.3": - version: 18.6.2 - resolution: "@commitlint/config-conventional@npm:18.6.2" + version: 18.6.3 + resolution: "@commitlint/config-conventional@npm:18.6.3" dependencies: "@commitlint/types": "npm:^18.6.1" conventional-changelog-conventionalcommits: "npm:^7.0.2" - checksum: ff4ccff3c2992c209703eb7d08f8e1c6d8471d4f0778f384dc0fef490cc023227f1b662f7136a301804d650518e00c7f859aa3eb1a156448f837b2a50206430d + checksum: 047f84598f80f7f793bdb0ffc9cf9059c199da6c5bc12ab87084fa933faee08c9290e3331f6f0d7e07c4f0ffb0b5c678e5036025aeabb8e74af296b9146c6354 languageName: node linkType: hard @@ -1468,7 +1469,20 @@ __metadata: languageName: node linkType: hard -"@defi-wonderland/smock@npm:^2.0.7": +"@defi-wonderland/natspec-smells@npm:^1.1.6": + version: 1.1.6 + resolution: "@defi-wonderland/natspec-smells@npm:1.1.6" + dependencies: + fast-glob: "npm:3.3.2" + solc-typed-ast: "npm:18.2.4" + yargs: "npm:17.7.2" + bin: + natspec-smells: lib/main.js + checksum: 7c0cad96a7b7885387cd32bc3eb48fad9c8fddb78b42d36fe923d494a09dc18a6ba9951c641ad1c1d94829d6c6f0a759c590235ae208ff5b562a2e9582d02b7b + languageName: node + linkType: hard + +"@defi-wonderland/smock@npm:~2.3.0": version: 2.3.5 resolution: "@defi-wonderland/smock@npm:2.3.5" dependencies: @@ -1568,7 +1582,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": version: 4.10.0 resolution: "@eslint-community/regexpp@npm:4.10.0" checksum: c5f60ef1f1ea7649fa7af0e80a5a79f64b55a8a8fa5086de4727eb4c86c652aedee407a9c143b8995d2c0b2d75c1222bec9ba5d73dbfc1f314550554f0979ef4 @@ -1592,13 +1606,6 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.56.0": - version: 8.56.0 - resolution: "@eslint/js@npm:8.56.0" - checksum: 60b3a1cf240e2479cec9742424224465dc50e46d781da1b7f5ef240501b2d1202c225bd456207faac4b34a64f4765833345bc4ddffd00395e1db40fa8c426f5a - languageName: node - linkType: hard - "@eslint/js@npm:8.57.0": version: 8.57.0 resolution: "@eslint/js@npm:8.57.0" @@ -1763,7 +1770,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.0, @ethersproject/abi@npm:^5.1.2, @ethersproject/abi@npm:^5.5.0, @ethersproject/abi@npm:^5.6.0, @ethersproject/abi@npm:^5.6.3, @ethersproject/abi@npm:^5.7.0": +"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.0, @ethersproject/abi@npm:^5.0.9, @ethersproject/abi@npm:^5.1.2, @ethersproject/abi@npm:^5.5.0, @ethersproject/abi@npm:^5.6.0, @ethersproject/abi@npm:^5.6.3, @ethersproject/abi@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/abi@npm:5.7.0" dependencies: @@ -1849,6 +1856,19 @@ __metadata: languageName: node linkType: hard +"@ethersproject/address@npm:5.6.1": + version: 5.6.1 + resolution: "@ethersproject/address@npm:5.6.1" + dependencies: + "@ethersproject/bignumber": "npm:^5.6.2" + "@ethersproject/bytes": "npm:^5.6.1" + "@ethersproject/keccak256": "npm:^5.6.1" + "@ethersproject/logger": "npm:^5.6.0" + "@ethersproject/rlp": "npm:^5.6.1" + checksum: 7ac29a0abcb9970c6f5f9a2d2e8247d3c433ee9a022861cbf4f8d437f095a5293f3d323b8ec3433df622364071232b227248f1ac04c4ddea353bf18e2e4d76cf + languageName: node + linkType: hard + "@ethersproject/address@npm:5.7.0, @ethersproject/address@npm:>=5.0.0-beta.128, @ethersproject/address@npm:^5.0.0, @ethersproject/address@npm:^5.0.2, @ethersproject/address@npm:^5.0.8, @ethersproject/address@npm:^5.6.0, @ethersproject/address@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/address@npm:5.7.0" @@ -1911,7 +1931,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/bignumber@npm:5.7.0, @ethersproject/bignumber@npm:>=5.0.0-beta.130, @ethersproject/bignumber@npm:^5.0.0, @ethersproject/bignumber@npm:^5.1.1, @ethersproject/bignumber@npm:^5.6.0, @ethersproject/bignumber@npm:^5.7.0": +"@ethersproject/bignumber@npm:5.7.0, @ethersproject/bignumber@npm:>=5.0.0-beta.130, @ethersproject/bignumber@npm:^5.0.0, @ethersproject/bignumber@npm:^5.1.1, @ethersproject/bignumber@npm:^5.6.0, @ethersproject/bignumber@npm:^5.6.2, @ethersproject/bignumber@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/bignumber@npm:5.7.0" dependencies: @@ -1931,7 +1951,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/bytes@npm:5.7.0, @ethersproject/bytes@npm:>=5.0.0-beta.129, @ethersproject/bytes@npm:^5.0.0, @ethersproject/bytes@npm:^5.0.8, @ethersproject/bytes@npm:^5.6.0, @ethersproject/bytes@npm:^5.7.0": +"@ethersproject/bytes@npm:5.7.0, @ethersproject/bytes@npm:>=5.0.0-beta.129, @ethersproject/bytes@npm:^5.0.0, @ethersproject/bytes@npm:^5.0.8, @ethersproject/bytes@npm:^5.6.0, @ethersproject/bytes@npm:^5.6.1, @ethersproject/bytes@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/bytes@npm:5.7.0" dependencies: @@ -2130,7 +2150,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/keccak256@npm:5.7.0, @ethersproject/keccak256@npm:>=5.0.0-beta.127, @ethersproject/keccak256@npm:^5.6.0, @ethersproject/keccak256@npm:^5.7.0": +"@ethersproject/keccak256@npm:5.7.0, @ethersproject/keccak256@npm:>=5.0.0-beta.127, @ethersproject/keccak256@npm:^5.6.0, @ethersproject/keccak256@npm:^5.6.1, @ethersproject/keccak256@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/keccak256@npm:5.7.0" dependencies: @@ -2332,7 +2352,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/rlp@npm:5.7.0, @ethersproject/rlp@npm:^5.6.0, @ethersproject/rlp@npm:^5.7.0": +"@ethersproject/rlp@npm:5.7.0, @ethersproject/rlp@npm:^5.6.0, @ethersproject/rlp@npm:^5.6.1, @ethersproject/rlp@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/rlp@npm:5.7.0" dependencies: @@ -2610,9 +2630,9 @@ __metadata: linkType: hard "@fastify/busboy@npm:^2.0.0": - version: 2.1.0 - resolution: "@fastify/busboy@npm:2.1.0" - checksum: 7bb641080aac7cf01d88749ad331af10ba9ec3713ec07cabbe833908c75df21bd56249bb6173bdec07f5a41896b21e3689316f86684c06635da45f91ff4565a2 + version: 2.1.1 + resolution: "@fastify/busboy@npm:2.1.1" + checksum: 6f8027a8cba7f8f7b736718b013f5a38c0476eea67034c94a0d3c375e2b114366ad4419e6a6fa7ffc2ef9c6d3e0435d76dd584a7a1cbac23962fda7650b579e3 languageName: node linkType: hard @@ -2795,7 +2815,7 @@ __metadata: languageName: node linkType: hard -"@graphprotocol/contracts@npm:5.3.3, @graphprotocol/contracts@npm:^5.0.0": +"@graphprotocol/contracts@npm:5.3.3": version: 5.3.3 resolution: "@graphprotocol/contracts@npm:5.3.3" dependencies: @@ -2805,12 +2825,12 @@ __metadata: languageName: node linkType: hard -"@graphprotocol/contracts@workspace:^6.2.0, @graphprotocol/contracts@workspace:packages/contracts": +"@graphprotocol/contracts@workspace:^7.0.0, @graphprotocol/contracts@workspace:packages/contracts": version: 0.0.0-use.local resolution: "@graphprotocol/contracts@workspace:packages/contracts" dependencies: "@arbitrum/sdk": "npm:~3.1.13" - "@defi-wonderland/smock": "npm:^2.0.7" + "@defi-wonderland/smock": "npm:~2.3.0" "@ethersproject/experimental": "npm:^5.6.0" "@graphprotocol/common-ts": "npm:^1.8.3" "@graphprotocol/sdk": "workspace:^0.5.0" @@ -2866,10 +2886,11 @@ __metadata: prettier-plugin-solidity: "npm:^1.3.1" solhint: "npm:^4.1.1" solhint-graph-config: "workspace:^0.0.1" + solhint-plugin-graph: "workspace:^0.0.1" solidity-coverage: "npm:^0.7.16" ts-node: "npm:^10.9.1" typechain: "npm:^5.0.0" - typescript: "npm:^4.7.4" + typescript: "npm:^5.2.2" winston: "npm:^3.3.3" yaml: "npm:^1.10.2" yargs: "npm:^17.0.0" @@ -2917,6 +2938,51 @@ __metadata: languageName: unknown linkType: soft +"@graphprotocol/horizon@workspace:^0.0.1, @graphprotocol/horizon@workspace:packages/horizon": + version: 0.0.0-use.local + resolution: "@graphprotocol/horizon@workspace:packages/horizon" + dependencies: + "@defi-wonderland/natspec-smells": "npm:^1.1.6" + "@graphprotocol/contracts": "workspace:^7.0.0" + "@nomicfoundation/hardhat-chai-matchers": "npm:^2.0.0" + "@nomicfoundation/hardhat-ethers": "npm:^3.0.8" + "@nomicfoundation/hardhat-foundry": "npm:^1.1.1" + "@nomicfoundation/hardhat-ignition": "npm:^0.15.9" + "@nomicfoundation/hardhat-ignition-ethers": "npm:^0.15.9" + "@nomicfoundation/hardhat-network-helpers": "npm:^1.0.0" + "@nomicfoundation/hardhat-toolbox": "npm:^4.0.0" + "@nomicfoundation/hardhat-verify": "npm:^2.0.10" + "@nomicfoundation/ignition-core": "npm:^0.15.9" + "@openzeppelin/contracts": "npm:^5.0.2" + "@openzeppelin/contracts-upgradeable": "npm:^5.0.2" + "@typechain/ethers-v6": "npm:^0.5.0" + "@typechain/hardhat": "npm:^9.0.0" + "@types/chai": "npm:^4.2.0" + "@types/mocha": "npm:>=9.1.0" + "@types/node": "npm:>=16.0.0" + chai: "npm:^4.2.0" + eslint: "npm:^8.56.0" + eslint-graph-config: "workspace:^0.0.1" + ethers: "npm:^6.13.4" + hardhat: "npm:^2.22.18" + hardhat-contract-sizer: "npm:^2.10.0" + hardhat-gas-reporter: "npm:^1.0.8" + hardhat-graph-protocol: "workspace:^0.0.1" + hardhat-secure-accounts: "npm:^1.0.5" + hardhat-storage-layout: "npm:^0.1.7" + lint-staged: "npm:^15.2.2" + prettier: "npm:^3.2.5" + prettier-plugin-solidity: "npm:^1.3.1" + solhint: "npm:^4.5.2" + solhint-graph-config: "workspace:^0.0.1" + solhint-plugin-graph: "workspace:^0.0.1" + solidity-coverage: "npm:^0.8.0" + ts-node: "npm:>=8.0.0" + typechain: "npm:^8.3.0" + typescript: "npm:^5.6.3" + languageName: unknown + linkType: soft + "@graphprotocol/pino-sentry-simple@npm:0.7.1": version: 0.7.1 resolution: "@graphprotocol/pino-sentry-simple@npm:0.7.1" @@ -2936,7 +3002,7 @@ __metadata: "@arbitrum/sdk": "npm:~3.1.13" "@ethersproject/experimental": "npm:^5.7.0" "@graphprotocol/common-ts": "npm:^2.0.7" - "@graphprotocol/contracts": "workspace:^6.2.0" + "@graphprotocol/contracts": "workspace:^7.0.0" "@nomicfoundation/hardhat-network-helpers": "npm:^1.0.9" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@types/chai": "npm:^4.3.9" @@ -2967,13 +3033,61 @@ __metadata: languageName: unknown linkType: soft +"@graphprotocol/subgraph-service@workspace:^0.0.1, @graphprotocol/subgraph-service@workspace:packages/subgraph-service": + version: 0.0.0-use.local + resolution: "@graphprotocol/subgraph-service@workspace:packages/subgraph-service" + dependencies: + "@defi-wonderland/natspec-smells": "npm:^1.1.6" + "@graphprotocol/contracts": "workspace:^7.0.0" + "@graphprotocol/horizon": "workspace:^0.0.1" + "@nomicfoundation/hardhat-chai-matchers": "npm:^2.0.0" + "@nomicfoundation/hardhat-ethers": "npm:^3.0.8" + "@nomicfoundation/hardhat-foundry": "npm:^1.1.1" + "@nomicfoundation/hardhat-ignition": "npm:^0.15.9" + "@nomicfoundation/hardhat-ignition-ethers": "npm:^0.15.9" + "@nomicfoundation/hardhat-network-helpers": "npm:^1.0.0" + "@nomicfoundation/hardhat-toolbox": "npm:^4.0.0" + "@nomicfoundation/hardhat-verify": "npm:^2.0.10" + "@nomicfoundation/ignition-core": "npm:^0.15.9" + "@openzeppelin/contracts": "npm:^5.0.2" + "@openzeppelin/contracts-upgradeable": "npm:^5.0.2" + "@typechain/ethers-v6": "npm:^0.5.0" + "@typechain/hardhat": "npm:^9.0.0" + "@types/chai": "npm:^4.2.0" + "@types/mocha": "npm:>=9.1.0" + "@types/node": "npm:>=16.0.0" + chai: "npm:^4.2.0" + eslint: "npm:^8.56.0" + eslint-graph-config: "workspace:^0.0.1" + ethers: "npm:^6.13.4" + hardhat: "npm:^2.22.18" + hardhat-contract-sizer: "npm:^2.10.0" + hardhat-gas-reporter: "npm:^1.0.8" + hardhat-graph-protocol: "workspace:^0.0.1" + hardhat-secure-accounts: "npm:^1.0.5" + hardhat-storage-layout: "npm:^0.1.7" + json5: "npm:^2.2.3" + lint-staged: "npm:^15.2.2" + prettier: "npm:^3.2.5" + prettier-plugin-solidity: "npm:^1.3.1" + solhint: "npm:^4.5.4" + solhint-graph-config: "workspace:^0.0.1" + solhint-plugin-graph: "workspace:^0.0.1" + solidity-coverage: "npm:^0.8.0" + solidity-docgen: "npm:^0.6.0-beta.36" + ts-node: "npm:>=8.0.0" + typechain: "npm:^8.3.0" + typescript: "npm:^5.3.3" + languageName: unknown + linkType: soft + "@graphprotocol/token-distribution@workspace:packages/token-distribution": version: 0.0.0-use.local resolution: "@graphprotocol/token-distribution@workspace:packages/token-distribution" dependencies: "@ethersproject/experimental": "npm:^5.0.7" "@graphprotocol/client-cli": "npm:^2.0.2" - "@graphprotocol/contracts": "npm:^5.0.0" + "@graphprotocol/contracts": "workspace:^7.0.0" "@nomiclabs/hardhat-ethers": "npm:^2.0.0" "@nomiclabs/hardhat-etherscan": "npm:^3.1.7" "@nomiclabs/hardhat-waffle": "npm:^2.0.0" @@ -3007,7 +3121,7 @@ __metadata: solhint-plugin-prettier: "npm:^0.1.0" ts-node: "npm:^10.9.1" typechain: "npm:^5.0.0" - typescript: "npm:^4.0.2" + typescript: "npm:^5.2.2" languageName: unknown linkType: soft @@ -3879,7 +3993,7 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.13, @humanwhocodes/config-array@npm:^0.11.14": +"@humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" dependencies: @@ -3898,9 +4012,9 @@ __metadata: linkType: hard "@humanwhocodes/object-schema@npm:^2.0.2": - version: 2.0.2 - resolution: "@humanwhocodes/object-schema@npm:2.0.2" - checksum: 6fd83dc320231d71c4541d0244051df61f301817e9f9da9fd4cb7e44ec8aacbde5958c1665b0c419401ab935114fdf532a6ad5d4e7294b1af2f347dd91a6983f + version: 2.0.3 + resolution: "@humanwhocodes/object-schema@npm:2.0.3" + checksum: 80520eabbfc2d32fe195a93557cef50dfe8c8905de447f022675aaf66abc33ae54098f5ea78548d925aa671cd4ab7c7daa5ad704fe42358c9b5e7db60f80696c languageName: node linkType: hard @@ -3918,14 +4032,14 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": - version: 0.3.3 - resolution: "@jridgewell/gen-mapping@npm:0.3.3" +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" dependencies: - "@jridgewell/set-array": "npm:^1.0.1" + "@jridgewell/set-array": "npm:^1.2.1" "@jridgewell/sourcemap-codec": "npm:^1.4.10" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 376fc11cf5a967318ba3ddd9d8e91be528eab6af66810a713c49b0c3f8dc67e9949452c51c38ab1b19aa618fb5e8594da5a249977e26b1e7fea1ee5a1fcacc74 + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 1be4fd4a6b0f41337c4f5fdf4afc3bd19e39c3691924817108b82ffcb9c9e609c273f936932b9fba4b3a298ce2eb06d9bff4eb1cc3bd81c4f4ee1b4917e25feb languageName: node linkType: hard @@ -3936,10 +4050,10 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.0.1": - version: 1.1.2 - resolution: "@jridgewell/set-array@npm:1.1.2" - checksum: bc7ab4c4c00470de4e7562ecac3c0c84f53e7ee8a711e546d67c47da7febe7c45cd67d4d84ee3c9b2c05ae8e872656cdded8a707a283d30bd54fbc65aef821ab +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 languageName: node linkType: hard @@ -3960,13 +4074,13 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.22 - resolution: "@jridgewell/trace-mapping@npm:0.3.22" +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: "@jridgewell/resolve-uri": "npm:^3.1.0" "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 18cf19f88e2792c1c91515f2b629aae05f3cdbb2e60c3886e16e80725234ce26dd10144c4981c05d9366e7094498c0b4fe5c1a89f4a730d7376a4ba4af448149 + checksum: 3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 languageName: node linkType: hard @@ -3980,11 +4094,11 @@ __metadata: linkType: hard "@ljharb/through@npm:^2.3.9, @ljharb/through@npm:~2.3.9": - version: 2.3.12 - resolution: "@ljharb/through@npm:2.3.12" + version: 2.3.13 + resolution: "@ljharb/through@npm:2.3.13" dependencies: - call-bind: "npm:^1.0.5" - checksum: 7560aaef7b6ef88c16783ffde37278e2177c7f0f5427400059a8a7687b144dc711bf5b2347ab27e858a29f25e4b868d77c915c9614bc399b82b8123430614653 + call-bind: "npm:^1.0.7" + checksum: fb60b2fb2c674a674d8ebdb8972ccf52f8a62a9c1f5a2ac42557bc0273231c65d642aa2d7627cbb300766a25ae4642acd0f95fba2f8a1ff891086f0cb15807c3 languageName: node linkType: hard @@ -4034,6 +4148,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.2.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": "npm:1.3.2" + checksum: 0bac7d1bbfb3c2286910b02598addd33243cb97c3f36f987ecc927a4be8d7d88e0fcb12b0f0ef8a044e7307d1844dd5c49bb724bfa0a79c8ec50ba60768c97f6 + languageName: node + linkType: hard + "@noble/curves@npm:1.3.0, @noble/curves@npm:~1.3.0": version: 1.3.0 resolution: "@noble/curves@npm:1.3.0" @@ -4050,6 +4173,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.3.2": + version: 1.3.2 + resolution: "@noble/hashes@npm:1.3.2" + checksum: 2482cce3bce6a596626f94ca296e21378e7a5d4c09597cbc46e65ffacc3d64c8df73111f2265444e36a3168208628258bbbaccba2ef24f65f58b2417638a20e7 + languageName: node + linkType: hard + "@noble/hashes@npm:1.3.3, @noble/hashes@npm:~1.3.2": version: 1.3.3 resolution: "@noble/hashes@npm:1.3.3" @@ -4057,6 +4187,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:^1.4.0": + version: 1.4.0 + resolution: "@noble/hashes@npm:1.4.0" + checksum: 8c3f005ee72e7b8f9cff756dfae1241485187254e3f743873e22073d63906863df5d4f13d441b7530ea614b7a093f0d889309f28b59850f33b66cb26a779a4a5 + languageName: node + linkType: hard + "@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:~1.7.0": version: 1.7.1 resolution: "@noble/secp256k1@npm:1.7.1" @@ -4091,6 +4228,198 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/edr-darwin-arm64@npm:0.3.8": + version: 0.3.8 + resolution: "@nomicfoundation/edr-darwin-arm64@npm:0.3.8" + checksum: 98298ddd1dd1d513245be57dc89aad51a0ff6da2f1227c32085a8d49c0d3cbc311981539037c3030b652e1008253d6e72081f019d44cf47c6aa8f14175505554 + languageName: node + linkType: hard + +"@nomicfoundation/edr-darwin-arm64@npm:0.6.5": + version: 0.6.5 + resolution: "@nomicfoundation/edr-darwin-arm64@npm:0.6.5" + checksum: 1ed23f670f280834db7b0cc144d8287b3a572639917240beb6c743ff0f842fadf200eb3e226a13f0650d8a611f5092ace093679090ceb726d97fb4c6023073e6 + languageName: node + linkType: hard + +"@nomicfoundation/edr-darwin-arm64@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-darwin-arm64@npm:0.7.0" + checksum: 7a643fe1c2a1e907699e0b2469672f9d88510c399bd6ef893e480b601189da6daf654e73537bb811f160a397a28ce1b4fe0e36ba763919ac7ee0922a62d09d51 + languageName: node + linkType: hard + +"@nomicfoundation/edr-darwin-x64@npm:0.3.8": + version: 0.3.8 + resolution: "@nomicfoundation/edr-darwin-x64@npm:0.3.8" + checksum: a09fb0030b5dc7e202e7d42aae9f52af86090999a50cbfd9a4b3197b9c7fc8c7325c01d67fdd88b7037a8831e0e89ecf131155edf2c98c453c28f37c6da346f4 + languageName: node + linkType: hard + +"@nomicfoundation/edr-darwin-x64@npm:0.6.5": + version: 0.6.5 + resolution: "@nomicfoundation/edr-darwin-x64@npm:0.6.5" + checksum: 298810fe1ed61568beeb4e4a8ddfb4d3e3cf49d51f89578d5edb5817a7d131069c371d07ea000b246daa2fd57fa4853ab983e3a2e2afc9f27005156e5abfa500 + languageName: node + linkType: hard + +"@nomicfoundation/edr-darwin-x64@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-darwin-x64@npm:0.7.0" + checksum: c33a0320fc4f4e27ef6718a678cfc6ff9fe5b03d3fc604cb503a7291e5f9999da1b4e45ebeff77e24031c4dd53e6defecb3a0d475c9f51d60ea6f48e78f74d8e + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-arm64-gnu@npm:0.3.8": + version: 0.3.8 + resolution: "@nomicfoundation/edr-linux-arm64-gnu@npm:0.3.8" + checksum: 20766169cb3425202a45812f01f9e49560f7cedfb797ea548759136fb8078d6179c11557b69fab8514e7f21a3f3f856dec0ba7a1584d5b431ac1440142f2012e + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-arm64-gnu@npm:0.6.5": + version: 0.6.5 + resolution: "@nomicfoundation/edr-linux-arm64-gnu@npm:0.6.5" + checksum: 695850a75dda9ad00899ca2bd150c72c6b7a2470c352348540791e55459dc6f87ff88b3b647efe07dfe24d4b6aa9d9039724a9761ffc7a557e3e75a784c302a1 + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-arm64-gnu@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-linux-arm64-gnu@npm:0.7.0" + checksum: 8347524cecca3a41ecb6e05581f386ccc6d7e831d4080eca5723724c4307c30ee787a944c70028360cb280a7f61d4967c152ff7b319ccfe08eadf1583a15d018 + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-arm64-musl@npm:0.3.8": + version: 0.3.8 + resolution: "@nomicfoundation/edr-linux-arm64-musl@npm:0.3.8" + checksum: dabd1ee8fc5f45382e8d58770138ba0cd6536915aa5f99459404d27dec046c76e427a0971ed705da83a6aa65e9272ac84b86b14313bd83ea4fea2c81c2d3f1e5 + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-arm64-musl@npm:0.6.5": + version: 0.6.5 + resolution: "@nomicfoundation/edr-linux-arm64-musl@npm:0.6.5" + checksum: 9a6e01a545491b12673334628b6e1601c7856cb3973451ba1a4c29cf279e9a4874b5e5082fc67d899af7930b6576565e2c7e3dbe67824bfe454bf9ce87435c56 + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-arm64-musl@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-linux-arm64-musl@npm:0.7.0" + checksum: ace6d7691058250341dc0d0a2915c2020cc563ab70627f816e06abca7f0181e93941e5099d4a7ca0e6f8f225caff8be2c6563ad7ab8eeaf9124cb2cc53b9d9ac + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-x64-gnu@npm:0.3.8": + version: 0.3.8 + resolution: "@nomicfoundation/edr-linux-x64-gnu@npm:0.3.8" + checksum: 060aaa197a8401b0142d07042e72159703c6e61ef866e98548c00ff2512deb75d179536d63a89ce6cd0866269d22b2823459be730c2766c95b73645f7d4d1afc + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-x64-gnu@npm:0.6.5": + version: 0.6.5 + resolution: "@nomicfoundation/edr-linux-x64-gnu@npm:0.6.5" + checksum: 959b62520cc9375284fcc1ae2ad67c5711d387912216e0b0ab7a3d087ef03967e2c8c8bd2e87697a3b1369fc6a96ec60399e3d71317a8be0cb8864d456a30e36 + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-x64-gnu@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-linux-x64-gnu@npm:0.7.0" + checksum: 11a0eb76a628772ec28fe000b3014e83081f216b0f89568eb42f46c1d3d6ee10015d897857f372087e95651aeeea5cf525c161070f2068bd5e4cf3ccdd4b0201 + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-x64-musl@npm:0.3.8": + version: 0.3.8 + resolution: "@nomicfoundation/edr-linux-x64-musl@npm:0.3.8" + checksum: 49a7cb73b833a694744e2f5323d7268009eb1961e01ceb91f9137cf843e54571636b89418f204222d5f40b9122dd1c0e058b6efd368a438bc3bdb50205ea70d9 + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-x64-musl@npm:0.6.5": + version: 0.6.5 + resolution: "@nomicfoundation/edr-linux-x64-musl@npm:0.6.5" + checksum: d91153a8366005e6a6124893a1da377568157709a147e6c9a18fe6dacae21d3847f02d2e9e89794dc6cb8dbdcd7ee7e49e6c9d3dc74c8dc80cea44e4810752da + languageName: node + linkType: hard + +"@nomicfoundation/edr-linux-x64-musl@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-linux-x64-musl@npm:0.7.0" + checksum: 5559718b3ec00b9f6c9a6cfa6c60540b8f277728482db46183aa907d60f169bc7c8908551b5790c8bad2b0d618ade5ede15b94bdd209660cf1ce707b1fe99fd6 + languageName: node + linkType: hard + +"@nomicfoundation/edr-win32-x64-msvc@npm:0.3.8": + version: 0.3.8 + resolution: "@nomicfoundation/edr-win32-x64-msvc@npm:0.3.8" + checksum: b7dfea0f01fd034ff6b48c44f75836f4dda84975f49c3655c97e76f2535945ed532756e7f2c22fa31035c4a29ce26d9546051f993ce553a8c4d570298525f55c + languageName: node + linkType: hard + +"@nomicfoundation/edr-win32-x64-msvc@npm:0.6.5": + version: 0.6.5 + resolution: "@nomicfoundation/edr-win32-x64-msvc@npm:0.6.5" + checksum: 96c2f68393b517f9b45cb4e777eb594a969abc3fea10bf11756cd050a7e8cefbe27808bd44d8e8a16dc9c425133a110a2ad186e1e6d29b49f234811db52a1edb + languageName: node + linkType: hard + +"@nomicfoundation/edr-win32-x64-msvc@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-win32-x64-msvc@npm:0.7.0" + checksum: 19c10fa99245397556bf70971cc7d68544dc4a63ec7cc087fd09b2541729ec57d03166592837394b0fad903fbb20b1428ec67eed29926227155aa5630a249306 + languageName: node + linkType: hard + +"@nomicfoundation/edr@npm:^0.3.7": + version: 0.3.8 + resolution: "@nomicfoundation/edr@npm:0.3.8" + dependencies: + "@nomicfoundation/edr-darwin-arm64": "npm:0.3.8" + "@nomicfoundation/edr-darwin-x64": "npm:0.3.8" + "@nomicfoundation/edr-linux-arm64-gnu": "npm:0.3.8" + "@nomicfoundation/edr-linux-arm64-musl": "npm:0.3.8" + "@nomicfoundation/edr-linux-x64-gnu": "npm:0.3.8" + "@nomicfoundation/edr-linux-x64-musl": "npm:0.3.8" + "@nomicfoundation/edr-win32-x64-msvc": "npm:0.3.8" + checksum: 56f4debf1d736783f498624fc2d647b075a56baedc6daf593a3e9627c99724e3fbf665245daa321a6f1db74694c11c2d70585d84fd13ecc90bf5632f503e1f04 + languageName: node + linkType: hard + +"@nomicfoundation/edr@npm:^0.6.4": + version: 0.6.5 + resolution: "@nomicfoundation/edr@npm:0.6.5" + dependencies: + "@nomicfoundation/edr-darwin-arm64": "npm:0.6.5" + "@nomicfoundation/edr-darwin-x64": "npm:0.6.5" + "@nomicfoundation/edr-linux-arm64-gnu": "npm:0.6.5" + "@nomicfoundation/edr-linux-arm64-musl": "npm:0.6.5" + "@nomicfoundation/edr-linux-x64-gnu": "npm:0.6.5" + "@nomicfoundation/edr-linux-x64-musl": "npm:0.6.5" + "@nomicfoundation/edr-win32-x64-msvc": "npm:0.6.5" + checksum: 4344efbc7173119bd69dd37c5e60a232ab8307153e9cc329014df95a60f160026042afdd4dc34188f29fc8e8c926f0a3abdf90fb69bed92be031a206da3a6df5 + languageName: node + linkType: hard + +"@nomicfoundation/edr@npm:^0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr@npm:0.7.0" + dependencies: + "@nomicfoundation/edr-darwin-arm64": "npm:0.7.0" + "@nomicfoundation/edr-darwin-x64": "npm:0.7.0" + "@nomicfoundation/edr-linux-arm64-gnu": "npm:0.7.0" + "@nomicfoundation/edr-linux-arm64-musl": "npm:0.7.0" + "@nomicfoundation/edr-linux-x64-gnu": "npm:0.7.0" + "@nomicfoundation/edr-linux-x64-musl": "npm:0.7.0" + "@nomicfoundation/edr-win32-x64-msvc": "npm:0.7.0" + checksum: 7dc0ae7533a9b57bfdee5275e08d160ff01cba1496cc7341a2782706b40f43e5c448ea0790b47dd1cf2712fa08295f271329109ed2313d9c7ff074ca3ae303e0 + languageName: node + linkType: hard + "@nomicfoundation/ethereumjs-block@npm:4.2.2": version: 4.2.2 resolution: "@nomicfoundation/ethereumjs-block@npm:4.2.2" @@ -4120,20 +4449,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-block@npm:5.0.4": - version: 5.0.4 - resolution: "@nomicfoundation/ethereumjs-block@npm:5.0.4" - dependencies: - "@nomicfoundation/ethereumjs-common": "npm:4.0.4" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.4" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.4" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.4" - "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - ethereum-cryptography: "npm:0.1.3" - checksum: 9f8cb09f5910275188b1ad48611856c14131ad28022b958cc648f8e095fadd41002454a3396c612c2977691e59a8baad4f6adf07aab3f9c4b20b5f24c71dd7a0 - languageName: node - linkType: hard - "@nomicfoundation/ethereumjs-blockchain@npm:6.2.2": version: 6.2.2 resolution: "@nomicfoundation/ethereumjs-blockchain@npm:6.2.2" @@ -4175,24 +4490,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-blockchain@npm:7.0.4": - version: 7.0.4 - resolution: "@nomicfoundation/ethereumjs-blockchain@npm:7.0.4" - dependencies: - "@nomicfoundation/ethereumjs-block": "npm:5.0.4" - "@nomicfoundation/ethereumjs-common": "npm:4.0.4" - "@nomicfoundation/ethereumjs-ethash": "npm:3.0.4" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.4" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.4" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.4" - "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - debug: "npm:^4.3.3" - ethereum-cryptography: "npm:0.1.3" - lru-cache: "npm:^10.0.0" - checksum: 47f73fea07880edb68b2614981226d9315a1a3dc673d0e09d68f0bc9ebd93cbfe6ccceb3cb56f777dc9b9386cff39878349fd7474d460a38478e7175b47788f9 - languageName: node - linkType: hard - "@nomicfoundation/ethereumjs-common@npm:3.1.2": version: 3.1.2 resolution: "@nomicfoundation/ethereumjs-common@npm:3.1.2" @@ -4250,19 +4547,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-ethash@npm:3.0.4": - version: 3.0.4 - resolution: "@nomicfoundation/ethereumjs-ethash@npm:3.0.4" - dependencies: - "@nomicfoundation/ethereumjs-block": "npm:5.0.4" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.4" - "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - bigint-crypto-utils: "npm:^3.2.2" - ethereum-cryptography: "npm:0.1.3" - checksum: c4cb846b656720910fe6d4a741ad0be93833c701456da983fa3a1795dda6faaa0ab4fb3130446e648db94b68358e1e4e30024d372fb40d3f65a40650af1a83bc - languageName: node - linkType: hard - "@nomicfoundation/ethereumjs-evm@npm:1.3.2, @nomicfoundation/ethereumjs-evm@npm:^1.0.0-rc.3": version: 1.3.2 resolution: "@nomicfoundation/ethereumjs-evm@npm:1.3.2" @@ -4295,22 +4579,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-evm@npm:2.0.4": - version: 2.0.4 - resolution: "@nomicfoundation/ethereumjs-evm@npm:2.0.4" - dependencies: - "@nomicfoundation/ethereumjs-common": "npm:4.0.4" - "@nomicfoundation/ethereumjs-statemanager": "npm:2.0.4" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.4" - "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - "@types/debug": "npm:^4.1.9" - debug: "npm:^4.3.3" - ethereum-cryptography: "npm:0.1.3" - rustbn-wasm: "npm:^0.2.0" - checksum: f21f4d0f1a74c903934d1c6c3abcd1342c9a812c71f142999c9f3f823119761cfdb4e89e829195fa446adb772b7f4b23ee9d55ed31bd92d5d03df438c3c29202 - languageName: node - linkType: hard - "@nomicfoundation/ethereumjs-rlp@npm:4.0.3": version: 4.0.3 resolution: "@nomicfoundation/ethereumjs-rlp@npm:4.0.3" @@ -4367,27 +4635,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-statemanager@npm:2.0.4": - version: 2.0.4 - resolution: "@nomicfoundation/ethereumjs-statemanager@npm:2.0.4" - dependencies: - "@nomicfoundation/ethereumjs-common": "npm:4.0.4" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.4" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.4" - "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - debug: "npm:^4.3.3" - ethereum-cryptography: "npm:0.1.3" - js-sdsl: "npm:^4.1.4" - lru-cache: "npm:^10.0.0" - peerDependencies: - "@nomicfoundation/ethereumjs-verkle": 0.0.2 - peerDependenciesMeta: - "@nomicfoundation/ethereumjs-verkle": - optional: true - checksum: 6190fef25b1099ede00edf69194903a5c5df18563adae3f053e2eafacc91eeab547d257e6aa47ce7ff5264533bb7bb0fbfcb18e75ea66376ac7305ad40dafbf4 - languageName: node - linkType: hard - "@nomicfoundation/ethereumjs-trie@npm:5.0.5": version: 5.0.5 resolution: "@nomicfoundation/ethereumjs-trie@npm:5.0.5" @@ -4413,20 +4660,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-trie@npm:6.0.4": - version: 6.0.4 - resolution: "@nomicfoundation/ethereumjs-trie@npm:6.0.4" - dependencies: - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.4" - "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - "@types/readable-stream": "npm:^2.3.13" - ethereum-cryptography: "npm:0.1.3" - lru-cache: "npm:^10.0.0" - readable-stream: "npm:^3.6.0" - checksum: b475d858f98037431c54c7177d0ce95133a81f1ee98a0b042136d65c88b7b5b3d86ba981cf78292776781d43d60daed44edf508ff4d416355bd00517e9142c9f - languageName: node - linkType: hard - "@nomicfoundation/ethereumjs-tx@npm:4.1.2": version: 4.1.2 resolution: "@nomicfoundation/ethereumjs-tx@npm:4.1.2" @@ -4506,18 +4739,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-verkle@npm:0.0.2": - version: 0.0.2 - resolution: "@nomicfoundation/ethereumjs-verkle@npm:0.0.2" - dependencies: - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.4" - "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - lru-cache: "npm:^10.0.0" - rust-verkle-wasm: "npm:^0.0.1" - checksum: 7f6b3be75949adbdb8ecea4539a7c4cdce9ee1d76159b5d77f04a794e4e9de42570fc0491fc4769ecc3553547031996b16ec88ba18d68bf0e1b0d1149effb51b - languageName: node - linkType: hard - "@nomicfoundation/ethereumjs-vm@npm:7.0.1": version: 7.0.1 resolution: "@nomicfoundation/ethereumjs-vm@npm:7.0.1" @@ -4539,25 +4760,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-vm@npm:7.0.4": - version: 7.0.4 - resolution: "@nomicfoundation/ethereumjs-vm@npm:7.0.4" - dependencies: - "@nomicfoundation/ethereumjs-block": "npm:5.0.4" - "@nomicfoundation/ethereumjs-blockchain": "npm:7.0.4" - "@nomicfoundation/ethereumjs-common": "npm:4.0.4" - "@nomicfoundation/ethereumjs-evm": "npm:2.0.4" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.4" - "@nomicfoundation/ethereumjs-statemanager": "npm:2.0.4" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.4" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.4" - "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - debug: "npm:^4.3.3" - ethereum-cryptography: "npm:0.1.3" - checksum: a1893c8aa0fb8666bcb7371d8a57f641bd6079a79f9e08175aca214cd4a3d7eda3e54046a7d1b4c6cbc2356e80b1e6b44295e56df2d184ceb157eb67e54ee326 - languageName: node - linkType: hard - "@nomicfoundation/ethereumjs-vm@npm:^6.0.0-rc.3": version: 6.4.2 resolution: "@nomicfoundation/ethereumjs-vm@npm:6.4.2" @@ -4582,7 +4784,79 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/hardhat-network-helpers@npm:^1.0.9": +"@nomicfoundation/hardhat-chai-matchers@npm:^2.0.0": + version: 2.0.6 + resolution: "@nomicfoundation/hardhat-chai-matchers@npm:2.0.6" + dependencies: + "@types/chai-as-promised": "npm:^7.1.3" + chai-as-promised: "npm:^7.1.1" + deep-eql: "npm:^4.0.1" + ordinal: "npm:^1.0.3" + peerDependencies: + "@nomicfoundation/hardhat-ethers": ^3.0.0 + chai: ^4.2.0 + ethers: ^6.1.0 + hardhat: ^2.9.4 + checksum: 0d65a0b0a552391ee3b20db9ffc9559138c02bc9c9cdd30106c9b5247d5a9e9b412480772faecab210a8cade0dfcddce3b04f5d3f3371e80085d6883a917dfe5 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-ethers@npm:^3.0.8": + version: 3.0.8 + resolution: "@nomicfoundation/hardhat-ethers@npm:3.0.8" + dependencies: + debug: "npm:^4.1.1" + lodash.isequal: "npm:^4.5.0" + peerDependencies: + ethers: ^6.1.0 + hardhat: ^2.0.0 + checksum: 478b5d9607e7fc50377bec45ecebbf74240719c76aa08c81052d2a2174eee6f422db8cfd3f13fd17a080d8ff1046fac50dfffa3a2e57c9e3ed466932239e4af2 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-foundry@npm:^1.1.1": + version: 1.1.2 + resolution: "@nomicfoundation/hardhat-foundry@npm:1.1.2" + dependencies: + chalk: "npm:^2.4.2" + peerDependencies: + hardhat: ^2.17.2 + checksum: 54cad1a0d1fc3c2137722a1b213dcbd18f77e63778bda9593052e6090868b2fb7bf8087c10404c6b181fa631e75f7e70490b5b97c3e4eba8f52ef449131dada0 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-ignition-ethers@npm:^0.15.9": + version: 0.15.9 + resolution: "@nomicfoundation/hardhat-ignition-ethers@npm:0.15.9" + peerDependencies: + "@nomicfoundation/hardhat-ethers": ^3.0.4 + "@nomicfoundation/hardhat-ignition": ^0.15.9 + "@nomicfoundation/ignition-core": ^0.15.9 + ethers: ^6.7.0 + hardhat: ^2.18.0 + checksum: 3e5ebe4b0eeea2ddefeaac3ef8db474399cf9688547ef8e39780cb7af3bbb4fb2db9e73ec665f071bb7203cb667e7a9587c86b94c8bdd6346630a263c57b3056 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-ignition@npm:^0.15.9": + version: 0.15.9 + resolution: "@nomicfoundation/hardhat-ignition@npm:0.15.9" + dependencies: + "@nomicfoundation/ignition-core": "npm:^0.15.9" + "@nomicfoundation/ignition-ui": "npm:^0.15.9" + chalk: "npm:^4.0.0" + debug: "npm:^4.3.2" + fs-extra: "npm:^10.0.0" + json5: "npm:^2.2.3" + prompts: "npm:^2.4.2" + peerDependencies: + "@nomicfoundation/hardhat-verify": ^2.0.1 + hardhat: ^2.18.0 + checksum: b8d6b3f92a0183d6d3bb7b3f9919860ba001dc8d0995d74ad1a324110b93d4dfbdbfb685e8a4a3bec6da5870750325d63ebe014653a7248366adac02ff142841 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-network-helpers@npm:^1.0.0, @nomicfoundation/hardhat-network-helpers@npm:^1.0.9": version: 1.0.10 resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.0.10" dependencies: @@ -4593,6 +4867,93 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/hardhat-toolbox@npm:^4.0.0": + version: 4.0.0 + resolution: "@nomicfoundation/hardhat-toolbox@npm:4.0.0" + peerDependencies: + "@nomicfoundation/hardhat-chai-matchers": ^2.0.0 + "@nomicfoundation/hardhat-ethers": ^3.0.0 + "@nomicfoundation/hardhat-network-helpers": ^1.0.0 + "@nomicfoundation/hardhat-verify": ^2.0.0 + "@typechain/ethers-v6": ^0.5.0 + "@typechain/hardhat": ^9.0.0 + "@types/chai": ^4.2.0 + "@types/mocha": ">=9.1.0" + "@types/node": ">=16.0.0" + chai: ^4.2.0 + ethers: ^6.4.0 + hardhat: ^2.11.0 + hardhat-gas-reporter: ^1.0.8 + solidity-coverage: ^0.8.1 + ts-node: ">=8.0.0" + typechain: ^8.3.0 + typescript: ">=4.5.0" + checksum: 9de2511cee509754afd4631f5f1497160bf4d3ecea741e9deeb22a6e662e16ce7ba37cffe836d30a958f08719c0c5906decc3c73df6a5d90c248b4654c8b15d1 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-verify@npm:^2.0.10": + version: 2.0.10 + resolution: "@nomicfoundation/hardhat-verify@npm:2.0.10" + dependencies: + "@ethersproject/abi": "npm:^5.1.2" + "@ethersproject/address": "npm:^5.0.2" + cbor: "npm:^8.1.0" + chalk: "npm:^2.4.2" + debug: "npm:^4.1.1" + lodash.clonedeep: "npm:^4.5.0" + semver: "npm:^6.3.0" + table: "npm:^6.8.0" + undici: "npm:^5.14.0" + peerDependencies: + hardhat: ^2.0.4 + checksum: c5b8e214f2dcdda0530dbb338f65e01bbb6e6b0f96930a88c01b1d1cf2def3f470e0bdb362807bb9a7dbe8216f937b55d14ab251d3cccd2208d6b60cebf2c358 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-verify@npm:^2.0.12": + version: 2.0.12 + resolution: "@nomicfoundation/hardhat-verify@npm:2.0.12" + dependencies: + "@ethersproject/abi": "npm:^5.1.2" + "@ethersproject/address": "npm:^5.0.2" + cbor: "npm:^8.1.0" + debug: "npm:^4.1.1" + lodash.clonedeep: "npm:^4.5.0" + picocolors: "npm:^1.1.0" + semver: "npm:^6.3.0" + table: "npm:^6.8.0" + undici: "npm:^5.14.0" + peerDependencies: + hardhat: ^2.0.4 + checksum: 551f11346480175362023807b4cebbdacc5627db70e2b4fb0afa04d8ec2c26c3b05d2e74821503e881ba745ec6e2c3a678af74206364099ec14e584a811b2564 + languageName: node + linkType: hard + +"@nomicfoundation/ignition-core@npm:^0.15.9": + version: 0.15.9 + resolution: "@nomicfoundation/ignition-core@npm:0.15.9" + dependencies: + "@ethersproject/address": "npm:5.6.1" + "@nomicfoundation/solidity-analyzer": "npm:^0.1.1" + cbor: "npm:^9.0.0" + debug: "npm:^4.3.2" + ethers: "npm:^6.7.0" + fs-extra: "npm:^10.0.0" + immer: "npm:10.0.2" + lodash: "npm:4.17.21" + ndjson: "npm:2.0.0" + checksum: fe02e3f4a981ef338e3acf75cf2e05535c2aba21f4c5b5831b1430fcaa7bbb42b16bd8ac4bb0b9f036d0b9eb1aede5fa57890f0c3863c4ae173d45ac3e484ed8 + languageName: node + linkType: hard + +"@nomicfoundation/ignition-ui@npm:^0.15.9": + version: 0.15.9 + resolution: "@nomicfoundation/ignition-ui@npm:0.15.9" + checksum: 88097576c4186bfdf365f4864463386e7a345be1f8c0b8eebe589267e782735f8cec55e1c5af6c0f0872ba111d79616422552dc7e26c643d01b1768a2b0fb129 + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer-darwin-arm64@npm:0.1.1": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer-darwin-arm64@npm:0.1.1" @@ -4600,6 +4961,13 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/solidity-analyzer-darwin-arm64@npm:0.1.2": + version: 0.1.2 + resolution: "@nomicfoundation/solidity-analyzer-darwin-arm64@npm:0.1.2" + checksum: ef3b13bb2133fea6621db98f991036a3a84d2b240160edec50beafa6ce821fe2f0f5cd4aa61adb9685aff60cd0425982ffd15e0b868b7c768e90e26b8135b825 + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer-darwin-x64@npm:0.1.1": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer-darwin-x64@npm:0.1.1" @@ -4607,6 +4975,13 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/solidity-analyzer-darwin-x64@npm:0.1.2": + version: 0.1.2 + resolution: "@nomicfoundation/solidity-analyzer-darwin-x64@npm:0.1.2" + checksum: 3cb6a00cd200b94efd6f59ed626c705c6f773b92ccf8b90471285cd0e81b35f01edb30c1aa5a4633393c2adb8f20fd34e90c51990dc4e30658e8a67c026d16c9 + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer-freebsd-x64@npm:0.1.1": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer-freebsd-x64@npm:0.1.1" @@ -4621,6 +4996,13 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/solidity-analyzer-linux-arm64-gnu@npm:0.1.2": + version: 0.1.2 + resolution: "@nomicfoundation/solidity-analyzer-linux-arm64-gnu@npm:0.1.2" + checksum: cb9725e7bdc3ba9c1feaef96dbf831c1a59c700ca633a9929fd97debdcb5ce06b5d7b4e6dbc076279978707214d01e2cd126d8e3f4cabc5c16525c031a47b95c + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer-linux-arm64-musl@npm:0.1.1": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer-linux-arm64-musl@npm:0.1.1" @@ -4628,6 +5010,13 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/solidity-analyzer-linux-arm64-musl@npm:0.1.2": + version: 0.1.2 + resolution: "@nomicfoundation/solidity-analyzer-linux-arm64-musl@npm:0.1.2" + checksum: 82a90b1d09ad266ddc510ece2e397f51fdaf29abf7263d2a3a85accddcba2ac24cceb670a3120800611cdcc552eed04919d071e259fdda7564818359ed541f5d + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer-linux-x64-gnu@npm:0.1.1": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer-linux-x64-gnu@npm:0.1.1" @@ -4635,6 +5024,13 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/solidity-analyzer-linux-x64-gnu@npm:0.1.2": + version: 0.1.2 + resolution: "@nomicfoundation/solidity-analyzer-linux-x64-gnu@npm:0.1.2" + checksum: d1f20d4d55683bd041ead957e5461b2e43a39e959f905e8866de1d65f8d96118e9b861e994604d9002cb7f056be0844e36c241a6bb531c336b399609977c0998 + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer-linux-x64-musl@npm:0.1.1": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer-linux-x64-musl@npm:0.1.1" @@ -4642,6 +5038,13 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/solidity-analyzer-linux-x64-musl@npm:0.1.2": + version: 0.1.2 + resolution: "@nomicfoundation/solidity-analyzer-linux-x64-musl@npm:0.1.2" + checksum: 6c17f9af3aaf184c0a217cf723076051c502d85e731dbc97f34b838f9ae1b597577abac54a2af49b3fd986b09131c52fa21fd5393b22d05e1ec7fee96a8249c2 + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer-win32-arm64-msvc@npm:0.1.1": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer-win32-arm64-msvc@npm:0.1.1" @@ -4663,6 +5066,13 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/solidity-analyzer-win32-x64-msvc@npm:0.1.2": + version: 0.1.2 + resolution: "@nomicfoundation/solidity-analyzer-win32-x64-msvc@npm:0.1.2" + checksum: da198464f5ee0d19b6decdfaa65ee0df3097b8960b8483bb7080931968815a5d60f27191229d47a198955784d763d5996f0b92bfde3551612ad972c160b0b000 + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer@npm:^0.1.0": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer@npm:0.1.1" @@ -4702,6 +5112,36 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/solidity-analyzer@npm:^0.1.1": + version: 0.1.2 + resolution: "@nomicfoundation/solidity-analyzer@npm:0.1.2" + dependencies: + "@nomicfoundation/solidity-analyzer-darwin-arm64": "npm:0.1.2" + "@nomicfoundation/solidity-analyzer-darwin-x64": "npm:0.1.2" + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "npm:0.1.2" + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "npm:0.1.2" + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "npm:0.1.2" + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "npm:0.1.2" + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "npm:0.1.2" + dependenciesMeta: + "@nomicfoundation/solidity-analyzer-darwin-arm64": + optional: true + "@nomicfoundation/solidity-analyzer-darwin-x64": + optional: true + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": + optional: true + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": + optional: true + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": + optional: true + "@nomicfoundation/solidity-analyzer-linux-x64-musl": + optional: true + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": + optional: true + checksum: e4f503e9287e18967535af669ca7e26e2682203c45a34ea85da53122da1dee1278f2b8c76c20c67fadd7c1b1a98eeecffd2cbc136860665e3afa133817c0de54 + languageName: node + linkType: hard + "@nomiclabs/hardhat-ethers@npm:^2.0.0, @nomiclabs/hardhat-ethers@npm:^2.0.2, @nomiclabs/hardhat-ethers@npm:^2.1.1, @nomiclabs/hardhat-ethers@npm:^2.2.3": version: 2.2.3 resolution: "@nomiclabs/hardhat-ethers@npm:2.2.3" @@ -4761,24 +5201,24 @@ __metadata: linkType: hard "@npmcli/agent@npm:^2.0.0": - version: 2.2.1 - resolution: "@npmcli/agent@npm:2.2.1" + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" dependencies: agent-base: "npm:^7.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.1" lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.1" - checksum: 38ee5cbe8f3cde13be916e717bfc54fd1a7605c07af056369ff894e244c221e0b56b08ca5213457477f9bc15bca9e729d51a4788829b5c3cf296b3c996147f76 + socks-proxy-agent: "npm:^8.0.3" + checksum: 325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae languageName: node linkType: hard "@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" dependencies: semver: "npm:^7.3.5" - checksum: 162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + checksum: c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 languageName: node linkType: hard @@ -4789,6 +5229,15 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts-upgradeable@npm:^5.0.2": + version: 5.0.2 + resolution: "@openzeppelin/contracts-upgradeable@npm:5.0.2" + peerDependencies: + "@openzeppelin/contracts": 5.0.2 + checksum: 0bd47a4fa0ba8084c1df9573968ff02387bc21514d846b5feb4ad42f90f3ba26bb1e40f17f03e4fa24ffbe473b9ea06c137283297884ab7d5b98d2c112904dc9 + languageName: node + linkType: hard + "@openzeppelin/contracts@npm:^3.3.0-solc-0.7, @openzeppelin/contracts@npm:^3.4.1": version: 3.4.2 resolution: "@openzeppelin/contracts@npm:3.4.2" @@ -4803,29 +5252,36 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts@npm:^5.0.2": + version: 5.0.2 + resolution: "@openzeppelin/contracts@npm:5.0.2" + checksum: d042661db7bb2f3a4b9ef30bba332e86ac20907d171f2ebfccdc9255cc69b62786fead8d6904b8148a8f26946bc7c15eead91b95f75db0c193a99d52e528663e + languageName: node + linkType: hard + "@openzeppelin/defender-admin-client@npm:^1.46.0": - version: 1.54.1 - resolution: "@openzeppelin/defender-admin-client@npm:1.54.1" + version: 1.54.4 + resolution: "@openzeppelin/defender-admin-client@npm:1.54.4" dependencies: - "@openzeppelin/defender-base-client": "npm:1.54.1" + "@openzeppelin/defender-base-client": "npm:1.54.4" axios: "npm:^1.4.0" ethers: "npm:^5.7.2" lodash: "npm:^4.17.19" node-fetch: "npm:^2.6.0" - checksum: b8d9b112bc9c477aca23a9b7249801eb997295302afe60e5c0883579f1ca1069a411ee7be9f972de52019bf59e0d36b7c11503ec09d4328c15899fa4bfdf6d6c + checksum: a44439ebde420aa35b36db7c2845978dd099e0fb6edc394e7b65be67ec1135e99ccc8059e78cb70c8a8fd5598ba40d082648664af95f9cb97a7673c075f64abb languageName: node linkType: hard -"@openzeppelin/defender-base-client@npm:1.54.1, @openzeppelin/defender-base-client@npm:^1.46.0": - version: 1.54.1 - resolution: "@openzeppelin/defender-base-client@npm:1.54.1" +"@openzeppelin/defender-base-client@npm:1.54.4, @openzeppelin/defender-base-client@npm:^1.46.0": + version: 1.54.4 + resolution: "@openzeppelin/defender-base-client@npm:1.54.4" dependencies: amazon-cognito-identity-js: "npm:^6.0.1" async-retry: "npm:^1.3.3" axios: "npm:^1.4.0" lodash: "npm:^4.17.19" node-fetch: "npm:^2.6.0" - checksum: 10a709dfc4fc55ad8d431b1c6d5e67129131d9763f3a91b9ec9ee63653cc8b0a8567809d04341bcb8615e0e20c917c2045a45693070ee9d83da11620338d874d + checksum: 636614a8012cfbbe065b8ccc3ef6a16635e2f1b48f7421242de81f74def99a51795464e9a2bd02e7305ff32b339a8847d7733452e6fe7810d4badd0c2086c38f languageName: node linkType: hard @@ -4879,8 +5335,8 @@ __metadata: linkType: hard "@openzeppelin/upgrades-core@npm:^1.27.0": - version: 1.32.5 - resolution: "@openzeppelin/upgrades-core@npm:1.32.5" + version: 1.33.1 + resolution: "@openzeppelin/upgrades-core@npm:1.33.1" dependencies: cbor: "npm:^9.0.0" chalk: "npm:^4.1.0" @@ -4892,7 +5348,7 @@ __metadata: solidity-ast: "npm:^0.4.51" bin: openzeppelin-upgrades-core: dist/cli/cli.js - checksum: aac654c46aadcfba55024a17d029edcbc529be2fae16fa8d4efc3fd8114cbb4d36d8974a62b1f34fcfa593b3b072898995034ff3bb6a921e00bcac669d8b1998 + checksum: 96fbed78baec7502709a38f5a00180750c24e96aa047a438b34c0d9895dc4ffed47b7e4fedab57cabfd733a3c079e78c98eec23a6df61f3fb4c8b2c9d096bcba languageName: node linkType: hard @@ -4917,15 +5373,15 @@ __metadata: linkType: hard "@peculiar/webcrypto@npm:^1.4.0": - version: 1.4.5 - resolution: "@peculiar/webcrypto@npm:1.4.5" + version: 1.4.6 + resolution: "@peculiar/webcrypto@npm:1.4.6" dependencies: "@peculiar/asn1-schema": "npm:^2.3.8" "@peculiar/json-schema": "npm:^1.1.12" pvtsutils: "npm:^1.3.5" tslib: "npm:^2.6.2" - webcrypto-core: "npm:^1.7.8" - checksum: 7cefe2a1329e3d41721268cf483dfd7a7a3bfbdaaba0ca8db9a538b49a51c6977f66f7f85e6f0c2162ff8d09540821bedc6766586d83c4be565b1af49ea81a86 + webcrypto-core: "npm:^1.7.9" + checksum: b9c80b1a0a3e3ebbf045bd5167fe99ec4a83b170e9aa15a5952a9138afc278210d772306dcc57101d9407c3f9c70dbf6bfb4d8b3f20996ad35c650bb0fe6a90c languageName: node linkType: hard @@ -4987,9 +5443,9 @@ __metadata: linkType: hard "@repeaterjs/repeater@npm:^3.0.4": - version: 3.0.5 - resolution: "@repeaterjs/repeater@npm:3.0.5" - checksum: e6e1aca2bbfe0b8e974bc5185a6839f9e78ec8acb96b6d6911a9dfc958443689f9bc38bcc6d554e6c8598f597f0151841aafbd1ee3ef16262ee93d18b2c1d4b5 + version: 3.0.6 + resolution: "@repeaterjs/repeater@npm:3.0.6" + checksum: c3915e2603927c7d6a9eb09673bc28fc49ab3a86947ec191a74663b33deebee2fcc4b03c31cc663ff27bd6db9e6c9487639b6935e265d601ce71b8c497f5f4a8 languageName: node linkType: hard @@ -5080,10 +5536,10 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.1, @scure/base@npm:~1.1.0, @scure/base@npm:~1.1.4": - version: 1.1.5 - resolution: "@scure/base@npm:1.1.5" - checksum: 6eb07be0202fac74a57c79d0d00a45f6f7e57447010c1e3d90a4275d197829727b7abc54b248fc6f9bef9ae374f7be5ee9154dde5b5b73da773560bf17aa8504 +"@scure/base@npm:~1.1.0, @scure/base@npm:~1.1.4": + version: 1.1.6 + resolution: "@scure/base@npm:1.1.6" + checksum: 237a46a1f45391fc57719154f14295db936a0b1562ea3e182dd42d7aca082dbb7062a28d6c49af16a7e478b12dae8a0fe678d921ea5056bcc30238d29eb05c55 languageName: node linkType: hard @@ -5232,12 +5688,12 @@ __metadata: languageName: node linkType: hard -"@smithy/types@npm:^2.9.1": - version: 2.10.0 - resolution: "@smithy/types@npm:2.10.0" +"@smithy/types@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/types@npm:3.0.0" dependencies: - tslib: "npm:^2.5.0" - checksum: e82d7584e9b0d0c8fb6da059af6ebcb8af86dba7c1dc3e757b4998eca636f8dd697ea47701ff6c755789f0a63ef16f960fcc682ab2ca185cef4c6a6db5f022dc + tslib: "npm:^2.6.2" + checksum: 9f6eefa4f715a8f0bfd79787f82156b4785baaa1524496abe9fc3db96c36f7c782fb962353601d8bd2bba3b449d999d48a09b2b25405bfcd7fb5e1d1c935f1fb languageName: node linkType: hard @@ -5273,72 +5729,72 @@ __metadata: languageName: node linkType: hard -"@stylistic/eslint-plugin-js@npm:1.6.2, @stylistic/eslint-plugin-js@npm:^1.6.2": - version: 1.6.2 - resolution: "@stylistic/eslint-plugin-js@npm:1.6.2" +"@stylistic/eslint-plugin-js@npm:1.8.1, @stylistic/eslint-plugin-js@npm:^1.8.1": + version: 1.8.1 + resolution: "@stylistic/eslint-plugin-js@npm:1.8.1" dependencies: - "@types/eslint": "npm:^8.56.2" + "@types/eslint": "npm:^8.56.10" acorn: "npm:^8.11.3" escape-string-regexp: "npm:^4.0.0" eslint-visitor-keys: "npm:^3.4.3" espree: "npm:^9.6.1" peerDependencies: eslint: ">=8.40.0" - checksum: b74b08802cb888b64b2432e2fa7723c664d1b6a3ee5ad48f8caeef83eebcfcf291f92f967b65e79854d19a660e9428567247de402eeb9e59d9320f71c8c3881e + checksum: 0f5aa5f4afd0a0120a7675f5f0abb2d6fc5ee6197390613a69dcf510644964f1eb8ef8c060be2cf8c1885863a507bdf42e74954f886dca4102e49acfee50c577 languageName: node linkType: hard -"@stylistic/eslint-plugin-jsx@npm:1.6.2": - version: 1.6.2 - resolution: "@stylistic/eslint-plugin-jsx@npm:1.6.2" +"@stylistic/eslint-plugin-jsx@npm:1.8.1": + version: 1.8.1 + resolution: "@stylistic/eslint-plugin-jsx@npm:1.8.1" dependencies: - "@stylistic/eslint-plugin-js": "npm:^1.6.2" - "@types/eslint": "npm:^8.56.2" + "@stylistic/eslint-plugin-js": "npm:^1.8.1" + "@types/eslint": "npm:^8.56.10" estraverse: "npm:^5.3.0" - picomatch: "npm:^4.0.1" + picomatch: "npm:^4.0.2" peerDependencies: eslint: ">=8.40.0" - checksum: 882daa8ac9e3d7db5009dd6a9b4c406edab0ced233d3d49e6a9a0acb94ed68d8e5ec9a705c8925c7adc103ef9c057f33b74c32c7087f40a61c5b85f31f530fcc + checksum: 3bf9a543c1dc661bbe1fb939a46a8cc9543f8c2fbd53b39c05861e042e28c7483cf318152c5cbf1dbc6022e0e211375931f707c5d9a7a63f83a74f857f7b28b4 languageName: node linkType: hard -"@stylistic/eslint-plugin-plus@npm:1.6.2": - version: 1.6.2 - resolution: "@stylistic/eslint-plugin-plus@npm:1.6.2" +"@stylistic/eslint-plugin-plus@npm:1.8.1": + version: 1.8.1 + resolution: "@stylistic/eslint-plugin-plus@npm:1.8.1" dependencies: - "@types/eslint": "npm:^8.56.2" + "@types/eslint": "npm:^8.56.10" "@typescript-eslint/utils": "npm:^6.21.0" peerDependencies: eslint: "*" - checksum: 426e68c7edcb96d48f8ccb22c07fb5b7b26d85210494cfbafb6331d67496946ae3af841933059c31594d5f7c8b4e867de2345418d64a1885b102988d7679fae1 + checksum: 644cf23179b69df12bc798357ae0193eed2dd68a6e5e8dd11f5c8ecad0898eb58b5dac68fafd31c52b852b0800cb21ce457397d4d94cb60549311283540a2dd4 languageName: node linkType: hard -"@stylistic/eslint-plugin-ts@npm:1.6.2": - version: 1.6.2 - resolution: "@stylistic/eslint-plugin-ts@npm:1.6.2" +"@stylistic/eslint-plugin-ts@npm:1.8.1": + version: 1.8.1 + resolution: "@stylistic/eslint-plugin-ts@npm:1.8.1" dependencies: - "@stylistic/eslint-plugin-js": "npm:1.6.2" - "@types/eslint": "npm:^8.56.2" + "@stylistic/eslint-plugin-js": "npm:1.8.1" + "@types/eslint": "npm:^8.56.10" "@typescript-eslint/utils": "npm:^6.21.0" peerDependencies: eslint: ">=8.40.0" - checksum: 15e19abfce7ac77843d1331ee5cab74b405bdb5fb890acb071f6a7c21dc81b338d6482f227aa0af0b32faa61de25166b3a3253cec1df867f99ca105155e001cc + checksum: 1bd0bb220998b7ebd3a7e788b8bafc6674291160604825eea547cfd1a9530b02777ffe5775ddce932388044d7e932c9693be8c30fa989b03ee0ddf98ebc3d40d languageName: node linkType: hard "@stylistic/eslint-plugin@npm:^1.6.2": - version: 1.6.2 - resolution: "@stylistic/eslint-plugin@npm:1.6.2" + version: 1.8.1 + resolution: "@stylistic/eslint-plugin@npm:1.8.1" dependencies: - "@stylistic/eslint-plugin-js": "npm:1.6.2" - "@stylistic/eslint-plugin-jsx": "npm:1.6.2" - "@stylistic/eslint-plugin-plus": "npm:1.6.2" - "@stylistic/eslint-plugin-ts": "npm:1.6.2" - "@types/eslint": "npm:^8.56.2" + "@stylistic/eslint-plugin-js": "npm:1.8.1" + "@stylistic/eslint-plugin-jsx": "npm:1.8.1" + "@stylistic/eslint-plugin-plus": "npm:1.8.1" + "@stylistic/eslint-plugin-ts": "npm:1.8.1" + "@types/eslint": "npm:^8.56.10" peerDependencies: eslint: ">=8.40.0" - checksum: 353c852e1ace9b80bfe18a31b977723d79081397a12ebd14f34f2f5c2eeebc71a38ea17b65b9e3e3d8fcf09c14807822bf9123551e8df1785a3d1d1a6ee39c51 + checksum: 036e4e9dcc4674810bd36b22404234b6a34b79b762fe8275470916ce5a3d3b10e5ef9ba86fb786fcbb39b801a6757079915c1cdd57d07c88ed472fdff5192cc4 languageName: node linkType: hard @@ -5432,9 +5888,9 @@ __metadata: linkType: hard "@tsconfig/node10@npm:^1.0.7": - version: 1.0.9 - resolution: "@tsconfig/node10@npm:1.0.9" - checksum: c176a2c1e1b16be120c328300ea910df15fb9a5277010116d26818272341a11483c5a80059389d04edacf6fd2d03d4687ad3660870fdd1cc0b7109e160adb220 + version: 1.0.11 + resolution: "@tsconfig/node10@npm:1.0.11" + checksum: 28a0710e5d039e0de484bdf85fee883bfd3f6a8980601f4d44066b0a6bcd821d31c4e231d1117731c4e24268bd4cf2a788a6787c12fc7f8d11014c07d582783c languageName: node linkType: hard @@ -5505,6 +5961,20 @@ __metadata: languageName: node linkType: hard +"@typechain/ethers-v6@npm:^0.5.0": + version: 0.5.1 + resolution: "@typechain/ethers-v6@npm:0.5.1" + dependencies: + lodash: "npm:^4.17.15" + ts-essentials: "npm:^7.0.1" + peerDependencies: + ethers: 6.x + typechain: ^8.3.2 + typescript: ">=4.7.0" + checksum: f3c80151c07e01adbf520e0854426649edb0ee540920569487dd8da7eca2fa8615710f4c0eda008e7afdf255fbb8dfdebf721a5d324a4dffeb087611d9bd64b9 + languageName: node + linkType: hard + "@typechain/hardhat@npm:^2.0.0": version: 2.3.1 resolution: "@typechain/hardhat@npm:2.3.1" @@ -5531,6 +6001,20 @@ __metadata: languageName: node linkType: hard +"@typechain/hardhat@npm:^9.0.0": + version: 9.1.0 + resolution: "@typechain/hardhat@npm:9.1.0" + dependencies: + fs-extra: "npm:^9.1.0" + peerDependencies: + "@typechain/ethers-v6": ^0.5.1 + ethers: ^6.1.0 + hardhat: ^2.9.9 + typechain: ^8.3.2 + checksum: 3a1220efefc7b02ca335696167f6c5332a33ff3fbf9f20552468566a1760f76bc88d330683e97ca6213eb9518a2a901391c31c84c0548006b72bd2ec62d4af9c + languageName: node + linkType: hard + "@types/async-eventemitter@npm:^0.2.1": version: 0.2.4 resolution: "@types/async-eventemitter@npm:0.2.4" @@ -5580,7 +6064,7 @@ __metadata: languageName: node linkType: hard -"@types/chai-as-promised@npm:^7.1.5, @types/chai-as-promised@npm:^7.1.7": +"@types/chai-as-promised@npm:^7.1.3, @types/chai-as-promised@npm:^7.1.5, @types/chai-as-promised@npm:^7.1.7": version: 7.1.8 resolution: "@types/chai-as-promised@npm:7.1.8" dependencies: @@ -5589,10 +6073,17 @@ __metadata: languageName: node linkType: hard -"@types/chai@npm:*, @types/chai@npm:^4.3.9": - version: 4.3.12 - resolution: "@types/chai@npm:4.3.12" - checksum: e5d952726d7f053812579962b07d0e4965c160c6a90bf466580e639cd3a1f1d30da1abbfe782383538a043a07908f9dfb823fa9065b37752a5f27d62234f44d5 +"@types/chai@npm:*, @types/chai@npm:^4.2.0, @types/chai@npm:^4.3.9": + version: 4.3.16 + resolution: "@types/chai@npm:4.3.16" + checksum: 745d4a9be429d5d86a7ab26064610b8957fe12dd80e94dc7d0707cf3db1c889e3ffe0d73d69bb15e6d376bf4462a7a75e9d8fc1051750b5d656d6cfe459829b7 + languageName: node + linkType: hard + +"@types/chai@npm:^4.0.0": + version: 4.3.20 + resolution: "@types/chai@npm:4.3.20" + checksum: 4601189d611752e65018f1ecadac82e94eed29f348e1d5430e5681a60b01e1ecf855d9bcc74ae43b07394751f184f6970fac2b5561fc57a1f36e93a0f5ffb6e8 languageName: node linkType: hard @@ -5605,7 +6096,7 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.1.10, @types/debug@npm:^4.1.7, @types/debug@npm:^4.1.8, @types/debug@npm:^4.1.9": +"@types/debug@npm:^4.1.10, @types/debug@npm:^4.1.12, @types/debug@npm:^4.1.7, @types/debug@npm:^4.1.8": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" dependencies: @@ -5623,13 +6114,13 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:^8.56.2": - version: 8.56.3 - resolution: "@types/eslint@npm:8.56.3" +"@types/eslint@npm:*, @types/eslint@npm:^8.56.10": + version: 8.56.10 + resolution: "@types/eslint@npm:8.56.10" dependencies: "@types/estree": "npm:*" "@types/json-schema": "npm:*" - checksum: c5d81d0001fae211451b39d82b2bc8d7224b00d52a514954a33840a3665f36f3bde3be602eec6ad08d1fff59108052cd7746ced4237116bc3d8ac01a7cf5b5fe + checksum: 674349d6c342c3864d70f4d5a9965f96fb253801532752c8c500ad6a1c2e8b219e01ccff5dc8791dcb58b5483012c495708bb9f3ff929f5c9322b3da126c15d3 languageName: node linkType: hard @@ -5719,9 +6210,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.200": - version: 4.14.202 - resolution: "@types/lodash@npm:4.14.202" - checksum: 6064d43c8f454170841bd67c8266cc9069d9e570a72ca63f06bceb484cb4a3ee60c9c1f305c1b9e3a87826049fd41124b8ef265c4dd08b00f6766609c7fe9973 + version: 4.17.4 + resolution: "@types/lodash@npm:4.17.4" + checksum: 0124c64cb9fe7a0f78b6777955abd05ef0d97844d49118652eae45f8fa57bfb7f5a7a9bccc0b5a84c0a6dc09631042e4590cb665acb9d58dfd5e6543c75341ec languageName: node linkType: hard @@ -5755,13 +6246,20 @@ __metadata: languageName: node linkType: hard -"@types/mocha@npm:^10.0.3": +"@types/mocha@npm:>=9.1.0, @types/mocha@npm:^10.0.3": version: 10.0.6 resolution: "@types/mocha@npm:10.0.6" checksum: 4526c9e88388f9e1004c6d3937c5488a39908810f26b927173c58d52b43057f3895627dc06538e96706e08b88158885f869ec6311f6b58fd72bdef715f26d6c3 languageName: node linkType: hard +"@types/mocha@npm:^10.0.9": + version: 10.0.10 + resolution: "@types/mocha@npm:10.0.10" + checksum: d2b8c48138cde6923493e42b38e839695eb42edd04629abe480a8f34c0e3f50dd82a55832c2e8d2b6e6f9e4deb492d7d733e600fbbdd5a0ceccbcfc6844ff9d5 + languageName: node + linkType: hard + "@types/mocha@npm:^8.2.2": version: 8.2.3 resolution: "@types/mocha@npm:8.2.3" @@ -5793,21 +6291,28 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:^20.11.19, @types/node@npm:^20.4.2, @types/node@npm:^20.8.7, @types/node@npm:^20.9.0": - version: 20.11.20 - resolution: "@types/node@npm:20.11.20" +"@types/node@npm:*, @types/node@npm:>=12, @types/node@npm:>=16.0.0, @types/node@npm:^20.11.19, @types/node@npm:^20.4.2, @types/node@npm:^20.8.7, @types/node@npm:^20.9.0": + version: 20.12.12 + resolution: "@types/node@npm:20.12.12" dependencies: undici-types: "npm:~5.26.4" - checksum: 8e8de211e6d54425c603388a9b5cc9c434101985d0a1c88aabbf65d10df2b1fccd71855c20e61ae8a75c7aea56cb0f64e722cf7914cff1247d0b62ce21996ac4 + checksum: f374b763c744e8f16e4f38cf6e2c0eef31781ec9228c9e43a6f267880fea420fab0a238b59f10a7cb3444e49547c5e3785787e371fc242307310995b21988812 languageName: node linkType: hard -"@types/node@npm:>=12": - version: 20.11.30 - resolution: "@types/node@npm:20.11.30" +"@types/node@npm:18.15.13": + version: 18.15.13 + resolution: "@types/node@npm:18.15.13" + checksum: 6e5f61c559e60670a7a8fb88e31226ecc18a21be103297ca4cf9848f0a99049dae77f04b7ae677205f2af494f3701b113ba8734f4b636b355477a6534dbb8ada + languageName: node + linkType: hard + +"@types/node@npm:22.7.5": + version: 22.7.5 + resolution: "@types/node@npm:22.7.5" dependencies: - undici-types: "npm:~5.26.4" - checksum: 867cfaf969c6d8850d8d7304e7ab739898a50ecb1395b61ff2335644f5f48d7a46fbc4a14cee967aed65ec134b61a746edae70d1f32f11321ccf29165e3bc4e6 + undici-types: "npm:~6.19.2" + checksum: cf11f74f1a26053ec58066616e3a8685b6bcd7259bc569738b8f752009f9f0f7f85a1b2d24908e5b0f752482d1e8b6babdf1fbb25758711ec7bb9500bfcd6e60 languageName: node linkType: hard @@ -5869,17 +6374,10 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:^6.2.31, @types/qs@npm:^6.9.4": - version: 6.9.11 - resolution: "@types/qs@npm:6.9.11" - checksum: 657a50f05b694d6fd3916d24177cfa0f3b8b87d9deff4ffa4dddcb0b03583ebf7c47b424b8de400270fb9a5cc1e9cf790dd82c833c6935305851e7da8ede3ff5 - languageName: node - linkType: hard - -"@types/qs@npm:^6.9.7": - version: 6.9.13 - resolution: "@types/qs@npm:6.9.13" - checksum: 0e2dbfb6ee13657cd87742f92c94f1b7a70901452fce477fb9391928165766623581a3229b34fde3e832afbedee509de98984124f38b513ba072d6e193ea06cd +"@types/qs@npm:^6.2.31, @types/qs@npm:^6.9.4, @types/qs@npm:^6.9.7": + version: 6.9.15 + resolution: "@types/qs@npm:6.9.15" + checksum: 49c5ff75ca3adb18a1939310042d273c9fc55920861bd8e5100c8a923b3cda90d759e1a95e18334092da1c8f7b820084687770c83a1ccef04fb2c6908117c823 languageName: node linkType: hard @@ -5921,9 +6419,9 @@ __metadata: linkType: hard "@types/semver@npm:^7.5.0": - version: 7.5.7 - resolution: "@types/semver@npm:7.5.7" - checksum: fb72d8b86a7779650f14ae89542f1da2ab624adb8188d98754b1d29a2fe3d41f0348bf9435b60ad145df1812fd2a09b3256779aa23b532c199f3dee59619a1eb + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: 8663ff927234d1c5fcc04b33062cb2b9fcfbe0f5f351ed26c4d1e1581657deebd506b41ff7fdf89e787e3d33ce05854bc01686379b89e9c49b564c4cfa988efa languageName: node linkType: hard @@ -5977,9 +6475,9 @@ __metadata: linkType: hard "@types/validator@npm:^13.7.1, @types/validator@npm:^13.7.17": - version: 13.11.9 - resolution: "@types/validator@npm:13.11.9" - checksum: 856ebfcfe25d6c91a90235e0eb27302a737832530898195bbfb265da52ae7fe6d68f684942574f8818d3c262cae7a1de99f145dac73fc57217933af1bfc199cb + version: 13.11.10 + resolution: "@types/validator@npm:13.11.10" + checksum: fe63a20fa90d3e8c661d0ac5b5af162cdd387b9e8fd67f5a0a00ca308e4e2d7602467cc32ef3e2c979b737629fa9e2ff593d3946ee4f8667bbb80af0494b9c66 languageName: node linkType: hard @@ -6027,28 +6525,26 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:7.0.2": - version: 7.0.2 - resolution: "@typescript-eslint/eslint-plugin@npm:7.0.2" +"@typescript-eslint/eslint-plugin@npm:7.9.0": + version: 7.9.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.9.0" dependencies: - "@eslint-community/regexpp": "npm:^4.5.1" - "@typescript-eslint/scope-manager": "npm:7.0.2" - "@typescript-eslint/type-utils": "npm:7.0.2" - "@typescript-eslint/utils": "npm:7.0.2" - "@typescript-eslint/visitor-keys": "npm:7.0.2" - debug: "npm:^4.3.4" + "@eslint-community/regexpp": "npm:^4.10.0" + "@typescript-eslint/scope-manager": "npm:7.9.0" + "@typescript-eslint/type-utils": "npm:7.9.0" + "@typescript-eslint/utils": "npm:7.9.0" + "@typescript-eslint/visitor-keys": "npm:7.9.0" graphemer: "npm:^1.4.0" - ignore: "npm:^5.2.4" + ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" + ts-api-utils: "npm:^1.3.0" peerDependencies: "@typescript-eslint/parser": ^7.0.0 eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 76727ad48f01c1bb4ef37690e7ed12754930ce3a4bbe5dcd52f24d42f4625fc0b151db8189947f3956b4a09a562eb2da683ff65b57a13a15426eee3b680f80a5 + checksum: 5c0ded9cb2210c141d236075f01a86447bf497a5061773c3c64a90756264776b4c4df100f7588e36d34f727eca55afd52fe6696a3cbe2d1f131250934254603a languageName: node linkType: hard @@ -6077,21 +6573,21 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:7.0.2": - version: 7.0.2 - resolution: "@typescript-eslint/parser@npm:7.0.2" +"@typescript-eslint/parser@npm:7.9.0": + version: 7.9.0 + resolution: "@typescript-eslint/parser@npm:7.9.0" dependencies: - "@typescript-eslint/scope-manager": "npm:7.0.2" - "@typescript-eslint/types": "npm:7.0.2" - "@typescript-eslint/typescript-estree": "npm:7.0.2" - "@typescript-eslint/visitor-keys": "npm:7.0.2" + "@typescript-eslint/scope-manager": "npm:7.9.0" + "@typescript-eslint/types": "npm:7.9.0" + "@typescript-eslint/typescript-estree": "npm:7.9.0" + "@typescript-eslint/visitor-keys": "npm:7.9.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: acffdbea0bba24398ba8bd1ccf5b59438bc093e41d7a325019383094f39d676b5cf2f5963bfa5e332e54728e5b9e14be3984752ee91da6f0e1a3e0b613422d0e + checksum: 16ca04645429436d9b7986cddda979ef4d088f4223f4a69e04a369e0fd4852dd5ff3d4b99da2e43cddaa2b421b24ff42f275d87bd110ae2356bdd0e81c2534e7 languageName: node linkType: hard @@ -6123,13 +6619,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.0.2": - version: 7.0.2 - resolution: "@typescript-eslint/scope-manager@npm:7.0.2" +"@typescript-eslint/scope-manager@npm:7.9.0": + version: 7.9.0 + resolution: "@typescript-eslint/scope-manager@npm:7.9.0" dependencies: - "@typescript-eslint/types": "npm:7.0.2" - "@typescript-eslint/visitor-keys": "npm:7.0.2" - checksum: 60241a0dbed7605133b6242d7fc172e8ee649e1033b8a179cebe3e21c60e0c08c12679fd37644cfef57c95a5d75a3927afc9d6365a5f9684c1d043285db23c66 + "@typescript-eslint/types": "npm:7.9.0" + "@typescript-eslint/visitor-keys": "npm:7.9.0" + checksum: 1ba6fc559a42a9b54e38c3ac2b6669efcff1a30292fb4e5fc8739c890a6c0f37d1a6aee1d115198f57c88e4f1776e95c1d7143de5cb5b970d5eb3023e97789dd languageName: node linkType: hard @@ -6150,20 +6646,20 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.0.2": - version: 7.0.2 - resolution: "@typescript-eslint/type-utils@npm:7.0.2" +"@typescript-eslint/type-utils@npm:7.9.0": + version: 7.9.0 + resolution: "@typescript-eslint/type-utils@npm:7.9.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.0.2" - "@typescript-eslint/utils": "npm:7.0.2" + "@typescript-eslint/typescript-estree": "npm:7.9.0" + "@typescript-eslint/utils": "npm:7.9.0" debug: "npm:^4.3.4" - ts-api-utils: "npm:^1.0.1" + ts-api-utils: "npm:^1.3.0" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: fa7957aa65cb0d7366c7c9be94e45cc2f1ebe9981cbf393054b505c6d555a01b2a2fe7cd1254d668f30183a275032f909186ce0b9f213f64b776bd7872144a6e + checksum: 775280fb179268f8bacd60e684d9d5a1c6a379646b082c7244bf2dfb7dd693053bd9efa473b71e10a86db69322b0a2cecf5598d019684930df50000bf3d70af0 languageName: node linkType: hard @@ -6174,10 +6670,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:7.0.2": - version: 7.0.2 - resolution: "@typescript-eslint/types@npm:7.0.2" - checksum: 5f95266cc2cd0e6cf1239dcd36b53c7d98b01ba12c61947316f0d879df87b912b4d23f0796324e2ab0fb8780503a338da41a4695fa91d90392b6c6aca5239fa7 +"@typescript-eslint/types@npm:7.9.0": + version: 7.9.0 + resolution: "@typescript-eslint/types@npm:7.9.0" + checksum: d5f4a547dba4865ee2391bf06f2b3f8e8592a561976d2be35bb61ce340c7d1b7b4b25ac6ab5b9941813b465b9420bebb7b2179b1d71f6a83069feeb000b3558d languageName: node linkType: hard @@ -6200,22 +6696,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.0.2": - version: 7.0.2 - resolution: "@typescript-eslint/typescript-estree@npm:7.0.2" +"@typescript-eslint/typescript-estree@npm:7.9.0": + version: 7.9.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.9.0" dependencies: - "@typescript-eslint/types": "npm:7.0.2" - "@typescript-eslint/visitor-keys": "npm:7.0.2" + "@typescript-eslint/types": "npm:7.9.0" + "@typescript-eslint/visitor-keys": "npm:7.9.0" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" - minimatch: "npm:9.0.3" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" peerDependenciesMeta: typescript: optional: true - checksum: 2f6795b05fced9f2e0887f6735aa1a0b20516952792e4be13cd94c5e56db8ad013ba27aeb56f89fedff8b7af587f854482f00aac75b418611c74e42169c29aeb + checksum: cfc3d2b7a5433c9a2989c7289bc72b49786993782801ad8ca5a07c651df457a67fbce13b120c86c34c03d56570a90e5cf4f3b8806349f103a3658f2366ec28ea languageName: node linkType: hard @@ -6236,20 +6732,17 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.0.2": - version: 7.0.2 - resolution: "@typescript-eslint/utils@npm:7.0.2" +"@typescript-eslint/utils@npm:7.9.0": + version: 7.9.0 + resolution: "@typescript-eslint/utils@npm:7.9.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@types/json-schema": "npm:^7.0.12" - "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:7.0.2" - "@typescript-eslint/types": "npm:7.0.2" - "@typescript-eslint/typescript-estree": "npm:7.0.2" - semver: "npm:^7.5.4" + "@typescript-eslint/scope-manager": "npm:7.9.0" + "@typescript-eslint/types": "npm:7.9.0" + "@typescript-eslint/typescript-estree": "npm:7.9.0" peerDependencies: eslint: ^8.56.0 - checksum: b4ae9a36393c92b332e99d70219d1ee056271261f7433924db804e5f06d97ca60408b9c7a655afce8a851982e7153243a625d6cc76fea764f767f96c8f3e16da + checksum: cb99d6a950e7da0319bc7b923a82c52c0798a14e837afee51b2295cfbde02e0a2ac8e0b5904cd7bd01d1b376c7a6ad3739101b486feaf2517c8640024deb88c7 languageName: node linkType: hard @@ -6263,13 +6756,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.0.2": - version: 7.0.2 - resolution: "@typescript-eslint/visitor-keys@npm:7.0.2" +"@typescript-eslint/visitor-keys@npm:7.9.0": + version: 7.9.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.9.0" dependencies: - "@typescript-eslint/types": "npm:7.0.2" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 4146d1ad6ce9374e6b5a75677fc709816bdc5fe324b1a857405f21dad23bb28c79cfd0555bc2a01c4af1d9e9ee81ff5e29ec41cc9d05b0b1101cc4264e7f21d1 + "@typescript-eslint/types": "npm:7.9.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 19181d8b9d2d7bc43d5c8884661cd9a86ac316392b8e590187cc507442093a1ba2bef0cc22181b8298d5dc9f455abb73cffa4663451bdf32b1b7fe12160c5c99 languageName: node linkType: hard @@ -6293,12 +6786,12 @@ __metadata: linkType: hard "@urql/core@npm:>=2.3.6": - version: 4.2.3 - resolution: "@urql/core@npm:4.2.3" + version: 5.0.3 + resolution: "@urql/core@npm:5.0.3" dependencies: - "@0no-co/graphql.web": "npm:^1.0.1" + "@0no-co/graphql.web": "npm:^1.0.5" wonka: "npm:^6.3.2" - checksum: 2e2e5f94365c5513c6eb8d996d0b60211ca5ef122bfb7e77a192f63f8f469689ead2b232b835ce47f8127d7c9a5c8083548cac05f3b4be4f01f723dbfb575da0 + checksum: 7ae9dfe4cbce949a0e2856f46e5c0f62a1bdb6eda6f24480468b62f886cc03131e77b578da4fbb9fe8c1622b7542fdd371b83e7883f01a296924b5dfbe738f10 languageName: node linkType: hard @@ -6433,6 +6926,19 @@ __metadata: languageName: node linkType: hard +"abitype@npm:0.7.1": + version: 0.7.1 + resolution: "abitype@npm:0.7.1" + peerDependencies: + typescript: ">=4.9.4" + zod: ^3 >=3.19.1 + peerDependenciesMeta: + zod: + optional: true + checksum: c95386afc8438b29d09fcb6d7bc3a457958ab0a472483a363bdb9bf9f42e1b90ab5b05a16f04b653ad0bf79f4451233fe35fc6c7a04b66cb4eba9df7d8e49f12 + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -6556,6 +7062,13 @@ __metadata: languageName: node linkType: hard +"aes-js@npm:4.0.0-beta.5": + version: 4.0.0-beta.5 + resolution: "aes-js@npm:4.0.0-beta.5" + checksum: 444f4eefa1e602cbc4f2a3c644bc990f93fd982b148425fee17634da510586fc09da940dcf8ace1b2d001453c07ff042e55f7a0482b3cc9372bf1ef75479090c + languageName: node + linkType: hard + "aes-js@npm:^3.1.1": version: 3.1.2 resolution: "aes-js@npm:3.1.2" @@ -6572,12 +7085,12 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": - version: 7.1.0 - resolution: "agent-base@npm:7.1.0" +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" dependencies: debug: "npm:^4.3.4" - checksum: fc974ab57ffdd8421a2bc339644d312a9cca320c20c3393c9d8b1fd91731b9bbabdb985df5fc860f5b79d81c3e350daa3fcb31c5c07c0bb385aafc817df004ce + checksum: e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 languageName: node linkType: hard @@ -6630,27 +7143,27 @@ __metadata: linkType: hard "ajv@npm:^8.0.0, ajv@npm:^8.0.1, ajv@npm:^8.11.0, ajv@npm:^8.12.0": - version: 8.12.0 - resolution: "ajv@npm:8.12.0" + version: 8.13.0 + resolution: "ajv@npm:8.13.0" dependencies: - fast-deep-equal: "npm:^3.1.1" + fast-deep-equal: "npm:^3.1.3" json-schema-traverse: "npm:^1.0.0" require-from-string: "npm:^2.0.2" - uri-js: "npm:^4.2.2" - checksum: ac4f72adf727ee425e049bc9d8b31d4a57e1c90da8d28bcd23d60781b12fcd6fc3d68db5df16994c57b78b94eed7988f5a6b482fd376dc5b084125e20a0a622e + uri-js: "npm:^4.4.1" + checksum: 14c6497b6f72843986d7344175a1aa0e2c35b1e7f7475e55bc582cddb765fca7e6bf950f465dc7846f817776d9541b706f4b5b3fbedd8dfdeb5fce6f22864264 languageName: node linkType: hard "amazon-cognito-identity-js@npm:^6.0.1": - version: 6.3.11 - resolution: "amazon-cognito-identity-js@npm:6.3.11" + version: 6.3.13 + resolution: "amazon-cognito-identity-js@npm:6.3.13" dependencies: "@aws-crypto/sha256-js": "npm:1.2.2" buffer: "npm:4.9.2" fast-base64-decode: "npm:^1.0.0" isomorphic-unfetch: "npm:^3.0.0" js-cookie: "npm:^2.2.1" - checksum: 4619e4c19770722ac243c98fb7d4aff35eb0b8f5a2db9ea31a5765f5c54deb7245e316e7e9f633f07d70520f13be157fc90c6139c5f0f2ecc59e5e7d16ee76b1 + checksum: 274785ef26573969bcc12d8f36d1f529da7fd50068ee52c7e3213c61549ce319db7c477c8c11633fc7f6bfe114fd93071228720742c0b091bafe18e7c13536a8 languageName: node linkType: hard @@ -6693,6 +7206,13 @@ __metadata: languageName: node linkType: hard +"ansi-escapes@npm:^6.2.0": + version: 6.2.1 + resolution: "ansi-escapes@npm:6.2.1" + checksum: a2c6f58b044be5f69662ee17073229b492daa2425a7fd99a665db6c22eab6e4ab42752807def7281c1c7acfed48f87f2362dda892f08c2c437f1b39c6b033103 + languageName: node + linkType: hard + "ansi-regex@npm:^2.0.0": version: 2.1.1 resolution: "ansi-regex@npm:2.1.1" @@ -6753,7 +7273,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0": +"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c @@ -6952,15 +7472,16 @@ __metadata: linkType: hard "array.prototype.findlast@npm:^1.2.2": - version: 1.2.4 - resolution: "array.prototype.findlast@npm:1.2.4" + version: 1.2.5 + resolution: "array.prototype.findlast@npm:1.2.5" dependencies: - call-bind: "npm:^1.0.5" + call-bind: "npm:^1.0.7" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.22.3" + es-abstract: "npm:^1.23.2" es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" es-shim-unscopables: "npm:^1.0.2" - checksum: 4b5145a68ebaa00ef3d61de07c6694cad73d60763079f1e7662b948e5a167b5121b0c1e6feae8df1e42ead07c21699e25242b95cd5c48e094fd530b192aa4150 + checksum: ddc952b829145ab45411b9d6adcb51a8c17c76bf89c9dd64b52d5dffa65d033da8c076ed2e17091779e83bc892b9848188d7b4b33453c5565e65a92863cb2775 languageName: node linkType: hard @@ -6977,15 +7498,17 @@ __metadata: linkType: hard "array.prototype.reduce@npm:^1.0.6": - version: 1.0.6 - resolution: "array.prototype.reduce@npm:1.0.6" + version: 1.0.7 + resolution: "array.prototype.reduce@npm:1.0.7" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" es-array-method-boxes-properly: "npm:^1.0.0" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" is-string: "npm:^1.0.7" - checksum: 4082757ff094c372d94e5b5c7f7f12dae11cfdf41dec7cd7a54a528f6a92155442bac38eddd23a82be7e8fd9c458b124163e791cb5841372d02b1ba964a92816 + checksum: 97aac907d7b15088d5b991bad79de96f95ea0d47a701a034e2dc816e0aabaed2fb401d7fe65ab6fda05eafa58319aa2d1bac404f515e162b81b3b61a51224db2 languageName: node linkType: hard @@ -7019,15 +7542,14 @@ __metadata: languageName: node linkType: hard -"asn1.js@npm:^5.2.0": - version: 5.4.1 - resolution: "asn1.js@npm:5.4.1" +"asn1.js@npm:^4.10.1": + version: 4.10.1 + resolution: "asn1.js@npm:4.10.1" dependencies: bn.js: "npm:^4.0.0" inherits: "npm:^2.0.1" minimalistic-assert: "npm:^1.0.0" - safer-buffer: "npm:^2.1.0" - checksum: b577232fa6069cc52bb128e564002c62b2b1fe47f7137bdcd709c0b8495aa79cee0f8cc458a831b2d8675900eea0d05781b006be5e1aa4f0ae3577a73ec20324 + checksum: afa7f3ab9e31566c80175a75b182e5dba50589dcc738aa485be42bdd787e2a07246a4b034d481861123cbe646a7656f318f4f1cad2e9e5e808a210d5d6feaa88 languageName: node linkType: hard @@ -7196,7 +7718,7 @@ __metadata: languageName: node linkType: hard -"available-typed-arrays@npm:^1.0.6, available-typed-arrays@npm:^1.0.7": +"available-typed-arrays@npm:^1.0.7": version: 1.0.7 resolution: "available-typed-arrays@npm:1.0.7" dependencies: @@ -7234,18 +7756,29 @@ __metadata: dependencies: follow-redirects: "npm:^1.14.9" form-data: "npm:^4.0.0" - checksum: 76d673d2a90629944b44d6f345f01e58e9174690f635115d5ffd4aca495d99bcd8f95c590d5ccb473513f5ebc1d1a6e8934580d0c57cdd0498c3a101313ef771 + checksum: 76d673d2a90629944b44d6f345f01e58e9174690f635115d5ffd4aca495d99bcd8f95c590d5ccb473513f5ebc1d1a6e8934580d0c57cdd0498c3a101313ef771 + languageName: node + linkType: hard + +"axios@npm:^1.4.0, axios@npm:^1.5.1": + version: 1.7.1 + resolution: "axios@npm:1.7.1" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 554395472f18f4ddb43b4be2900473bc1a4d589464a8ab16f6954c53d9cace4317d5c9e009d5bb05f098d9565b2fa45f152a5d4cecb87536c8f0c370c25a7770 languageName: node linkType: hard -"axios@npm:^1.4.0, axios@npm:^1.5.1": - version: 1.6.7 - resolution: "axios@npm:1.6.7" +"axios@npm:^1.6.8": + version: 1.8.1 + resolution: "axios@npm:1.8.1" dependencies: - follow-redirects: "npm:^1.15.4" + follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 131bf8e62eee48ca4bd84e6101f211961bf6a21a33b95e5dfb3983d5a2fe50d9fffde0b57668d7ce6f65063d3dc10f2212cbcb554f75cfca99da1c73b210358d + checksum: b2e1d5a61264502deee4b50f0a6df0aa3b174c546ccf68c0dff714a2b8863232e0bd8cb5b84f853303e97f242a98260f9bb9beabeafe451ad5af538e9eb7ac22 languageName: node linkType: hard @@ -8017,7 +8550,7 @@ __metadata: languageName: node linkType: hard -"bigint-crypto-utils@npm:^3.0.23, bigint-crypto-utils@npm:^3.2.2": +"bigint-crypto-utils@npm:^3.0.23": version: 3.3.0 resolution: "bigint-crypto-utils@npm:3.3.0" checksum: 7d06fa01d63e8e9513eee629fe8a426993276b1bdca5aefd0eb3188cee7026334d29e801ef6187a5bc6105ebf26e6e79e6fab544a7da769ccf55b913e66d2a14 @@ -8039,9 +8572,9 @@ __metadata: linkType: hard "binary-extensions@npm:^2.0.0": - version: 2.2.0 - resolution: "binary-extensions@npm:2.2.0" - checksum: d73d8b897238a2d3ffa5f59c0241870043aa7471335e89ea5e1ff48edb7c2d0bb471517a3e4c5c3f4c043615caa2717b5f80a5e61e07503d51dc85cb848e665d + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 languageName: node linkType: hard @@ -8335,14 +8868,14 @@ __metadata: languageName: node linkType: hard -"browser-stdout@npm:1.3.1": +"browser-stdout@npm:1.3.1, browser-stdout@npm:^1.3.1": version: 1.3.1 resolution: "browser-stdout@npm:1.3.1" checksum: c40e482fd82be872b6ea7b9f7591beafbf6f5ba522fe3dade98ba1573a1c29a11101564993e4eb44e5488be8f44510af072df9a9637c739217eb155ceb639205 languageName: node linkType: hard -"browserify-aes@npm:^1.0.0, browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": +"browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" dependencies: @@ -8390,19 +8923,20 @@ __metadata: linkType: hard "browserify-sign@npm:^4.0.0": - version: 4.2.2 - resolution: "browserify-sign@npm:4.2.2" + version: 4.2.3 + resolution: "browserify-sign@npm:4.2.3" dependencies: bn.js: "npm:^5.2.1" browserify-rsa: "npm:^4.1.0" create-hash: "npm:^1.2.0" create-hmac: "npm:^1.1.7" - elliptic: "npm:^6.5.4" + elliptic: "npm:^6.5.5" + hash-base: "npm:~3.0" inherits: "npm:^2.0.4" - parse-asn1: "npm:^5.1.6" - readable-stream: "npm:^3.6.2" + parse-asn1: "npm:^5.1.7" + readable-stream: "npm:^2.3.8" safe-buffer: "npm:^5.2.1" - checksum: 4d1292e5c165d93455630515003f0e95eed9239c99e2d373920c5b56903d16296a3d23cd4bdc4d298f55ad9b83714a9e63bc4839f1166c303349a16e84e9b016 + checksum: 30c0eba3f5970a20866a4d3fbba2c5bd1928cd24f47faf995f913f1499214c6f3be14bb4d6ec1ab5c6cafb1eca9cb76ba1c2e1c04ed018370634d4e659c77216 languageName: node linkType: hard @@ -8591,8 +9125,8 @@ __metadata: linkType: hard "cacache@npm:^18.0.0": - version: 18.0.2 - resolution: "cacache@npm:18.0.2" + version: 18.0.3 + resolution: "cacache@npm:18.0.3" dependencies: "@npmcli/fs": "npm:^3.1.0" fs-minipass: "npm:^3.0.0" @@ -8606,7 +9140,7 @@ __metadata: ssri: "npm:^10.0.0" tar: "npm:^6.1.11" unique-filename: "npm:^3.0.0" - checksum: 7992665305cc251a984f4fdbab1449d50e88c635bc43bf2785530c61d239c61b349e5734461baa461caaee65f040ab14e2d58e694f479c0810cffd181ba5eabc + checksum: dfda92840bb371fb66b88c087c61a74544363b37a265023223a99965b16a16bbb87661fe4948718d79df6e0cc04e85e62784fbcf1832b2a5e54ff4c46fbb45b7 languageName: node linkType: hard @@ -8773,9 +9307,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30000844, caniuse-lite@npm:^1.0.30001587": - version: 1.0.30001589 - resolution: "caniuse-lite@npm:1.0.30001589" - checksum: 20debfb949413f603011bc7dacaf050010778bc4f8632c86fafd1bd0c43180c95ae7c31f6c82348f6309e5e221934e327c3607a216e3f09640284acf78cd6d4d + version: 1.0.30001620 + resolution: "caniuse-lite@npm:1.0.30001620" + checksum: 3783117143fbdc98c1b91a579d0f2a7bcee7008f322ba7a2bf56a6c3d105400772c7ed8026840b4ea909ec7bf254bcc36532f2ce1b1a1240b00d0335da39b7ec languageName: node linkType: hard @@ -8830,13 +9364,28 @@ __metadata: linkType: hard "chai-as-promised@npm:^7.1.1": - version: 7.1.1 - resolution: "chai-as-promised@npm:7.1.1" + version: 7.1.2 + resolution: "chai-as-promised@npm:7.1.2" dependencies: check-error: "npm:^1.0.2" peerDependencies: - chai: ">= 2.1.2 < 5" - checksum: e25a602c3a8cd0b97ce6b0c7ddaaf4bd8517941da9f44dc65262c5268ea463f634dc495cdef6a21eaeffdb5022b6f4c3781027b8308273b7fff089c605abf6aa + chai: ">= 2.1.2 < 6" + checksum: ee20ed75296d8cbf828b2f3c9ad64627cee67b1a38b8e906ca59fe788fb6965ddb10f702ae66645ed88f15a905ade4f2d9f8540029e92e2d59b229c9f912273f + languageName: node + linkType: hard + +"chai@npm:^4.0.0": + version: 4.5.0 + resolution: "chai@npm:4.5.0" + dependencies: + assertion-error: "npm:^1.1.0" + check-error: "npm:^1.0.3" + deep-eql: "npm:^4.1.3" + get-func-name: "npm:^2.0.2" + loupe: "npm:^2.3.6" + pathval: "npm:^1.1.1" + type-detect: "npm:^4.1.0" + checksum: b8cb596bd1aece1aec659e41a6e479290c7d9bee5b3ad63d2898ad230064e5b47889a3bc367b20100a0853b62e026e2dc514acf25a3c9385f936aa3614d4ab4d languageName: node linkType: hard @@ -8855,6 +9404,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:5.3.0": + version: 5.3.0 + resolution: "chalk@npm:5.3.0" + checksum: 8297d436b2c0f95801103ff2ef67268d362021b8210daf8ddbe349695333eb3610a71122172ff3b0272f1ef2cf7cc2c41fdaa4715f52e49ffe04c56340feed09 + languageName: node + linkType: hard + "chalk@npm:^1.1.3": version: 1.1.3 resolution: "chalk@npm:1.1.3" @@ -9016,7 +9572,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.4.0, chokidar@npm:^3.5.2": +"chokidar@npm:^3.4.0, chokidar@npm:^3.5.2, chokidar@npm:^3.5.3": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -9035,6 +9591,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^4.0.0": + version: 4.0.1 + resolution: "chokidar@npm:4.0.1" + dependencies: + readdirp: "npm:^4.0.1" + checksum: 4bb7a3adc304059810bb6c420c43261a15bb44f610d77c35547addc84faa0374265c3adc67f25d06f363d9a4571962b02679268c40de07676d260de1986efea9 + languageName: node + linkType: hard + "chownr@npm:^1.1.4": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -9154,6 +9719,15 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^4.0.0": + version: 4.0.0 + resolution: "cli-cursor@npm:4.0.0" + dependencies: + restore-cursor: "npm:^4.0.0" + checksum: e776e8c3c6727300d0539b0d25160b2bb56aed1a63942753ba1826b012f337a6f4b7ace3548402e4f2f13b5e16bfd751be672c44b203205e7eca8be94afec42c + languageName: node + linkType: hard + "cli-spinners@npm:^2.5.0": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" @@ -9176,15 +9750,15 @@ __metadata: linkType: hard "cli-table3@npm:^0.6.0, cli-table3@npm:^0.6.2": - version: 0.6.3 - resolution: "cli-table3@npm:0.6.3" + version: 0.6.5 + resolution: "cli-table3@npm:0.6.5" dependencies: "@colors/colors": "npm:1.5.0" string-width: "npm:^4.2.0" dependenciesMeta: "@colors/colors": optional: true - checksum: 39e580cb346c2eaf1bd8f4ff055ae644e902b8303c164a1b8894c0dc95941f92e001db51f49649011be987e708d9fa3183ccc2289a4d376a057769664048cc0c + checksum: d7cc9ed12212ae68241cc7a3133c52b844113b17856e11f4f81308acc3febcea7cc9fd298e70933e294dd642866b29fd5d113c2c098948701d0c35f09455de78 languageName: node linkType: hard @@ -9217,6 +9791,16 @@ __metadata: languageName: node linkType: hard +"cli-truncate@npm:^4.0.0": + version: 4.0.0 + resolution: "cli-truncate@npm:4.0.0" + dependencies: + slice-ansi: "npm:^5.0.0" + string-width: "npm:^7.0.0" + checksum: d7f0b73e3d9b88cb496e6c086df7410b541b56a43d18ade6a573c9c18bd001b1c3fba1ad578f741a4218fdc794d042385f8ac02c25e1c295a2d8b9f3cb86eb4c + languageName: node + linkType: hard + "cli-width@npm:^3.0.0": version: 3.0.0 resolution: "cli-width@npm:3.0.0" @@ -9385,7 +9969,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.16": +"colorette@npm:^2.0.16, colorette@npm:^2.0.20": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 @@ -9469,6 +10053,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:11.1.0, commander@npm:^11.1.0": + version: 11.1.0 + resolution: "commander@npm:11.1.0" + checksum: 13cc6ac875e48780250f723fb81c1c1178d35c5decb1abb1b628b3177af08a8554e76b2c0f29de72d69eef7c864d12613272a71fabef8047922bc622ab75a179 + languageName: node + linkType: hard + "commander@npm:2.11.0": version: 2.11.0 resolution: "commander@npm:2.11.0" @@ -9490,10 +10081,10 @@ __metadata: languageName: node linkType: hard -"commander@npm:^11.1.0": - version: 11.1.0 - resolution: "commander@npm:11.1.0" - checksum: 13cc6ac875e48780250f723fb81c1c1178d35c5decb1abb1b628b3177af08a8554e76b2c0f29de72d69eef7c864d12613272a71fabef8047922bc622ab75a179 +"commander@npm:^12.0.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 languageName: node linkType: hard @@ -9511,6 +10102,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^8.1.0": + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 8b043bb8322ea1c39664a1598a95e0495bfe4ca2fad0d84a92d7d1d8d213e2a155b441d2470c8e08de7c4a28cf2bc6e169211c49e1b21d9f7edc6ae4d9356060 + languageName: node + linkType: hard + "commander@npm:^9.3.0, commander@npm:^9.4.0": version: 9.5.0 resolution: "commander@npm:9.5.0" @@ -9736,6 +10334,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:0.6.0": + version: 0.6.0 + resolution: "cookie@npm:0.6.0" + checksum: f2318b31af7a31b4ddb4a678d024514df5e705f9be5909a192d7f116cfb6d45cbacf96a473fa733faa95050e7cff26e7832bb3ef94751592f1387b71c8956686 + languageName: node + linkType: hard + "cookiejar@npm:^2.1.1": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -9751,9 +10356,9 @@ __metadata: linkType: hard "core-js-pure@npm:^3.0.1": - version: 3.36.0 - resolution: "core-js-pure@npm:3.36.0" - checksum: 1c5ecb37451bcebaa449e36285d27c4c79d5ff24b8bfd44491ce661cfc12b5c56471c847d306d21a56894338d00abea4993a6f8e07c71d4e887d1f71e410d22e + version: 3.37.1 + resolution: "core-js-pure@npm:3.37.1" + checksum: 38200d08862b4ef2207af72a7525f7b9ac750f5e1d84ef27a3e314aefa69518179a9b732f51ebe35c3b38606d9fa4f686fcf6eff067615cc293a3b1c84041e74 languageName: node linkType: hard @@ -10029,13 +10634,13 @@ __metadata: languageName: node linkType: hard -"d@npm:1, d@npm:^1.0.1": - version: 1.0.1 - resolution: "d@npm:1.0.1" +"d@npm:1, d@npm:^1.0.1, d@npm:^1.0.2": + version: 1.0.2 + resolution: "d@npm:1.0.2" dependencies: - es5-ext: "npm:^0.10.50" - type: "npm:^1.0.1" - checksum: 1fedcb3b956a461f64d86b94b347441beff5cef8910b6ac4ec509a2c67eeaa7093660a98b26601ac91f91260238add73bdf25867a9c0cb783774642bc4c1523f + es5-ext: "npm:^0.10.64" + type: "npm:^2.7.2" + checksum: 3e6ede10cd3b77586c47da48423b62bed161bf1a48bdbcc94d87263522e22f5dfb0e678a6dba5323fdc14c5d8612b7f7eb9e7d9e37b2e2d67a7bf9f116dabe5a languageName: node linkType: hard @@ -10055,6 +10660,39 @@ __metadata: languageName: node linkType: hard +"data-view-buffer@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-buffer@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.6" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 8984119e59dbed906a11fcfb417d7d861936f16697a0e7216fe2c6c810f6b5e8f4a5281e73f2c28e8e9259027190ac4a33e2a65fdd7fa86ac06b76e838918583 + languageName: node + linkType: hard + +"data-view-byte-length@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-byte-length@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: b7d9e48a0cf5aefed9ab7d123559917b2d7e0d65531f43b2fd95b9d3a6b46042dd3fca597c42bba384e66b70d7ad66ff23932f8367b241f53d93af42cfe04ec2 + languageName: node + linkType: hard + +"data-view-byte-offset@npm:^1.0.0": + version: 1.0.0 + resolution: "data-view-byte-offset@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.6" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 21b0d2e53fd6e20cc4257c873bf6d36d77bd6185624b84076c0a1ddaa757b49aaf076254006341d35568e89f52eecd1ccb1a502cfb620f2beca04f48a6a62a8f + languageName: node + linkType: hard + "dataloader@npm:2.2.2, dataloader@npm:^2.2.2": version: 2.2.2 resolution: "dataloader@npm:2.2.2" @@ -10124,6 +10762,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.5, debug@npm:^4.3.7": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b + languageName: node + linkType: hard + "decamelize-keys@npm:^1.1.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -10148,6 +10798,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.5.0 + resolution: "decimal.js@npm:10.5.0" + checksum: 785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3 + languageName: node + linkType: hard + "decode-uri-component@npm:^0.2.0": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -10180,7 +10837,7 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^4.1.3": +"deep-eql@npm:^4.0.1, deep-eql@npm:^4.1.3": version: 4.1.3 resolution: "deep-eql@npm:4.1.3" dependencies: @@ -10259,7 +10916,7 @@ __metadata: languageName: node linkType: hard -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1, define-data-property@npm:^1.1.2, define-data-property@npm:^1.1.4": +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" dependencies: @@ -10277,7 +10934,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": +"define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -10396,6 +11053,13 @@ __metadata: languageName: node linkType: hard +"detect-file@npm:^1.0.0": + version: 1.0.0 + resolution: "detect-file@npm:1.0.0" + checksum: c782a5f992047944c39d337c82f5d1d21d65d1378986d46c354df9d9ec6d5f356bca0182969c11b08b9b8a7af8727b3c2d5a9fad0b022be4a3bf4c216f63ed07 + languageName: node + linkType: hard + "detect-indent@npm:^4.0.0": version: 4.0.0 resolution: "detect-indent@npm:4.0.0" @@ -10413,15 +11077,15 @@ __metadata: linkType: hard "detect-port@npm:^1.3.0": - version: 1.5.1 - resolution: "detect-port@npm:1.5.1" + version: 1.6.1 + resolution: "detect-port@npm:1.6.1" dependencies: address: "npm:^1.0.1" debug: "npm:4" bin: detect: bin/detect-port.js detect-port: bin/detect-port.js - checksum: f2b204ad3a9f8e8b53fea35fcc97469f31a8e3e786a2f59fbc886397e33b5f130c5f964bf001b9a64d990047c3824f6a439308461ff19801df04ab48a754639e + checksum: 4ea9eb46a637cb21220dd0a62b6074792894fc77b2cacbc9de533d1908b2eedafa7bfd7547baaa2ac1e9c7ba7c289b34b17db896dca6da142f4fc6e2060eee17 languageName: node linkType: hard @@ -10453,7 +11117,7 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.0.0": +"diff@npm:^5.0.0, diff@npm:^5.2.0": version: 5.2.0 resolution: "diff@npm:5.2.0" checksum: aed0941f206fe261ecb258dc8d0ceea8abbde3ace5827518ff8d302f0fc9cc81ce116c4d8f379151171336caf0516b79e01abdc1ed1201b6440d895a66689eb4 @@ -10471,6 +11135,15 @@ __metadata: languageName: node linkType: hard +"difflib@npm:^0.2.4": + version: 0.2.4 + resolution: "difflib@npm:0.2.4" + dependencies: + heap: "npm:>= 0.2.0" + checksum: 4b151f1f6d378b0837ef28f4706d89d05b78f1093253b06c975c621f7ef8b048978588baf9e8f284c64b133d0abb08303b0789519cc91e5180d420cb3bb99c05 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -10583,14 +11256,14 @@ __metadata: linkType: hard "duplexify@npm:^4.1.1, duplexify@npm:^4.1.2": - version: 4.1.2 - resolution: "duplexify@npm:4.1.2" + version: 4.1.3 + resolution: "duplexify@npm:4.1.3" dependencies: end-of-stream: "npm:^1.4.1" inherits: "npm:^2.0.3" readable-stream: "npm:^3.1.1" - stream-shift: "npm:^1.0.0" - checksum: cacd09d8f1c58f92f83e17dffc14ece50415b32753446ed92046236a27a9e73cb914cda495d955ea12e0e615381082a511f20e219f48a06e84675c9d6950675b + stream-shift: "npm:^1.0.2" + checksum: 8a7621ae95c89f3937f982fe36d72ea997836a708471a75bb2a0eecde3330311b1e128a6dad510e0fd64ace0c56bff3484ed2e82af0e465600c82117eadfbda5 languageName: node linkType: hard @@ -10619,13 +11292,13 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.3.47, electron-to-chromium@npm:^1.4.668": - version: 1.4.681 - resolution: "electron-to-chromium@npm:1.4.681" - checksum: 5b2558dfb8bb82c20fb5fa1d9bbe06a3add47431dc3e1e4815e997be6ad387787047d9e534ed96839a9e7012520a5281c865158b09db41d10c029af003f05f94 + version: 1.4.774 + resolution: "electron-to-chromium@npm:1.4.774" + checksum: dc957ee574d14838abd16f48fb3e4bd069bf47807fe02144b850d4903265810e17abc5800a361941f787bd78692cf4f81eab70046a6757eb39e44c91ff520dd4 languageName: node linkType: hard -"elliptic@npm:6.5.4, elliptic@npm:^6.4.0, elliptic@npm:^6.5.2, elliptic@npm:^6.5.3, elliptic@npm:^6.5.4": +"elliptic@npm:6.5.4": version: 6.5.4 resolution: "elliptic@npm:6.5.4" dependencies: @@ -10640,6 +11313,28 @@ __metadata: languageName: node linkType: hard +"elliptic@npm:^6.4.0, elliptic@npm:^6.5.2, elliptic@npm:^6.5.3, elliptic@npm:^6.5.4, elliptic@npm:^6.5.5": + version: 6.5.5 + resolution: "elliptic@npm:6.5.5" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 3e591e93783a1b66f234ebf5bd3a8a9a8e063a75073a35a671e03e3b25253b6e33ac121f7efe9b8808890fffb17b40596cc19d01e6e8d1fa13b9a56ff65597c8 + languageName: node + linkType: hard + +"emoji-regex@npm:^10.3.0": + version: 10.3.0 + resolution: "emoji-regex@npm:10.3.0" + checksum: b4838e8dcdceb44cf47f59abe352c25ff4fe7857acaf5fb51097c427f6f75b44d052eb907a7a3b86f86bc4eae3a93f5c2b7460abe79c407307e6212d65c91163 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -10764,17 +11459,21 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3": - version: 1.22.4 - resolution: "es-abstract@npm:1.22.4" +"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2": + version: 1.23.3 + resolution: "es-abstract@npm:1.23.3" dependencies: array-buffer-byte-length: "npm:^1.0.1" arraybuffer.prototype.slice: "npm:^1.0.3" - available-typed-arrays: "npm:^1.0.6" + available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.7" + data-view-buffer: "npm:^1.0.1" + data-view-byte-length: "npm:^1.0.1" + data-view-byte-offset: "npm:^1.0.0" es-define-property: "npm:^1.0.0" es-errors: "npm:^1.3.0" - es-set-tostringtag: "npm:^2.0.2" + es-object-atoms: "npm:^1.0.0" + es-set-tostringtag: "npm:^2.0.3" es-to-primitive: "npm:^1.2.1" function.prototype.name: "npm:^1.1.6" get-intrinsic: "npm:^1.2.4" @@ -10782,15 +11481,16 @@ __metadata: globalthis: "npm:^1.0.3" gopd: "npm:^1.0.1" has-property-descriptors: "npm:^1.0.2" - has-proto: "npm:^1.0.1" + has-proto: "npm:^1.0.3" has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.1" + hasown: "npm:^2.0.2" internal-slot: "npm:^1.0.7" is-array-buffer: "npm:^3.0.4" is-callable: "npm:^1.2.7" - is-negative-zero: "npm:^2.0.2" + is-data-view: "npm:^1.0.1" + is-negative-zero: "npm:^2.0.3" is-regex: "npm:^1.1.4" - is-shared-array-buffer: "npm:^1.0.2" + is-shared-array-buffer: "npm:^1.0.3" is-string: "npm:^1.0.7" is-typed-array: "npm:^1.1.13" is-weakref: "npm:^1.0.2" @@ -10798,18 +11498,18 @@ __metadata: object-keys: "npm:^1.1.1" object.assign: "npm:^4.1.5" regexp.prototype.flags: "npm:^1.5.2" - safe-array-concat: "npm:^1.1.0" + safe-array-concat: "npm:^1.1.2" safe-regex-test: "npm:^1.0.3" - string.prototype.trim: "npm:^1.2.8" - string.prototype.trimend: "npm:^1.0.7" - string.prototype.trimstart: "npm:^1.0.7" - typed-array-buffer: "npm:^1.0.1" - typed-array-byte-length: "npm:^1.0.0" - typed-array-byte-offset: "npm:^1.0.0" - typed-array-length: "npm:^1.0.4" + string.prototype.trim: "npm:^1.2.9" + string.prototype.trimend: "npm:^1.0.8" + string.prototype.trimstart: "npm:^1.0.8" + typed-array-buffer: "npm:^1.0.2" + typed-array-byte-length: "npm:^1.0.1" + typed-array-byte-offset: "npm:^1.0.2" + typed-array-length: "npm:^1.0.6" unbox-primitive: "npm:^1.0.2" - which-typed-array: "npm:^1.1.14" - checksum: dc332c3a010c5e7b77b7ea8a4532ac455fa02e7bcabf996a47447165bafa72d0d99967407d0cf5dbbb5fbbf87f53cd8b706608ec70953523b8cd2b831b9a9d64 + which-typed-array: "npm:^1.1.15" + checksum: d27e9afafb225c6924bee9971a7f25f20c314f2d6cb93a63cada4ac11dcf42040896a6c22e5fb8f2a10767055ed4ddf400be3b1eb12297d281726de470b75666 languageName: node linkType: hard @@ -10836,7 +11536,16 @@ __metadata: languageName: node linkType: hard -"es-set-tostringtag@npm:^2.0.2": +"es-object-atoms@npm:^1.0.0": + version: 1.0.0 + resolution: "es-object-atoms@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 1fed3d102eb27ab8d983337bb7c8b159dd2a1e63ff833ec54eea1311c96d5b08223b433060ba240541ca8adba9eee6b0a60cdbf2f80634b784febc9cc8b687b4 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.0.3": version: 2.0.3 resolution: "es-set-tostringtag@npm:2.0.3" dependencies: @@ -10867,15 +11576,15 @@ __metadata: languageName: node linkType: hard -"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.50, es5-ext@npm:^0.10.62, es5-ext@npm:~0.10.14": - version: 0.10.63 - resolution: "es5-ext@npm:0.10.63" +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.50, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.63, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14": + version: 0.10.64 + resolution: "es5-ext@npm:0.10.64" dependencies: es6-iterator: "npm:^2.0.3" es6-symbol: "npm:^3.1.3" esniff: "npm:^2.0.1" next-tick: "npm:^1.1.0" - checksum: 1f20f9c73dc43cca9f1aa6908062f0a3d0bf3cee229a11a2cda6d4eca83583ceeb9b59ad36e951aa6e41ddd06d81aafac9a01de3d38a76b86f598a69ad0456bd + checksum: 4459b6ae216f3c615db086e02437bdfde851515a101577fd61b19f9b3c1ad924bab4d197981eb7f0ccb915f643f2fc10ff76b97a680e96cbb572d15a27acd9a3 languageName: node linkType: hard @@ -10898,16 +11607,16 @@ __metadata: linkType: hard "es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": - version: 3.1.3 - resolution: "es6-symbol@npm:3.1.3" + version: 3.1.4 + resolution: "es6-symbol@npm:3.1.4" dependencies: - d: "npm:^1.0.1" - ext: "npm:^1.1.2" - checksum: 22982f815f00df553a89f4fb74c5048fed85df598482b4bd38dbd173174247949c72982a7d7132a58b147525398400e5f182db59b0916cb49f1e245fb0e22233 + d: "npm:^1.0.2" + ext: "npm:^1.7.0" + checksum: 777bf3388db5d7919e09a0fd175aa5b8a62385b17cb2227b7a137680cba62b4d9f6193319a102642aa23d5840d38a62e4784f19cfa5be4a2210a3f0e9b23d15d languageName: node linkType: hard -"escalade@npm:^3.1.1": +"escalade@npm:^3.1.1, escalade@npm:^3.1.2": version: 3.1.2 resolution: "escalade@npm:3.1.2" checksum: 6b4adafecd0682f3aa1cd1106b8fff30e492c7015b178bc81b2d2f75106dabea6c6d6e8508fc491bd58e597c74abb0e8e2368f943ecb9393d4162e3c2f3cf287 @@ -11035,55 +11744,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.52.0, eslint@npm:^8.56.0": - version: 8.56.0 - resolution: "eslint@npm:8.56.0" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.6.1" - "@eslint/eslintrc": "npm:^2.1.4" - "@eslint/js": "npm:8.56.0" - "@humanwhocodes/config-array": "npm:^0.11.13" - "@humanwhocodes/module-importer": "npm:^1.0.1" - "@nodelib/fs.walk": "npm:^1.2.8" - "@ungap/structured-clone": "npm:^1.2.0" - ajv: "npm:^6.12.4" - chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" - debug: "npm:^4.3.2" - doctrine: "npm:^3.0.0" - escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^7.2.2" - eslint-visitor-keys: "npm:^3.4.3" - espree: "npm:^9.6.1" - esquery: "npm:^1.4.2" - esutils: "npm:^2.0.2" - fast-deep-equal: "npm:^3.1.3" - file-entry-cache: "npm:^6.0.1" - find-up: "npm:^5.0.0" - glob-parent: "npm:^6.0.2" - globals: "npm:^13.19.0" - graphemer: "npm:^1.4.0" - ignore: "npm:^5.2.0" - imurmurhash: "npm:^0.1.4" - is-glob: "npm:^4.0.0" - is-path-inside: "npm:^3.0.3" - js-yaml: "npm:^4.1.0" - json-stable-stringify-without-jsonify: "npm:^1.0.1" - levn: "npm:^0.4.1" - lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.1.2" - natural-compare: "npm:^1.4.0" - optionator: "npm:^0.9.3" - strip-ansi: "npm:^6.0.1" - text-table: "npm:^0.2.0" - bin: - eslint: bin/eslint.js - checksum: 2be598f7da1339d045ad933ffd3d4742bee610515cd2b0d9a2b8b729395a01d4e913552fff555b559fccaefd89d7b37632825789d1b06470608737ae69ab43fb - languageName: node - linkType: hard - -"eslint@npm:^8.57.0": +"eslint@npm:^8.52.0, eslint@npm:^8.56.0, eslint@npm:^8.57.0": version: 8.57.0 resolution: "eslint@npm:8.57.0" dependencies: @@ -11394,11 +12055,11 @@ __metadata: linkType: hard "ethereum-bloom-filters@npm:^1.0.6": - version: 1.0.10 - resolution: "ethereum-bloom-filters@npm:1.0.10" + version: 1.1.0 + resolution: "ethereum-bloom-filters@npm:1.1.0" dependencies: - js-sha3: "npm:^0.8.0" - checksum: ae70b0b0b6d83beece65638a634818f0bd1d00d7a4447e17b83797f4d8db4c49491b57119c5ed081c008fb766bb8f230f3603187fd6649d58a8cf3b9aa91549c + "@noble/hashes": "npm:^1.4.0" + checksum: 54b0b7a1fdf12fe02fc8f605f213d11ea026111b9d2af79ff58e8319c904d9d6cee77c62fe70bee62c4d0c7952caf58ebaf47a889d9e4199cf4da1a361a87b53 languageName: node linkType: hard @@ -11854,6 +12515,36 @@ __metadata: languageName: node linkType: hard +"ethers@npm:^6.13.4": + version: 6.13.4 + resolution: "ethers@npm:6.13.4" + dependencies: + "@adraffy/ens-normalize": "npm:1.10.1" + "@noble/curves": "npm:1.2.0" + "@noble/hashes": "npm:1.3.2" + "@types/node": "npm:22.7.5" + aes-js: "npm:4.0.0-beta.5" + tslib: "npm:2.7.0" + ws: "npm:8.17.1" + checksum: efcf9f39f841e38af68ec23cdbd745432c239c256aac4929842d1af04e55d7be0ff65e462f1cf3e93586f43f7bdcc0098fd56f2f7234f36d73e466521a5766ce + languageName: node + linkType: hard + +"ethers@npm:^6.7.0": + version: 6.13.2 + resolution: "ethers@npm:6.13.2" + dependencies: + "@adraffy/ens-normalize": "npm:1.10.1" + "@noble/curves": "npm:1.2.0" + "@noble/hashes": "npm:1.3.2" + "@types/node": "npm:18.15.13" + aes-js: "npm:4.0.0-beta.5" + tslib: "npm:2.4.0" + ws: "npm:8.17.1" + checksum: 5956389a180992f8b6d90bc21b2e0f28619a098513d3aeb7a350a0b7c5852d635a9d7fd4ced1af50c985dd88398716f66dfd4a2de96c5c3a67150b93543d92af + languageName: node + linkType: hard + "ethjs-unit@npm:0.1.6": version: 0.1.6 resolution: "ethjs-unit@npm:0.1.6" @@ -11928,6 +12619,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^5.0.1": + version: 5.0.1 + resolution: "eventemitter3@npm:5.0.1" + checksum: 4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 + languageName: node + linkType: hard + "events@npm:^3.0.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -11946,6 +12644,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^8.0.1" + human-signals: "npm:^5.0.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^3.0.0" + checksum: 2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af + languageName: node + linkType: hard + "execa@npm:^0.7.0": version: 0.7.0 resolution: "execa@npm:0.7.0" @@ -12028,6 +12743,15 @@ __metadata: languageName: node linkType: hard +"expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2": + version: 2.0.2 + resolution: "expand-tilde@npm:2.0.2" + dependencies: + homedir-polyfill: "npm:^1.0.1" + checksum: 205a60497422746d1c3acbc1d65bd609b945066f239a2b785e69a7a651ac4cbeb4e08555b1ea0023abbe855e6fcb5bbf27d0b371367fdccd303d4fb2b4d66845 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -12073,7 +12797,7 @@ __metadata: languageName: node linkType: hard -"express@npm:4.18.2, express@npm:^4.14.0": +"express@npm:4.18.2": version: 4.18.2 resolution: "express@npm:4.18.2" dependencies: @@ -12112,16 +12836,16 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.18.1": - version: 4.18.3 - resolution: "express@npm:4.18.3" +"express@npm:^4.14.0, express@npm:^4.18.1": + version: 4.19.2 + resolution: "express@npm:4.19.2" dependencies: accepts: "npm:~1.3.8" array-flatten: "npm:1.1.1" body-parser: "npm:1.20.2" content-disposition: "npm:0.5.4" content-type: "npm:~1.0.4" - cookie: "npm:0.5.0" + cookie: "npm:0.6.0" cookie-signature: "npm:1.0.6" debug: "npm:2.6.9" depd: "npm:2.0.0" @@ -12147,11 +12871,11 @@ __metadata: type-is: "npm:~1.6.18" utils-merge: "npm:1.0.1" vary: "npm:~1.1.2" - checksum: 0b9eeafbac549e3c67d92d083bf1773e358359f41ad142b92121935c6348d29079b75054555b3f62de39263fffc8ba06898b09fdd3e213e28e714c03c5d9f44c + checksum: e82e2662ea9971c1407aea9fc3c16d6b963e55e3830cd0ef5e00b533feda8b770af4e3be630488ef8a752d7c75c4fcefb15892868eeaafe7353cb9e3e269fdcb languageName: node linkType: hard -"ext@npm:^1.1.2": +"ext@npm:^1.7.0": version: 1.7.0 resolution: "ext@npm:1.7.0" dependencies: @@ -12301,7 +13025,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.9": +"fast-glob@npm:3.3.2, fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.9": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -12338,9 +13062,9 @@ __metadata: linkType: hard "fast-redact@npm:^3.0.0": - version: 3.3.0 - resolution: "fast-redact@npm:3.3.0" - checksum: d81562510681e9ba6404ee5d3838ff5257a44d2f80937f5024c099049ff805437d0fae0124458a7e87535cc9dcf4de305bb075cab8f08d6c720bbc3447861b4e + version: 3.5.0 + resolution: "fast-redact@npm:3.5.0" + checksum: 7e2ce4aad6e7535e0775bf12bd3e4f2e53d8051d8b630e0fa9e67f68cb0b0e6070d2f7a94b1d0522ef07e32f7c7cda5755e2b677a6538f1e9070ca053c42343a languageName: node linkType: hard @@ -12400,6 +13124,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.2": + version: 6.4.2 + resolution: "fdir@npm:6.4.2" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 34829886f34a3ca4170eca7c7180ec4de51a3abb4d380344063c0ae2e289b11d2ba8b724afee974598c83027fea363ff598caf2b51bc4e6b1e0d8b80cc530573 + languageName: node + linkType: hard + "fecha@npm:^4.2.0": version: 4.2.3 resolution: "fecha@npm:4.2.3" @@ -12619,6 +13355,18 @@ __metadata: languageName: node linkType: hard +"findup-sync@npm:^5.0.0": + version: 5.0.0 + resolution: "findup-sync@npm:5.0.0" + dependencies: + detect-file: "npm:^1.0.0" + is-glob: "npm:^4.0.3" + micromatch: "npm:^4.0.4" + resolve-dir: "npm:^1.0.1" + checksum: bbdb8af8c86a0bde4445e2f738003b92e4cd2a4539a5b45199d0252f2f504aeaf19aeca1fac776c3632c60657b2659151e72c8ead29a79617459a57419a0920b + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.2.0 resolution: "flat-cache@npm:3.2.0" @@ -12669,17 +13417,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.12.1, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.15.4": - version: 1.15.5 - resolution: "follow-redirects@npm:1.15.5" - peerDependenciesMeta: - debug: - optional: true - checksum: 418d71688ceaf109dfd6f85f747a0c75de30afe43a294caa211def77f02ef19865b547dfb73fde82b751e1cc507c06c754120b848fe5a7400b0a669766df7615 - languageName: node - linkType: hard - -"follow-redirects@npm:^1.14.9": +"follow-redirects@npm:^1.12.1, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.9, follow-redirects@npm:^1.15.6": version: 1.15.6 resolution: "follow-redirects@npm:1.15.6" peerDependenciesMeta: @@ -12857,6 +13595,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.2.0": + version: 11.3.0 + resolution: "fs-extra@npm:11.3.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 5f95e996186ff45463059feb115a22fb048bdaf7e487ecee8a8646c78ed8fdca63630e3077d4c16ce677051f5e60d3355a06f3cd61f3ca43f48cc58822a44d0a + languageName: node + linkType: hard + "fs-extra@npm:^4.0.2, fs-extra@npm:^4.0.3": version: 4.0.3 resolution: "fs-extra@npm:4.0.3" @@ -13080,6 +13829,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.2.0 + resolution: "get-east-asian-width@npm:1.2.0" + checksum: 914b1e217cf38436c24b4c60b4c45289e39a45bf9e65ef9fd343c2815a1a02b8a0215aeec8bf9c07c516089004b6e3826332481f40a09529fcadbf6e579f286b + languageName: node + linkType: hard + "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -13087,7 +13843,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4": +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4": version: 1.2.4 resolution: "get-intrinsic@npm:1.2.4" dependencies: @@ -13153,6 +13909,13 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^8.0.1": + version: 8.0.1 + resolution: "get-stream@npm:8.0.1" + checksum: 5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290 + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -13244,9 +14007,23 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.1.2": - version: 7.1.2 - resolution: "glob@npm:7.1.2" +"glob@npm:7.1.2": + version: 7.1.2 + resolution: "glob@npm:7.1.2" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.0.4" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 2fc8e29c6a6c5cb99854177e9c47a6e17130dd4ee06c5576d53b171e07b1fbc40fa613295dbd35a6f1a8d02a6214b39a0f848ed7cb74bfc2325cc32485d17cbe + languageName: node + linkType: hard + +"glob@npm:7.1.7": + version: 7.1.7 + resolution: "glob@npm:7.1.7" dependencies: fs.realpath: "npm:^1.0.0" inflight: "npm:^1.0.4" @@ -13254,7 +14031,7 @@ __metadata: minimatch: "npm:^3.0.4" once: "npm:^1.3.0" path-is-absolute: "npm:^1.0.0" - checksum: 2fc8e29c6a6c5cb99854177e9c47a6e17130dd4ee06c5576d53b171e07b1fbc40fa613295dbd35a6f1a8d02a6214b39a0f848ed7cb74bfc2325cc32485d17cbe + checksum: 173245e6f9ccf904309eb7ef4a44a11f3bf68e9e341dff5a28b5db0dd7123b7506daf41497f3437a0710f57198187b758c2351eeaabce4d16935e956920da6a4 languageName: node linkType: hard @@ -13272,7 +14049,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:8.1.0, glob@npm:^8.0.3": +"glob@npm:8.1.0, glob@npm:^8.0.3, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -13286,17 +14063,17 @@ __metadata: linkType: hard "glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": - version: 10.3.10 - resolution: "glob@npm:10.3.10" + version: 10.3.15 + resolution: "glob@npm:10.3.15" dependencies: foreground-child: "npm:^3.1.0" - jackspeak: "npm:^2.3.5" + jackspeak: "npm:^2.3.6" minimatch: "npm:^9.0.1" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry: "npm:^1.10.1" + minipass: "npm:^7.0.4" + path-scurry: "npm:^1.11.0" bin: glob: dist/esm/bin.mjs - checksum: 13d8a1feb7eac7945f8c8480e11cd4a44b24d26503d99a8d8ac8d5aefbf3e9802a2b6087318a829fad04cb4e829f25c5f4f1110c68966c498720dd261c7e344d + checksum: cda748ddc181b31b3df9548c0991800406d5cc3b3f8110e37a8751ec1e39f37cdae7d7782d5422d7df92775121cdf00599992dff22f7ff1260344843af227c2b languageName: node linkType: hard @@ -13336,6 +14113,17 @@ __metadata: languageName: node linkType: hard +"global-modules@npm:^1.0.0": + version: 1.0.0 + resolution: "global-modules@npm:1.0.0" + dependencies: + global-prefix: "npm:^1.0.1" + is-windows: "npm:^1.0.1" + resolve-dir: "npm:^1.0.0" + checksum: 7d91ecf78d4fcbc966b2d89c1400df273afea795bc8cadf39857ee1684e442065621fd79413ff5fcd9e90c6f1b2dc0123e644fa0b7811f987fd54c6b9afad858 + languageName: node + linkType: hard + "global-modules@npm:^2.0.0": version: 2.0.0 resolution: "global-modules@npm:2.0.0" @@ -13345,6 +14133,19 @@ __metadata: languageName: node linkType: hard +"global-prefix@npm:^1.0.1": + version: 1.0.2 + resolution: "global-prefix@npm:1.0.2" + dependencies: + expand-tilde: "npm:^2.0.2" + homedir-polyfill: "npm:^1.0.1" + ini: "npm:^1.3.4" + is-windows: "npm:^1.0.1" + which: "npm:^1.2.14" + checksum: d8037e300f1dc04d5d410d16afa662e71bfad22dcceba6c9727bb55cc273b8988ca940b3402f62e5392fd261dd9924a9a73a865ef2000219461f31f3fc86be06 + languageName: node + linkType: hard + "global-prefix@npm:^3.0.0": version: 3.0.0 resolution: "global-prefix@npm:3.0.0" @@ -13390,11 +14191,12 @@ __metadata: linkType: hard "globalthis@npm:^1.0.3": - version: 1.0.3 - resolution: "globalthis@npm:1.0.3" + version: 1.0.4 + resolution: "globalthis@npm:1.0.4" dependencies: - define-properties: "npm:^1.1.3" - checksum: 0db6e9af102a5254630351557ac15e6909bc7459d3e3f6b001e59fe784c96d31108818f032d9095739355a88467459e6488ff16584ee6250cd8c27dec05af4b0 + define-properties: "npm:^1.2.1" + gopd: "npm:^1.0.1" + checksum: 9d156f313af79d80b1566b93e19285f481c591ad6d0d319b4be5e03750d004dde40a39a0f26f7e635f9007a3600802f53ecd85a759b86f109e80a5f705e01846 languageName: node linkType: hard @@ -13573,11 +14375,11 @@ __metadata: linkType: hard "graphql-ws@npm:^5.12.1": - version: 5.15.0 - resolution: "graphql-ws@npm:5.15.0" + version: 5.16.0 + resolution: "graphql-ws@npm:5.16.0" peerDependencies: graphql: ">=0.11 <=16" - checksum: 4fcd93ed75261681b1def8cd96d1db0fc650586b145325b3fc134ab9c27ed48fbedad3e8261e3f3df65758a332d0420b8c60e13abb1ee8329ef624e312b61ccc + checksum: 5e538c3460ca997a1634bd0f64236d8d7aa6ac75c58aba549b49953faf0dd2497f4fa43eedb0bc82cfff50426c7ce47682a670d2571fd7f3af5dcf00911c9e1b languageName: node linkType: hard @@ -13631,7 +14433,7 @@ __metadata: languageName: node linkType: hard -"handlebars@npm:^4.0.1": +"handlebars@npm:^4.0.1, handlebars@npm:^4.7.7": version: 4.7.8 resolution: "handlebars@npm:4.7.8" dependencies: @@ -13685,7 +14487,7 @@ __metadata: languageName: node linkType: hard -"hardhat-contract-sizer@npm:^2.0.1, hardhat-contract-sizer@npm:^2.0.3": +"hardhat-contract-sizer@npm:^2.0.1, hardhat-contract-sizer@npm:^2.0.3, hardhat-contract-sizer@npm:^2.10.0": version: 2.10.0 resolution: "hardhat-contract-sizer@npm:2.10.0" dependencies: @@ -13761,7 +14563,7 @@ __metadata: languageName: node linkType: hard -"hardhat-gas-reporter@npm:^1.0.1, hardhat-gas-reporter@npm:^1.0.4": +"hardhat-gas-reporter@npm:^1.0.1, hardhat-gas-reporter@npm:^1.0.4, hardhat-gas-reporter@npm:^1.0.8": version: 1.0.10 resolution: "hardhat-gas-reporter@npm:1.0.10" dependencies: @@ -13774,6 +14576,35 @@ __metadata: languageName: node linkType: hard +"hardhat-graph-protocol@workspace:^0.0.1, hardhat-graph-protocol@workspace:packages/hardhat-graph-protocol": + version: 0.0.0-use.local + resolution: "hardhat-graph-protocol@workspace:packages/hardhat-graph-protocol" + dependencies: + "@graphprotocol/contracts": "workspace:^7.0.0" + "@graphprotocol/horizon": "workspace:^0.0.1" + "@graphprotocol/subgraph-service": "workspace:^0.0.1" + "@nomicfoundation/hardhat-ethers": "npm:^3.0.8" + "@nomicfoundation/hardhat-verify": "npm:^2.0.12" + "@types/chai": "npm:^4.0.0" + "@types/debug": "npm:^4.1.12" + "@types/mocha": "npm:^10.0.9" + chai: "npm:^4.0.0" + debug: "npm:^4.3.7" + eslint: "npm:^8.56.0" + eslint-graph-config: "workspace:^0.0.1" + ethers: "npm:^6.13.4" + hardhat: "npm:^2.22.16" + hardhat-secure-accounts: "npm:^1.0.4" + json5: "npm:^2.2.3" + mocha: "npm:^10.8.2" + ts-node: "npm:^8.0.0" + typescript: "npm:^5.6.3" + peerDependencies: + ethers: ^6.13.4 + hardhat: ^2.22.16 + languageName: unknown + linkType: soft + "hardhat-secure-accounts@npm:0.0.5": version: 0.0.5 resolution: "hardhat-secure-accounts@npm:0.0.5" @@ -13806,6 +14637,38 @@ __metadata: languageName: node linkType: hard +"hardhat-secure-accounts@npm:^1.0.4": + version: 1.0.4 + resolution: "hardhat-secure-accounts@npm:1.0.4" + dependencies: + debug: "npm:^4.3.4" + enquirer: "npm:^2.3.6" + lodash.clonedeep: "npm:^4.5.0" + prompt-sync: "npm:^4.2.0" + peerDependencies: + "@nomicfoundation/hardhat-ethers": ^3.0.0 + ethers: ^6.13.0 + hardhat: ^2.22.0 + checksum: 6406be693502234f342b9e27215bb3ecb3502a0d9f5299c5c1f5e1be28f08d26226cff511723e719639dc12fb2e4e329386a41e5c90f57b3bf3116656c45c2b5 + languageName: node + linkType: hard + +"hardhat-secure-accounts@npm:^1.0.5": + version: 1.0.5 + resolution: "hardhat-secure-accounts@npm:1.0.5" + dependencies: + debug: "npm:^4.3.4" + enquirer: "npm:^2.3.6" + lodash.clonedeep: "npm:^4.5.0" + prompt-sync: "npm:^4.2.0" + peerDependencies: + "@nomicfoundation/hardhat-ethers": ^3.0.0 + ethers: ^6.13.0 + hardhat: ^2.22.0 + checksum: b1a30f083b1a2919d6aee2f79253edcadff70017e8006df914bba4e6ad9963db622317ca333dfce38f16a9d60ee9425c0591b578cbf8c6db52d617161d4b46ee + languageName: node + linkType: hard + "hardhat-storage-layout@npm:0.1.6": version: 0.1.6 resolution: "hardhat-storage-layout@npm:0.1.6" @@ -13817,6 +14680,17 @@ __metadata: languageName: node linkType: hard +"hardhat-storage-layout@npm:^0.1.7": + version: 0.1.7 + resolution: "hardhat-storage-layout@npm:0.1.7" + dependencies: + console-table-printer: "npm:^2.9.0" + peerDependencies: + hardhat: ^2.0.3 + checksum: 257b52a079183953d079ae221d05551391ff57adbad1ba033a3ccfa1b9df495ddd29285e67a7d03da484aa69f65850feb64a9bd7e37f53c549efd3833ed8b38c + languageName: node + linkType: hard + "hardhat-tracer@npm:^1.0.0-alpha.6": version: 1.3.0 resolution: "hardhat-tracer@npm:1.3.0" @@ -13830,23 +14704,140 @@ __metadata: languageName: node linkType: hard +"hardhat@npm:^2.22.16": + version: 2.22.16 + resolution: "hardhat@npm:2.22.16" + dependencies: + "@ethersproject/abi": "npm:^5.1.2" + "@metamask/eth-sig-util": "npm:^4.0.0" + "@nomicfoundation/edr": "npm:^0.6.4" + "@nomicfoundation/ethereumjs-common": "npm:4.0.4" + "@nomicfoundation/ethereumjs-tx": "npm:5.0.4" + "@nomicfoundation/ethereumjs-util": "npm:9.0.4" + "@nomicfoundation/solidity-analyzer": "npm:^0.1.0" + "@sentry/node": "npm:^5.18.1" + "@types/bn.js": "npm:^5.1.0" + "@types/lru-cache": "npm:^5.1.0" + adm-zip: "npm:^0.4.16" + aggregate-error: "npm:^3.0.0" + ansi-escapes: "npm:^4.3.0" + boxen: "npm:^5.1.2" + chokidar: "npm:^4.0.0" + ci-info: "npm:^2.0.0" + debug: "npm:^4.1.1" + enquirer: "npm:^2.3.0" + env-paths: "npm:^2.2.0" + ethereum-cryptography: "npm:^1.0.3" + ethereumjs-abi: "npm:^0.6.8" + find-up: "npm:^5.0.0" + fp-ts: "npm:1.19.3" + fs-extra: "npm:^7.0.1" + immutable: "npm:^4.0.0-rc.12" + io-ts: "npm:1.10.4" + json-stream-stringify: "npm:^3.1.4" + keccak: "npm:^3.0.2" + lodash: "npm:^4.17.11" + mnemonist: "npm:^0.38.0" + mocha: "npm:^10.0.0" + p-map: "npm:^4.0.0" + picocolors: "npm:^1.1.0" + raw-body: "npm:^2.4.1" + resolve: "npm:1.17.0" + semver: "npm:^6.3.0" + solc: "npm:0.8.26" + source-map-support: "npm:^0.5.13" + stacktrace-parser: "npm:^0.1.10" + tinyglobby: "npm:^0.2.6" + tsort: "npm:0.0.1" + undici: "npm:^5.14.0" + uuid: "npm:^8.3.2" + ws: "npm:^7.4.6" + peerDependencies: + ts-node: "*" + typescript: "*" + peerDependenciesMeta: + ts-node: + optional: true + typescript: + optional: true + bin: + hardhat: internal/cli/bootstrap.js + checksum: d193d8dbd02aba9875fc4df23c49fe8cf441afb63382c9e248c776c75aca6e081e9b7b75fb262739f20bff152f9e0e4112bb22e3609dfa63ed4469d3ea46c0ca + languageName: node + linkType: hard + +"hardhat@npm:^2.22.18": + version: 2.22.18 + resolution: "hardhat@npm:2.22.18" + dependencies: + "@ethersproject/abi": "npm:^5.1.2" + "@metamask/eth-sig-util": "npm:^4.0.0" + "@nomicfoundation/edr": "npm:^0.7.0" + "@nomicfoundation/ethereumjs-common": "npm:4.0.4" + "@nomicfoundation/ethereumjs-tx": "npm:5.0.4" + "@nomicfoundation/ethereumjs-util": "npm:9.0.4" + "@nomicfoundation/solidity-analyzer": "npm:^0.1.0" + "@sentry/node": "npm:^5.18.1" + "@types/bn.js": "npm:^5.1.0" + "@types/lru-cache": "npm:^5.1.0" + adm-zip: "npm:^0.4.16" + aggregate-error: "npm:^3.0.0" + ansi-escapes: "npm:^4.3.0" + boxen: "npm:^5.1.2" + chokidar: "npm:^4.0.0" + ci-info: "npm:^2.0.0" + debug: "npm:^4.1.1" + enquirer: "npm:^2.3.0" + env-paths: "npm:^2.2.0" + ethereum-cryptography: "npm:^1.0.3" + ethereumjs-abi: "npm:^0.6.8" + find-up: "npm:^5.0.0" + fp-ts: "npm:1.19.3" + fs-extra: "npm:^7.0.1" + immutable: "npm:^4.0.0-rc.12" + io-ts: "npm:1.10.4" + json-stream-stringify: "npm:^3.1.4" + keccak: "npm:^3.0.2" + lodash: "npm:^4.17.11" + mnemonist: "npm:^0.38.0" + mocha: "npm:^10.0.0" + p-map: "npm:^4.0.0" + picocolors: "npm:^1.1.0" + raw-body: "npm:^2.4.1" + resolve: "npm:1.17.0" + semver: "npm:^6.3.0" + solc: "npm:0.8.26" + source-map-support: "npm:^0.5.13" + stacktrace-parser: "npm:^0.1.10" + tinyglobby: "npm:^0.2.6" + tsort: "npm:0.0.1" + undici: "npm:^5.14.0" + uuid: "npm:^8.3.2" + ws: "npm:^7.4.6" + peerDependencies: + ts-node: "*" + typescript: "*" + peerDependenciesMeta: + ts-node: + optional: true + typescript: + optional: true + bin: + hardhat: internal/cli/bootstrap.js + checksum: cd2fd8972b24d13a342747129e88bfe8bad45432ad88c66c743e81615e1c5db7d656c3e9748c03e517c94f6f6df717c4a14685c82c9f843c9be7c1e0a5f76c49 + languageName: node + linkType: hard + "hardhat@npm:^2.6.1, hardhat@npm:^2.6.4": - version: 2.20.1 - resolution: "hardhat@npm:2.20.1" + version: 2.22.4 + resolution: "hardhat@npm:2.22.4" dependencies: "@ethersproject/abi": "npm:^5.1.2" "@metamask/eth-sig-util": "npm:^4.0.0" - "@nomicfoundation/ethereumjs-block": "npm:5.0.4" - "@nomicfoundation/ethereumjs-blockchain": "npm:7.0.4" + "@nomicfoundation/edr": "npm:^0.3.7" "@nomicfoundation/ethereumjs-common": "npm:4.0.4" - "@nomicfoundation/ethereumjs-evm": "npm:2.0.4" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.4" - "@nomicfoundation/ethereumjs-statemanager": "npm:2.0.4" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.4" "@nomicfoundation/ethereumjs-tx": "npm:5.0.4" "@nomicfoundation/ethereumjs-util": "npm:9.0.4" - "@nomicfoundation/ethereumjs-verkle": "npm:0.0.2" - "@nomicfoundation/ethereumjs-vm": "npm:7.0.4" "@nomicfoundation/solidity-analyzer": "npm:^0.1.0" "@sentry/node": "npm:^5.18.1" "@types/bn.js": "npm:^5.1.0" @@ -13894,7 +14885,7 @@ __metadata: optional: true bin: hardhat: internal/cli/bootstrap.js - checksum: e27f1fc6b016d7ceb62795ff47384a02ad61722cdd2ab55c91d7ff2ce31973a1ea5e462431c7033ca53e66e4722c3597664b0ebaac70104622d227e94b4fb3e0 + checksum: b9f51c40bc0d392dedb7fe866da30115a9c120b89eafe1f0afcda3cf1b379d7163e6a33a9b7a8ba78df795b477c2fd2d83ace8eb992668271b610786e320d5b7 languageName: node linkType: hard @@ -14010,7 +15001,7 @@ __metadata: languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.1, has-property-descriptors@npm:^1.0.2": +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": version: 1.0.2 resolution: "has-property-descriptors@npm:1.0.2" dependencies: @@ -14033,7 +15024,7 @@ __metadata: languageName: node linkType: hard -"has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.1, has-tostringtag@npm:^1.0.2": +"has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": version: 1.0.2 resolution: "has-tostringtag@npm:1.0.2" dependencies: @@ -14099,6 +15090,16 @@ __metadata: languageName: node linkType: hard +"hash-base@npm:~3.0": + version: 3.0.4 + resolution: "hash-base@npm:3.0.4" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: a13357dccb3827f0bb0b56bf928da85c428dc8670f6e4a1c7265e4f1653ce02d69030b40fd01b0f1d218a995a066eea279cded9cec72d207b593bcdfe309c2f0 + languageName: node + linkType: hard + "hash-it@npm:^6.0.0": version: 6.0.0 resolution: "hash-it@npm:6.0.0" @@ -14126,12 +15127,12 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.0, hasown@npm:^2.0.1": - version: 2.0.1 - resolution: "hasown@npm:2.0.1" +"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" dependencies: function-bind: "npm:^1.1.2" - checksum: 9e27e70e8e4204f4124c8f99950d1ba2b1f5174864fd39ff26da190f9ea6488c1b3927dcc64981c26d1f637a971783c9489d62c829d393ea509e6f1ba20370bb + checksum: 3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 languageName: node linkType: hard @@ -14144,7 +15145,7 @@ __metadata: languageName: node linkType: hard -"he@npm:1.2.0": +"he@npm:1.2.0, he@npm:^1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" bin: @@ -14170,6 +15171,13 @@ __metadata: languageName: node linkType: hard +"heap@npm:>= 0.2.0": + version: 0.2.7 + resolution: "heap@npm:0.2.7" + checksum: 341c5d51ae13dc8346c371a8a69c57c972fcb9a3233090d3dd5ba29d483d6b5b4e75492443cbfeacd46608bb30e6680f646ffb7a6205900221735587d07a79b6 + languageName: node + linkType: hard + "helmet@npm:5.0.2": version: 5.0.2 resolution: "helmet@npm:5.0.2" @@ -14205,6 +15213,15 @@ __metadata: languageName: node linkType: hard +"homedir-polyfill@npm:^1.0.1": + version: 1.0.3 + resolution: "homedir-polyfill@npm:1.0.3" + dependencies: + parse-passwd: "npm:^1.0.0" + checksum: 3c099844f94b8b438f124bd5698bdcfef32b2d455115fb8050d7148e7f7b95fc89ba9922586c491f0e1cdebf437b1053c84ecddb8d596e109e9ac69c5b4a9e27 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4, hosted-git-info@npm:^2.6.0": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -14371,6 +15388,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^5.0.0": + version: 5.0.0 + resolution: "human-signals@npm:5.0.0" + checksum: 5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82 + languageName: node + linkType: hard + "husky@npm:^7.0.4": version: 7.0.4 resolution: "husky@npm:7.0.4" @@ -14430,7 +15454,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.2.4": +"ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": version: 5.3.1 resolution: "ignore@npm:5.3.1" checksum: 703f7f45ffb2a27fb2c5a8db0c32e7dee66b33a225d28e8db4e1be6474795f606686a6e3bcc50e1aa12f2042db4c9d4a7d60af3250511de74620fbed052ea4cd @@ -14458,10 +15482,17 @@ __metadata: languageName: node linkType: hard +"immer@npm:10.0.2": + version: 10.0.2 + resolution: "immer@npm:10.0.2" + checksum: b6c23538cd174a4cadd6f8d92bf0245e2c2a7bdabbd3200a08f1e99bb52e463fb552bb2d025ddd45f4e335390f8bd307e2c813e54a004dd651fe1ec161674e42 + languageName: node + linkType: hard + "immutable@npm:^4.0.0-rc.12": - version: 4.3.5 - resolution: "immutable@npm:4.3.5" - checksum: 63d2d7908241a955d18c7822fd2215b6e89ff5a1a33cc72cd475b013cbbdef7a705aa5170a51ce9f84a57f62fdddfaa34e7b5a14b33d8a43c65cc6a881d6e894 + version: 4.3.6 + resolution: "immutable@npm:4.3.6" + checksum: 7d0952a768b4fadcee47230ed86dc9505a4517095eceaf5a47e65288571c42400c6e4a2ae21eca4eda957cb7bc50720213135b62cf6a181639111f8acae128c3 languageName: node linkType: hard @@ -14925,6 +15956,15 @@ __metadata: languageName: node linkType: hard +"is-data-view@npm:^1.0.1": + version: 1.0.1 + resolution: "is-data-view@npm:1.0.1" + dependencies: + is-typed-array: "npm:^1.1.13" + checksum: a3e6ec84efe303da859107aed9b970e018e2bee7ffcb48e2f8096921a493608134240e672a2072577e5f23a729846241d9634806e8a0e51d9129c56d5f65442d + languageName: node + linkType: hard + "is-date-object@npm:^1.0.1, is-date-object@npm:^1.0.5": version: 1.0.5 resolution: "is-date-object@npm:1.0.5" @@ -15060,6 +16100,15 @@ __metadata: languageName: node linkType: hard +"is-fullwidth-code-point@npm:^5.0.0": + version: 5.0.0 + resolution: "is-fullwidth-code-point@npm:5.0.0" + dependencies: + get-east-asian-width: "npm:^1.0.0" + checksum: cd591b27d43d76b05fa65ed03eddce57a16e1eca0b7797ff7255de97019bcaf0219acfc0c4f7af13319e13541f2a53c0ace476f442b13267b9a6a7568f2b65c8 + languageName: node + linkType: hard + "is-function@npm:^1.0.1": version: 1.0.2 resolution: "is-function@npm:1.0.2" @@ -15133,7 +16182,7 @@ __metadata: languageName: node linkType: hard -"is-negative-zero@npm:^2.0.2": +"is-negative-zero@npm:^2.0.3": version: 2.0.3 resolution: "is-negative-zero@npm:2.0.3" checksum: bcdcf6b8b9714063ffcfa9929c575ac69bfdabb8f4574ff557dfc086df2836cf07e3906f5bbc4f2a5c12f8f3ba56af640c843cdfc74da8caed86c7c7d66fd08e @@ -15265,7 +16314,7 @@ __metadata: languageName: node linkType: hard -"is-shared-array-buffer@npm:^1.0.2": +"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.3": version: 1.0.3 resolution: "is-shared-array-buffer@npm:1.0.3" dependencies: @@ -15288,6 +16337,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8 + languageName: node + linkType: hard + "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -15607,7 +16663,7 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^2.3.5": +"jackspeak@npm:^2.3.6": version: 2.3.6 resolution: "jackspeak@npm:2.3.6" dependencies: @@ -15715,6 +16771,13 @@ __metadata: languageName: node linkType: hard +"jsel@npm:^1.1.6": + version: 1.1.6 + resolution: "jsel@npm:1.1.6" + checksum: de16e6b0f203f4143424dc2298a2aa48482fa880e329d552743dfa81a2dd34f75e6664e1aac768a638bbd48cdd0f8ec58d6a5aae7647b4b946473d6dd2475d19 + languageName: node + linkType: hard + "jsesc@npm:^1.3.0": version: 1.3.0 resolution: "jsesc@npm:1.3.0" @@ -15867,7 +16930,14 @@ __metadata: languageName: node linkType: hard -"json-stringify-safe@npm:~5.0.1": +"json-stream-stringify@npm:^3.1.4": + version: 3.1.6 + resolution: "json-stream-stringify@npm:3.1.6" + checksum: cb45e65143f4634ebb2dc0732410a942eaf86f88a7938b2f6397f4c6b96a7ba936e74d4d17db48c9221f669153996362b2ff50fe8c7fed8a7548646f98ae1f58 + languageName: node + linkType: hard + +"json-stringify-safe@npm:^5.0.1, json-stringify-safe@npm:~5.0.1": version: 5.0.1 resolution: "json-stringify-safe@npm:5.0.1" checksum: 7dbf35cd0411d1d648dceb6d59ce5857ec939e52e4afc37601aa3da611f0987d5cee5b38d58329ceddf3ed48bd7215229c8d52059ab01f2444a338bf24ed0f37 @@ -16333,6 +17403,13 @@ __metadata: languageName: node linkType: hard +"lilconfig@npm:3.0.0": + version: 3.0.0 + resolution: "lilconfig@npm:3.0.0" + checksum: 7f5ee7a658dc016cacf146815e8d88b06f06f4402823b8b0934e305a57a197f55ccc9c5cd4fb5ea1b2b821c8ccaf2d54abd59602a4931af06eabda332388d3e6 + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -16389,6 +17466,40 @@ __metadata: languageName: node linkType: hard +"lint-staged@npm:^15.2.2": + version: 15.2.2 + resolution: "lint-staged@npm:15.2.2" + dependencies: + chalk: "npm:5.3.0" + commander: "npm:11.1.0" + debug: "npm:4.3.4" + execa: "npm:8.0.1" + lilconfig: "npm:3.0.0" + listr2: "npm:8.0.1" + micromatch: "npm:4.0.5" + pidtree: "npm:0.6.0" + string-argv: "npm:0.3.2" + yaml: "npm:2.3.4" + bin: + lint-staged: bin/lint-staged.js + checksum: a1ba6c7ee53e30a0f6ea9a351d95d3d0d2be916a41b561e22907e9ea513eb18cb3dbe65bff3ec13fad15777999efe56b2e2a95427e31d12a9b7e7948c3630ee2 + languageName: node + linkType: hard + +"listr2@npm:8.0.1": + version: 8.0.1 + resolution: "listr2@npm:8.0.1" + dependencies: + cli-truncate: "npm:^4.0.0" + colorette: "npm:^2.0.20" + eventemitter3: "npm:^5.0.1" + log-update: "npm:^6.0.0" + rfdc: "npm:^1.3.0" + wrap-ansi: "npm:^9.0.0" + checksum: b565d6ceb3a4c2dbe0c1735c0fd907afd0d6f89de21aced8e05187b2d88ca2f8f9ebc5d743885396a00f05f13146f6be744d098a56ce0402cf1cd131485a7ff1 + languageName: node + linkType: hard + "listr2@npm:^3.2.2": version: 3.14.0 resolution: "listr2@npm:3.14.0" @@ -16626,7 +17737,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.14.2, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:~4.17.0": +"lodash@npm:4.17.21, lodash@npm:^4.14.2, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:~4.17.0": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -16655,6 +17766,19 @@ __metadata: languageName: node linkType: hard +"log-update@npm:^6.0.0": + version: 6.0.0 + resolution: "log-update@npm:6.0.0" + dependencies: + ansi-escapes: "npm:^6.2.0" + cli-cursor: "npm:^4.0.0" + slice-ansi: "npm:^7.0.0" + strip-ansi: "npm:^7.1.0" + wrap-ansi: "npm:^9.0.0" + checksum: e0b3c3401ef49ce3eb17e2f83d644765e4f7988498fc1344eaa4f31ab30e510dcc469a7fb64dc01bd1c8d9237d917598fa677a9818705fb3774c10f6e9d4b27c + languageName: node + linkType: hard + "logform@npm:^2.3.2, logform@npm:^2.4.0": version: 2.6.0 resolution: "logform@npm:2.6.0" @@ -16751,10 +17875,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.2.0 - resolution: "lru-cache@npm:10.2.0" - checksum: c9847612aa2daaef102d30542a8d6d9b2c2bb36581c1bf0dc3ebf5e5f3352c772a749e604afae2e46873b930a9e9523743faac4e5b937c576ab29196774712ee +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.2.2 + resolution: "lru-cache@npm:10.2.2" + checksum: 402d31094335851220d0b00985084288136136992979d0e015f0f1697e15d1c86052d7d53ae86b614e5b058425606efffc6969a31a091085d7a2b80a8a1e26d6 languageName: node linkType: hard @@ -16822,8 +17946,8 @@ __metadata: linkType: hard "make-fetch-happen@npm:^13.0.0": - version: 13.0.0 - resolution: "make-fetch-happen@npm:13.0.0" + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" dependencies: "@npmcli/agent": "npm:^2.0.0" cacache: "npm:^18.0.0" @@ -16834,9 +17958,10 @@ __metadata: minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" promise-retry: "npm:^2.0.1" ssri: "npm:^10.0.0" - checksum: 43b9f6dcbc6fe8b8604cb6396957c3698857a15ba4dbc38284f7f0e61f248300585ef1eb8cc62df54e9c724af977e45b5cdfd88320ef7f53e45070ed3488da55 + checksum: df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e languageName: node linkType: hard @@ -17103,6 +18228,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:4.0.5, micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" + dependencies: + braces: "npm:^3.0.2" + picomatch: "npm:^2.3.1" + checksum: 3d6505b20f9fa804af5d8c596cb1c5e475b9b0cd05f652c5b56141cf941bd72adaeb7a436fda344235cef93a7f29b7472efc779fcdb83b478eab0867b95cdeff + languageName: node + linkType: hard + "micromatch@npm:^2.1.5": version: 2.3.11 resolution: "micromatch@npm:2.3.11" @@ -17141,17 +18276,7 @@ __metadata: regex-not: "npm:^1.0.0" snapdragon: "npm:^0.8.1" to-regex: "npm:^3.0.2" - checksum: 531a32e7ac92bef60657820202be71b63d0f945c08a69cc4c239c0b19372b751483d464a850a2e3a5ff6cc9060641e43d44c303af104c1a27493d137d8af017f - languageName: node - linkType: hard - -"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": - version: 4.0.5 - resolution: "micromatch@npm:4.0.5" - dependencies: - braces: "npm:^3.0.2" - picomatch: "npm:^2.3.1" - checksum: 3d6505b20f9fa804af5d8c596cb1c5e475b9b0cd05f652c5b56141cf941bd72adaeb7a436fda344235cef93a7f29b7472efc779fcdb83b478eab0867b95cdeff + checksum: 531a32e7ac92bef60657820202be71b63d0f945c08a69cc4c239c0b19372b751483d464a850a2e3a5ff6cc9060641e43d44c303af104c1a27493d137d8af017f languageName: node linkType: hard @@ -17206,6 +18331,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf + languageName: node + linkType: hard + "mimic-response@npm:^1.0.0, mimic-response@npm:^1.0.1": version: 1.0.1 resolution: "mimic-response@npm:1.0.1" @@ -17257,12 +18389,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:*, minimatch@npm:9.0.3, minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" +"minimatch@npm:*, minimatch@npm:^9.0.1, minimatch@npm:^9.0.4": + version: 9.0.4 + resolution: "minimatch@npm:9.0.4" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 85f407dcd38ac3e180f425e86553911d101455ca3ad5544d6a7cec16286657e4f8a9aa6695803025c55e31e35a91a2252b5dc8e7d527211278b8b65b4dbd5eac + checksum: 2c16f21f50e64922864e560ff97c587d15fd491f65d92a677a344e970fe62aafdbeafe648965fa96d33c061b4d0eabfe0213466203dd793367e7f28658cf6414 languageName: node linkType: hard @@ -17284,7 +18416,16 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1": +"minimatch@npm:9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 85f407dcd38ac3e180f425e86553911d101455ca3ad5544d6a7cec16286657e4f8a9aa6695803025c55e31e35a91a2252b5dc8e7d527211278b8b65b4dbd5eac + languageName: node + linkType: hard + +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.6": version: 5.1.6 resolution: "minimatch@npm:5.1.6" dependencies: @@ -17328,8 +18469,8 @@ __metadata: linkType: hard "minipass-fetch@npm:^3.0.0": - version: 3.0.4 - resolution: "minipass-fetch@npm:3.0.4" + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" @@ -17338,7 +18479,7 @@ __metadata: dependenciesMeta: encoding: optional: true - checksum: 1b63c1f3313e88eeac4689f1b71c9f086598db9a189400e3ee960c32ed89e06737fa23976c9305c2d57464fb3fcdc12749d3378805c9d6176f5569b0d0ee8a75 + checksum: 9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b languageName: node linkType: hard @@ -17395,10 +18536,10 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": - version: 7.0.4 - resolution: "minipass@npm:7.0.4" - checksum: 6c7370a6dfd257bf18222da581ba89a5eaedca10e158781232a8b5542a90547540b4b9b7e7f490e4cda43acfbd12e086f0453728ecf8c19e0ef6921bc5958ac5 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": + version: 7.1.1 + resolution: "minipass@npm:7.1.1" + checksum: fdccc2f99c31083f45f881fd1e6971d798e333e078ab3c8988fb818c470fbd5e935388ad9adb286397eba50baebf46ef8ff487c8d3f455a69c6f3efc327bdff9 languageName: node linkType: hard @@ -17497,8 +18638,8 @@ __metadata: linkType: hard "mocha@npm:^10.0.0, mocha@npm:^10.2.0": - version: 10.3.0 - resolution: "mocha@npm:10.3.0" + version: 10.4.0 + resolution: "mocha@npm:10.4.0" dependencies: ansi-colors: "npm:4.1.1" browser-stdout: "npm:1.3.1" @@ -17523,7 +18664,38 @@ __metadata: bin: _mocha: bin/_mocha mocha: bin/mocha.js - checksum: 8dc93842468b2be5f820e5eb64208fb68ba3e5ee90cfe21a9f1d439f9ec031e8a8dc97f4d3206a376c9e05141cf689a812aedcf4545f71f69b3e9a51f312ec4a + checksum: e572e9d8c164e98f64de7e9498608de042fd841c6a7441f456a5e216e9aed2299e2c568d9dc27f2be2de06521e6b2d1dd774ab58a243b1c7697d14aec2f0f7f7 + languageName: node + linkType: hard + +"mocha@npm:^10.8.2": + version: 10.8.2 + resolution: "mocha@npm:10.8.2" + dependencies: + ansi-colors: "npm:^4.1.3" + browser-stdout: "npm:^1.3.1" + chokidar: "npm:^3.5.3" + debug: "npm:^4.3.5" + diff: "npm:^5.2.0" + escape-string-regexp: "npm:^4.0.0" + find-up: "npm:^5.0.0" + glob: "npm:^8.1.0" + he: "npm:^1.2.0" + js-yaml: "npm:^4.1.0" + log-symbols: "npm:^4.1.0" + minimatch: "npm:^5.1.6" + ms: "npm:^2.1.3" + serialize-javascript: "npm:^6.0.2" + strip-json-comments: "npm:^3.1.1" + supports-color: "npm:^8.1.1" + workerpool: "npm:^6.5.1" + yargs: "npm:^16.2.0" + yargs-parser: "npm:^20.2.9" + yargs-unparser: "npm:^2.0.0" + bin: + _mocha: bin/_mocha + mocha: bin/mocha.js + checksum: 1f786290a32a1c234f66afe2bfcc68aa50fe9c7356506bd39cca267efb0b4714a63a0cb333815578d63785ba2fba058bf576c2512db73997c0cae0d659a88beb languageName: node linkType: hard @@ -17619,7 +18791,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 @@ -17875,6 +19047,21 @@ __metadata: languageName: node linkType: hard +"ndjson@npm:2.0.0": + version: 2.0.0 + resolution: "ndjson@npm:2.0.0" + dependencies: + json-stringify-safe: "npm:^5.0.1" + minimist: "npm:^1.2.5" + readable-stream: "npm:^3.6.0" + split2: "npm:^3.0.0" + through2: "npm:^4.0.0" + bin: + ndjson: cli.js + checksum: b7f3de5e12e0466cfa3688a3ba6cedec0ab54bd821f1b16926c9ef7017983b131832430061d25dfcb635f65a254b535681eca213c6feb5d1958bee8d35a04cc9 + languageName: node + linkType: hard + "negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" @@ -17977,19 +19164,19 @@ __metadata: linkType: hard "node-gyp-build@npm:^4.2.0, node-gyp-build@npm:^4.3.0": - version: 4.8.0 - resolution: "node-gyp-build@npm:4.8.0" + version: 4.8.1 + resolution: "node-gyp-build@npm:4.8.1" bin: node-gyp-build: bin.js node-gyp-build-optional: optional.js node-gyp-build-test: build-test.js - checksum: 85324be16f81f0235cbbc42e3eceaeb1b5ab94c8d8f5236755e1435b4908338c65a4e75f66ee343cbcb44ddf9b52a428755bec16dcd983295be4458d95c8e1ad + checksum: e36ca3d2adf2b9cca316695d7687207c19ac6ed326d6d7c68d7112cebe0de4f82d6733dff139132539fcc01cf5761f6c9082a21864ab9172edf84282bc849ce7 languageName: node linkType: hard "node-gyp@npm:latest": - version: 10.0.1 - resolution: "node-gyp@npm:10.0.1" + version: 10.1.0 + resolution: "node-gyp@npm:10.1.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" @@ -18003,7 +19190,7 @@ __metadata: which: "npm:^4.0.0" bin: node-gyp: bin/node-gyp.js - checksum: abddfff7d873312e4ed4a5fb75ce893a5c4fb69e7fcb1dfa71c28a6b92a7f1ef6b62790dffb39181b5a82728ba8f2f32d229cf8cbe66769fe02cea7db4a555aa + checksum: 9cc821111ca244a01fb7f054db7523ab0a0cd837f665267eb962eb87695d71fb1e681f9e21464cc2fd7c05530dc4c81b810bca1a88f7d7186909b74477491a3c languageName: node linkType: hard @@ -18040,13 +19227,13 @@ __metadata: linkType: hard "nopt@npm:^7.0.0": - version: 7.2.0 - resolution: "nopt@npm:7.2.0" + version: 7.2.1 + resolution: "nopt@npm:7.2.1" dependencies: abbrev: "npm:^2.0.0" bin: nopt: bin/nopt.js - checksum: 9bd7198df6f16eb29ff16892c77bcf7f0cc41f9fb5c26280ac0def2cf8cf319f3b821b3af83eba0e74c85807cc430a16efe0db58fe6ae1f41e69519f585b6aff + checksum: a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 languageName: node linkType: hard @@ -18105,9 +19292,9 @@ __metadata: linkType: hard "normalize-url@npm:^8.0.0": - version: 8.0.0 - resolution: "normalize-url@npm:8.0.0" - checksum: 09582d56acd562d89849d9239852c2aff225c72be726556d6883ff36de50006803d32a023c10e917bcc1c55f73f3bb16434f67992fe9b61906a3db882192753c + version: 8.0.1 + resolution: "normalize-url@npm:8.0.1" + checksum: eb439231c4b84430f187530e6fdac605c5048ef4ec556447a10c00a91fc69b52d8d8298d9d608e68d3e0f7dc2d812d3455edf425e0f215993667c3183bcab1ef languageName: node linkType: hard @@ -18129,6 +19316,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^5.1.0": + version: 5.3.0 + resolution: "npm-run-path@npm:5.3.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: 124df74820c40c2eb9a8612a254ea1d557ddfab1581c3e751f825e3e366d9f00b0d76a3c94ecd8398e7f3eee193018622677e95816e8491f0797b21e30b2deba + languageName: node + linkType: hard + "nullthrows@npm:^1.1.1": version: 1.1.1 resolution: "nullthrows@npm:1.1.1" @@ -18200,12 +19396,12 @@ __metadata: linkType: hard "object-is@npm:^1.1.5": - version: 1.1.5 - resolution: "object-is@npm:1.1.5" + version: 1.1.6 + resolution: "object-is@npm:1.1.6" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.1.3" - checksum: 8c263fb03fc28f1ffb54b44b9147235c5e233dc1ca23768e7d2569740b5d860154d7cc29a30220fe28ed6d8008e2422aefdebfe987c103e1c5d190cf02d9d886 + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + checksum: 506af444c4dce7f8e31f34fc549e2fb8152d6b9c4a30c6e62852badd7f520b579c679af433e7a072f9d78eb7808d230dc12e1cf58da9154dfbf8813099ea0fe0 languageName: node linkType: hard @@ -18245,15 +19441,17 @@ __metadata: linkType: hard "object.getownpropertydescriptors@npm:^2.1.6": - version: 2.1.7 - resolution: "object.getownpropertydescriptors@npm:2.1.7" + version: 2.1.8 + resolution: "object.getownpropertydescriptors@npm:2.1.8" dependencies: array.prototype.reduce: "npm:^1.0.6" - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - safe-array-concat: "npm:^1.0.0" - checksum: 519c4eb47bd30dad1385994dbea59408c25f4bff68b29d918267091f3d597d39b04557691e94ee385fd9af7f191daffa59954e19c6f1e53215d6910d386005a2 + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + es-object-atoms: "npm:^1.0.0" + gopd: "npm:^1.0.1" + safe-array-concat: "npm:^1.1.2" + checksum: 553e9562fd86637c9c169df23a56f1d810d8c9b580a6d4be11552c009f32469310c9347f3d10325abf0cd9cfe4afc521a1e903fbd24148ae7ec860e1e7c75cf3 languageName: node linkType: hard @@ -18360,6 +19558,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c + languageName: node + linkType: hard + "open@npm:^7.4.2": version: 7.4.2 resolution: "open@npm:7.4.2" @@ -18403,16 +19610,16 @@ __metadata: linkType: hard "optionator@npm:^0.9.3": - version: 0.9.3 - resolution: "optionator@npm:0.9.3" + version: 0.9.4 + resolution: "optionator@npm:0.9.4" dependencies: - "@aashutoshrathi/word-wrap": "npm:^1.2.3" deep-is: "npm:^0.1.3" fast-levenshtein: "npm:^2.0.6" levn: "npm:^0.4.1" prelude-ls: "npm:^1.2.1" type-check: "npm:^0.4.0" - checksum: 66fba794d425b5be51353035cf3167ce6cfa049059cbb93229b819167687e0f48d2bc4603fcb21b091c99acb516aae1083624675b15c4765b2e4693a085e959c + word-wrap: "npm:^1.2.5" + checksum: 4afb687a059ee65b61df74dfe87d8d6815cd6883cb8b3d5883a910df72d0f5d029821f37025e4bccf4048873dbdb09acc6d303d27b8f76b1a80dd5a7d5334675 languageName: node linkType: hard @@ -18433,6 +19640,13 @@ __metadata: languageName: node linkType: hard +"ordinal@npm:^1.0.3": + version: 1.0.3 + resolution: "ordinal@npm:1.0.3" + checksum: faa276fc1b1660477fd5c8749323c9715ae4f482c21fb8e67e57d1eb57845ba1b902796ecdcf6405325a8c3b042360970f5dc3b7f8cc7d79e0b2a756ab09174d + languageName: node + linkType: hard + "os-homedir@npm:^1.0.0": version: 1.0.2 resolution: "os-homedir@npm:1.0.2" @@ -18669,16 +19883,17 @@ __metadata: languageName: node linkType: hard -"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.6": - version: 5.1.6 - resolution: "parse-asn1@npm:5.1.6" +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7": + version: 5.1.7 + resolution: "parse-asn1@npm:5.1.7" dependencies: - asn1.js: "npm:^5.2.0" - browserify-aes: "npm:^1.0.0" - evp_bytestokey: "npm:^1.0.0" - pbkdf2: "npm:^3.0.3" - safe-buffer: "npm:^5.1.1" - checksum: 4ed1d9b9e120c5484d29d67bb90171aac0b73422bc016d6294160aea983275c28a27ab85d862059a36a86a97dd31b7ddd97486802ca9fac67115fe3409e9dcbd + asn1.js: "npm:^4.10.1" + browserify-aes: "npm:^1.2.0" + evp_bytestokey: "npm:^1.0.3" + hash-base: "npm:~3.0" + pbkdf2: "npm:^3.1.2" + safe-buffer: "npm:^5.2.1" + checksum: 05eb5937405c904eb5a7f3633bab1acc11f4ae3478a07ef5c6d81ce88c3c0e505ff51f9c7b935ebc1265c868343793698fc91025755a895d0276f620f95e8a82 languageName: node linkType: hard @@ -18747,6 +19962,13 @@ __metadata: languageName: node linkType: hard +"parse-passwd@npm:^1.0.0": + version: 1.0.0 + resolution: "parse-passwd@npm:1.0.0" + checksum: 1c05c05f95f184ab9ca604841d78e4fe3294d46b8e3641d305dcc28e930da0e14e602dbda9f3811cd48df5b0e2e27dbef7357bf0d7c40e41b18c11c3a8b8d17b + languageName: node + linkType: hard + "parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -18878,6 +20100,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 + languageName: node + linkType: hard + "path-parse@npm:^1.0.6, path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -18901,13 +20130,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.10.1": - version: 1.10.1 - resolution: "path-scurry@npm:1.10.1" +"path-scurry@npm:^1.11.0": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" dependencies: - lru-cache: "npm:^9.1.1 || ^10.0.0" + lru-cache: "npm:^10.2.0" minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: e5dc78a7348d25eec61ab166317e9e9c7b46818aa2c2b9006c507a6ff48c672d011292d9662527213e558f5652ce0afcc788663a061d8b59ab495681840c0c1e + checksum: 32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d languageName: node linkType: hard @@ -18950,7 +20179,7 @@ __metadata: languageName: node linkType: hard -"pbkdf2@npm:^3.0.17, pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.0.9": +"pbkdf2@npm:^3.0.17, pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.0.9, pbkdf2@npm:^3.1.2": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" dependencies: @@ -18987,9 +20216,9 @@ __metadata: linkType: hard "pg-connection-string@npm:^2.5.0, pg-connection-string@npm:^2.6.1, pg-connection-string@npm:^2.6.2": - version: 2.6.2 - resolution: "pg-connection-string@npm:2.6.2" - checksum: e8fdea74fcc8bdc3d7c5c6eadd9425fdba7e67fb7fe836f9c0cecad94c8984e435256657d1d8ce0483d1fedef667e7a57e32449a63cb805cb0289fc34b62da35 + version: 2.6.4 + resolution: "pg-connection-string@npm:2.6.4" + checksum: 0d0b617df0fc6507bf6a94bdcd56c7a305788a1402d69bff9773350947c8f525d6d8136128065370749a3325e99658ae40fbdcce620fb8e60126181f0591a6a6 languageName: node linkType: hard @@ -19010,18 +20239,18 @@ __metadata: linkType: hard "pg-pool@npm:^3.5.1, pg-pool@npm:^3.6.1": - version: 3.6.1 - resolution: "pg-pool@npm:3.6.1" + version: 3.6.2 + resolution: "pg-pool@npm:3.6.2" peerDependencies: pg: ">=8.0" - checksum: 47837c4e4c2b9e195cec01bd58b6e276acc915537191707ad4d6ed975fd9bc03c73f63cb7fde4cb0e08ed059e35faf60fbd03744dee3af71d4b4631ab40eeb7f + checksum: 14c524549490954b5e48457a4b808df8f619f6deeb3b395b0cd184a8f4ed65a9273fe0697ba0341a41d6745af197f1437eb1cf51fff0cbbf5b0fb3852ebe5392 languageName: node linkType: hard "pg-protocol@npm:^1.5.0, pg-protocol@npm:^1.6.0": - version: 1.6.0 - resolution: "pg-protocol@npm:1.6.0" - checksum: 318a4d1e9cebd3927b10a8bc412f5017117a1f9a5fafb628d75847da7d1ab81c33250de58596bd0990029e14e92a995a851286d60fc236692299faf509572213 + version: 1.6.1 + resolution: "pg-protocol@npm:1.6.1" + checksum: 7eadef4010ac0a3925c460be7332ca4098a5c6d5181725a62193fcfa800000ae6632d98d814f3989b42cf5fdc3b45e34c714a1959d29174e81e30730e140ae5f languageName: node linkType: hard @@ -19091,10 +20320,17 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0": - version: 1.0.0 - resolution: "picocolors@npm:1.0.0" - checksum: 20a5b249e331c14479d94ec6817a182fd7a5680debae82705747b2db7ec50009a5f6648d0621c561b0572703f84dbef0858abcbd5856d3c5511426afcb1961f7 +"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1": + version: 1.0.1 + resolution: "picocolors@npm:1.0.1" + checksum: c63cdad2bf812ef0d66c8db29583802355d4ca67b9285d846f390cc15c2f6ccb94e8cb7eb6a6e97fc5990a6d3ad4ae42d86c84d3146e667c739a4234ed50d400 + languageName: node + linkType: hard + +"picocolors@npm:^1.1.0": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 languageName: node linkType: hard @@ -19105,10 +20341,19 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^4.0.1": - version: 4.0.1 - resolution: "picomatch@npm:4.0.1" - checksum: a036a085b18b376493e8ccef155bb03c65a2be7203582b717bb0498d1446e6a80f7f86a36e07877590abd0431f26c64c6154058c31f4f46105d3686a34fa3cf6 +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc + languageName: node + linkType: hard + +"pidtree@npm:0.6.0": + version: 0.6.0 + resolution: "pidtree@npm:0.6.0" + bin: + pidtree: bin/pidtree.js + checksum: 0829ec4e9209e230f74ebf4265f5ccc9ebfb488334b525cb13f86ff801dca44b362c41252cd43ae4d7653a10a5c6ab3be39d2c79064d6895e0d78dc50a5ed6e9 languageName: node linkType: hard @@ -19363,7 +20608,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.1.1, prettier@npm:^2.1.2, prettier@npm:^2.7.1, prettier@npm:^2.8.3": +"prettier@npm:^2.1.1, prettier@npm:^2.1.2, prettier@npm:^2.3.1, prettier@npm:^2.7.1, prettier@npm:^2.8.3": version: 2.8.8 resolution: "prettier@npm:2.8.8" bin: @@ -19395,6 +20640,13 @@ __metadata: languageName: node linkType: hard +"proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -19731,11 +20983,11 @@ __metadata: linkType: hard "qs@npm:^6.11.2, qs@npm:^6.4.0, qs@npm:^6.7.0, qs@npm:^6.9.4": - version: 6.11.2 - resolution: "qs@npm:6.11.2" + version: 6.12.1 + resolution: "qs@npm:6.12.1" dependencies: - side-channel: "npm:^1.0.4" - checksum: 4f95d4ff18ed480befcafa3390022817ffd3087fc65f146cceb40fc5edb9fa96cb31f648cae2fa96ca23818f0798bd63ad4ca369a0e22702fcd41379b3ab6571 + side-channel: "npm:^1.0.6" + checksum: 439e6d7c6583e7c69f2cab2c39c55b97db7ce576e4c7c469082b938b7fc8746e8d547baacb69b4cd2b6666484776c3f4840ad7163a4c5326300b0afa0acdd84b languageName: node linkType: hard @@ -19963,7 +21215,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:2 || 3, readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": +"readable-stream@npm:2 || 3, readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -19986,7 +21238,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.2.8, readable-stream@npm:^2.2.9, readable-stream@npm:^2.3.6, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.2.8, readable-stream@npm:^2.2.9, readable-stream@npm:^2.3.6, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -20024,6 +21276,13 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^4.0.1": + version: 4.0.2 + resolution: "readdirp@npm:4.0.2" + checksum: a16ecd8ef3286dcd90648c3b103e3826db2b766cdb4a988752c43a83f683d01c7059158d623cbcd8bdfb39e65d302d285be2d208e7d9f34d022d912b929217dd + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -20330,6 +21589,16 @@ __metadata: languageName: node linkType: hard +"resolve-dir@npm:^1.0.0, resolve-dir@npm:^1.0.1": + version: 1.0.1 + resolution: "resolve-dir@npm:1.0.1" + dependencies: + expand-tilde: "npm:^2.0.0" + global-modules: "npm:^1.0.0" + checksum: 8197ed13e4a51d9cd786ef6a09fc83450db016abe7ef3311ca39389b3e508d77c26fe0cf0483a9b407b8caa2764bb5ccc52cf6a017ded91492a416475a56066f + languageName: node + linkType: hard + "resolve-from@npm:5.0.0, resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" @@ -20462,6 +21731,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^4.0.0": + version: 4.0.0 + resolution: "restore-cursor@npm:4.0.0" + dependencies: + onetime: "npm:^5.1.0" + signal-exit: "npm:^3.0.2" + checksum: 6f7da8c5e422ac26aa38354870b1afac09963572cf2879443540449068cb43476e9cbccf6f8de3e0171e0d6f7f533c2bc1a0a008003c9a525bbc098e89041318 + languageName: node + linkType: hard + "ret@npm:~0.1.10": version: 0.1.15 resolution: "ret@npm:0.1.15" @@ -20534,13 +21813,13 @@ __metadata: linkType: hard "rimraf@npm:^5.0.0": - version: 5.0.5 - resolution: "rimraf@npm:5.0.5" + version: 5.0.7 + resolution: "rimraf@npm:5.0.7" dependencies: glob: "npm:^10.3.7" bin: rimraf: dist/esm/bin.mjs - checksum: d50dbe724f33835decd88395b25ed35995077c60a50ae78ded06e0185418914e555817aad1b4243edbff2254548c2f6ad6f70cc850040bebb4da9e8cc016f586 + checksum: bd6dbfaa98ae34ce1e54d1e06045d2d63e8859d9a1979bb4a4628b652b459a2d17b17dc20ee072b034bd2d09bd691e801d24c4d9cfe94e16fdbcc8470a1d4807 languageName: node linkType: hard @@ -20601,22 +21880,6 @@ __metadata: languageName: node linkType: hard -"rust-verkle-wasm@npm:^0.0.1": - version: 0.0.1 - resolution: "rust-verkle-wasm@npm:0.0.1" - checksum: 07536a7b4d1fe37dcfc9fc19fdee630d26c9c27acd3a18da8daf21c040b87bb162098fd304e1458e4edb72d8ed4cd0e74c9fcd75edb70eb5e29a1a524916ad12 - languageName: node - linkType: hard - -"rustbn-wasm@npm:^0.2.0": - version: 0.2.0 - resolution: "rustbn-wasm@npm:0.2.0" - dependencies: - "@scure/base": "npm:^1.1.1" - checksum: 86729b25a7295f706641366a3ba2bd59a666c1ddbdc40ec4a18f8f8dd5074aaf742ffaf260e80617513954e6a247f4e06f655fe99e9d75fddbdcc4cf0bea32b7 - languageName: node - linkType: hard - "rustbn.js@npm:~0.2.0": version: 0.2.0 resolution: "rustbn.js@npm:0.2.0" @@ -20642,15 +21905,15 @@ __metadata: languageName: node linkType: hard -"safe-array-concat@npm:^1.0.0, safe-array-concat@npm:^1.1.0": - version: 1.1.0 - resolution: "safe-array-concat@npm:1.1.0" +"safe-array-concat@npm:^1.0.0, safe-array-concat@npm:^1.1.2": + version: 1.1.2 + resolution: "safe-array-concat@npm:1.1.2" dependencies: - call-bind: "npm:^1.0.5" - get-intrinsic: "npm:^1.2.2" + call-bind: "npm:^1.0.7" + get-intrinsic: "npm:^1.2.4" has-symbols: "npm:^1.0.3" isarray: "npm:^2.0.5" - checksum: 833d3d950fc7507a60075f9bfaf41ec6dac7c50c7a9d62b1e6b071ecc162185881f92e594ff95c1a18301c881352dd6fd236d56999d5819559db7b92da9c28af + checksum: 12f9fdb01c8585e199a347eacc3bae7b5164ae805cdc8c6707199dbad5b9e30001a50a43c4ee24dc9ea32dbb7279397850e9208a7e217f4d8b1cf5d90129dec9 languageName: node linkType: hard @@ -20811,7 +22074,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.6.0, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4": +"semver@npm:7.6.0": version: 7.6.0 resolution: "semver@npm:7.6.0" dependencies: @@ -20831,6 +22094,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": + version: 7.6.2 + resolution: "semver@npm:7.6.2" + bin: + semver: bin/semver.js + checksum: 97d3441e97ace8be4b1976433d1c32658f6afaff09f143e52c593bae7eef33de19e3e369c88bd985ce1042c6f441c80c6803078d1de2a9988080b66684cbb30c + languageName: node + linkType: hard + "semver@npm:~5.4.1": version: 5.4.1 resolution: "semver@npm:5.4.1" @@ -20993,6 +22265,15 @@ __metadata: languageName: node linkType: hard +"serialize-javascript@npm:^6.0.2": + version: 6.0.2 + resolution: "serialize-javascript@npm:6.0.2" + dependencies: + randombytes: "npm:^2.1.0" + checksum: 2dd09ef4b65a1289ba24a788b1423a035581bef60817bea1f01eda8e3bda623f86357665fe7ac1b50f6d4f583f97db9615b3f07b2a2e8cbcb75033965f771dd2 + languageName: node + linkType: hard + "serve-static@npm:1.14.2": version: 1.14.2 resolution: "serve-static@npm:1.14.2" @@ -21038,16 +22319,16 @@ __metadata: linkType: hard "set-function-length@npm:^1.2.1": - version: 1.2.1 - resolution: "set-function-length@npm:1.2.1" + version: 1.2.2 + resolution: "set-function-length@npm:1.2.2" dependencies: - define-data-property: "npm:^1.1.2" + define-data-property: "npm:^1.1.4" es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.3" + get-intrinsic: "npm:^1.2.4" gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.1" - checksum: 1927e296599f2c04d210c1911f1600430a5e49e04a6d8bb03dca5487b95a574da9968813a2ced9a774bd3e188d4a6208352c8f64b8d4674cdb021dca21e190ca + has-property-descriptors: "npm:^1.0.2" + checksum: 82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c languageName: node linkType: hard @@ -21170,15 +22451,15 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.4": - version: 1.0.5 - resolution: "side-channel@npm:1.0.5" +"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" dependencies: - call-bind: "npm:^1.0.6" + call-bind: "npm:^1.0.7" es-errors: "npm:^1.3.0" get-intrinsic: "npm:^1.2.4" object-inspect: "npm:^1.13.1" - checksum: 31312fecb68997ce2893b1f6d1fd07d6dd41e05cc938e82004f056f7de96dd9df599ef9418acdf730dda948e867e933114bd2efe4170c0146d1ed7009700c252 + checksum: d2afd163dc733cc0a39aa6f7e39bf0c436293510dbccbff446733daeaf295857dbccf94297092ec8c53e2503acac30f0b78830876f0485991d62a90e9cad305f languageName: node linkType: hard @@ -21189,7 +22470,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 @@ -21306,6 +22587,16 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^7.0.0": + version: 7.1.0 + resolution: "slice-ansi@npm:7.1.0" + dependencies: + ansi-styles: "npm:^6.2.1" + is-fullwidth-code-point: "npm:^5.0.0" + checksum: 631c971d4abf56cf880f034d43fcc44ff883624867bf11ecbd538c47343911d734a4656d7bc02362b40b89d765652a7f935595441e519b59e2ad3f4d5d6fe7ca + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -21375,24 +22666,24 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.1": - version: 8.0.2 - resolution: "socks-proxy-agent@npm:8.0.2" +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.3 + resolution: "socks-proxy-agent@npm:8.0.3" dependencies: - agent-base: "npm:^7.0.2" + agent-base: "npm:^7.1.1" debug: "npm:^4.3.4" socks: "npm:^2.7.1" - checksum: a842402fc9b8848a31367f2811ca3cd14c4106588b39a0901cd7a69029998adfc6456b0203617c18ed090542ad0c24ee4e9d4c75a0c4b75071e214227c177eb7 + checksum: 4950529affd8ccd6951575e21c1b7be8531b24d924aa4df3ee32df506af34b618c4e50d261f4cc603f1bfd8d426915b7d629966c8ce45b05fb5ad8c8b9a6459d languageName: node linkType: hard "socks@npm:^2.7.1": - version: 2.8.1 - resolution: "socks@npm:2.8.1" + version: 2.8.3 + resolution: "socks@npm:2.8.3" dependencies: ip-address: "npm:^9.0.5" smart-buffer: "npm:^4.2.0" - checksum: ac77b515c260473cc7c4452f09b20939e22510ce3ae48385c516d1d5784374d5cc75be3cb18ff66cc985a7f4f2ef8fef84e984c5ec70aad58355ed59241f40a8 + checksum: d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 languageName: node linkType: hard @@ -21410,6 +22701,26 @@ __metadata: languageName: node linkType: hard +"solc-typed-ast@npm:18.2.4": + version: 18.2.4 + resolution: "solc-typed-ast@npm:18.2.4" + dependencies: + axios: "npm:^1.6.8" + commander: "npm:^12.0.0" + decimal.js: "npm:^10.4.3" + findup-sync: "npm:^5.0.0" + fs-extra: "npm:^11.2.0" + jsel: "npm:^1.1.6" + semver: "npm:^7.6.0" + solc: "npm:0.8.25" + src-location: "npm:^1.1.0" + web3-eth-abi: "npm:^4.2.0" + bin: + sol-ast-compile: dist/bin/compile.js + checksum: 77f6cc0fcfdd8cb662ead3489a558b9ff9bbeeb4f46af7a602f0f987b23953e33e21ca7e49cccfd4719e92a9b2aae400b00882c72efdd2819815a7ac5f478068 + languageName: node + linkType: hard + "solc@npm:0.7.3": version: 0.7.3 resolution: "solc@npm:0.7.3" @@ -21429,6 +22740,40 @@ __metadata: languageName: node linkType: hard +"solc@npm:0.8.25": + version: 0.8.25 + resolution: "solc@npm:0.8.25" + dependencies: + command-exists: "npm:^1.2.8" + commander: "npm:^8.1.0" + follow-redirects: "npm:^1.12.1" + js-sha3: "npm:0.8.0" + memorystream: "npm:^0.3.1" + semver: "npm:^5.5.0" + tmp: "npm:0.0.33" + bin: + solcjs: solc.js + checksum: d89b8b060cc0cec6c9948634da0606090da3710cada4ab7cb0f4a35721713beea716e73c1991329de6865fa997862c687b435e50113eaf66e3674a7f64e44d70 + languageName: node + linkType: hard + +"solc@npm:0.8.26": + version: 0.8.26 + resolution: "solc@npm:0.8.26" + dependencies: + command-exists: "npm:^1.2.8" + commander: "npm:^8.1.0" + follow-redirects: "npm:^1.12.1" + js-sha3: "npm:0.8.0" + memorystream: "npm:^0.3.1" + semver: "npm:^5.5.0" + tmp: "npm:0.0.33" + bin: + solcjs: solc.js + checksum: 1eea35da99c228d0dc1d831c29f7819e7921b67824c889a5e5f2e471a2ef5856a15fabc0b5de067f5ba994fa36fb5a563361963646fe98dad58a0e4fa17c8b2d + languageName: node + linkType: hard + "solc@npm:^0.4.20": version: 0.4.26 resolution: "solc@npm:0.4.26" @@ -21493,64 +22838,40 @@ __metadata: languageName: node linkType: hard -"solhint-graph-config@workspace:^0.0.1, solhint-graph-config@workspace:packages/solhint-graph-config": - version: 0.0.0-use.local - resolution: "solhint-graph-config@workspace:packages/solhint-graph-config" - peerDependencies: - prettier: ^3.2.5 - prettier-plugin-solidity: ^1.3.1 - solhint: ^4.1.1 - languageName: unknown - linkType: soft - -"solhint-plugin-prettier@npm:^0.1.0": - version: 0.1.0 - resolution: "solhint-plugin-prettier@npm:0.1.0" - dependencies: - "@prettier/sync": "npm:^0.3.0" - prettier-linter-helpers: "npm:^1.0.0" - peerDependencies: - prettier: ^3.0.0 - prettier-plugin-solidity: ^1.0.0 - checksum: 4f6211ddd954c2ffd82a2766dc4453f3036fd7022aea5a54f63b00c4afcd4669c42ef6add8e255d43ed8c0c592917f338dd34b66bc7c017bbfd3eb133952529d - languageName: node - linkType: hard - -"solhint@npm:^4.1.1": - version: 4.1.1 - resolution: "solhint@npm:4.1.1" - dependencies: - "@solidity-parser/parser": "npm:^0.16.0" - ajv: "npm:^6.12.6" - antlr4: "npm:^4.11.0" - ast-parents: "npm:^0.0.1" - chalk: "npm:^4.1.2" - commander: "npm:^10.0.0" - cosmiconfig: "npm:^8.0.0" - fast-diff: "npm:^1.2.0" - glob: "npm:^8.0.3" - ignore: "npm:^5.2.4" - js-yaml: "npm:^4.1.0" - latest-version: "npm:^7.0.0" - lodash: "npm:^4.17.21" - pluralize: "npm:^8.0.0" - prettier: "npm:^2.8.3" - semver: "npm:^7.5.2" - strip-ansi: "npm:^6.0.1" - table: "npm:^6.8.1" - text-table: "npm:^0.2.0" - dependenciesMeta: - prettier: - optional: true - bin: - solhint: solhint.js - checksum: b4bdb21bdc13f7ba3436d571a30cae6cb8dfb58ab1387df8bef25eeca25c8fbb3f625c49a5dddea41f8361aaeb4d8e2e9f986f580663ddd4574fd3d5de5f66c9 - languageName: node - linkType: hard - -"solhint@npm:^4.5.2": - version: 4.5.2 - resolution: "solhint@npm:4.5.2" +"solhint-graph-config@workspace:^0.0.1, solhint-graph-config@workspace:packages/solhint-graph-config": + version: 0.0.0-use.local + resolution: "solhint-graph-config@workspace:packages/solhint-graph-config" + dependencies: + solhint-plugin-graph: "workspace:*" + peerDependencies: + prettier: ^3.2.5 + prettier-plugin-solidity: ^1.3.1 + solhint: ^4.5.4 + languageName: unknown + linkType: soft + +"solhint-plugin-graph@workspace:*, solhint-plugin-graph@workspace:^0.0.1, solhint-plugin-graph@workspace:packages/solhint-plugin-graph": + version: 0.0.0-use.local + resolution: "solhint-plugin-graph@workspace:packages/solhint-plugin-graph" + languageName: unknown + linkType: soft + +"solhint-plugin-prettier@npm:^0.1.0": + version: 0.1.0 + resolution: "solhint-plugin-prettier@npm:0.1.0" + dependencies: + "@prettier/sync": "npm:^0.3.0" + prettier-linter-helpers: "npm:^1.0.0" + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-solidity: ^1.0.0 + checksum: 4f6211ddd954c2ffd82a2766dc4453f3036fd7022aea5a54f63b00c4afcd4669c42ef6add8e255d43ed8c0c592917f338dd34b66bc7c017bbfd3eb133952529d + languageName: node + linkType: hard + +"solhint@npm:^4.1.1, solhint@npm:^4.5.2, solhint@npm:^4.5.4": + version: 4.5.4 + resolution: "solhint@npm:4.5.4" dependencies: "@solidity-parser/parser": "npm:^0.18.0" ajv: "npm:^6.12.6" @@ -21576,16 +22897,16 @@ __metadata: optional: true bin: solhint: solhint.js - checksum: a54cc8df54ed711ca6be783b9291eb9e838f518f236d4df05c6ef412ead6a21f4a5bf3bace62ca7d2ed69f4566d5289db4c8c93e0c5e23ac91401904e1f1f312 + checksum: 85bc62536c6b1269403b2af369dcfc7c63d07c6efb994f4e3c4e7e96d3511a64ab10c68635c6676e09eee289104b5d349091960994e990cacf240497170c8aed languageName: node linkType: hard -"solidity-ast@npm:^0.4.51": - version: 0.4.55 - resolution: "solidity-ast@npm:0.4.55" +"solidity-ast@npm:^0.4.38, solidity-ast@npm:^0.4.51": + version: 0.4.56 + resolution: "solidity-ast@npm:0.4.56" dependencies: array.prototype.findlast: "npm:^1.2.2" - checksum: 6f945f014a34ccc9fe8ffbbef286e9f12d52a92bee79d01f8f16e81e2ccd610bbb3e60ab4bebaea7a868382c235d0f01938e32103a68d374753c355849f2279f + checksum: 6e10e58ddae8c7a9e53551bc61b673963c6671cac12050fa77575caff1ecc6a58440af8b7d85755fdda7e02d0516e2dcd3a5a976bd91a865375274163281d160 languageName: node linkType: hard @@ -21624,6 +22945,49 @@ __metadata: languageName: node linkType: hard +"solidity-coverage@npm:^0.8.0": + version: 0.8.12 + resolution: "solidity-coverage@npm:0.8.12" + dependencies: + "@ethersproject/abi": "npm:^5.0.9" + "@solidity-parser/parser": "npm:^0.18.0" + chalk: "npm:^2.4.2" + death: "npm:^1.1.0" + difflib: "npm:^0.2.4" + fs-extra: "npm:^8.1.0" + ghost-testrpc: "npm:^0.0.2" + global-modules: "npm:^2.0.0" + globby: "npm:^10.0.1" + jsonschema: "npm:^1.2.4" + lodash: "npm:^4.17.21" + mocha: "npm:^10.2.0" + node-emoji: "npm:^1.10.0" + pify: "npm:^4.0.1" + recursive-readdir: "npm:^2.2.2" + sc-istanbul: "npm:^0.4.5" + semver: "npm:^7.3.4" + shelljs: "npm:^0.8.3" + web3-utils: "npm:^1.3.6" + peerDependencies: + hardhat: ^2.11.0 + bin: + solidity-coverage: plugins/bin.js + checksum: 500de00ca5d0ff9531bcfc54040591e90bb281714c5ad810bc6aa77bb6ab696f15a141f6bcb1a47547dca68d10e717e1705741d4cf660d7dce8700fa97e6bfaf + languageName: node + linkType: hard + +"solidity-docgen@npm:^0.6.0-beta.36": + version: 0.6.0-beta.36 + resolution: "solidity-docgen@npm:0.6.0-beta.36" + dependencies: + handlebars: "npm:^4.7.7" + solidity-ast: "npm:^0.4.38" + peerDependencies: + hardhat: ^2.8.0 + checksum: 34ca1c18b2f662e5b6e407fad9346da91807b18ca57c877d909a4e730935475b0d3a5d8e91d79113c96741f4275944f9f3176a612894f4118e16cd9a9de80c02 + languageName: node + linkType: hard + "solium-plugin-security@npm:0.1.1": version: 0.1.1 resolution: "solium-plugin-security@npm:0.1.1" @@ -21687,7 +23051,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.13, source-map-support@npm:^0.5.16": +"source-map-support@npm:^0.5.13, source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.17": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -21819,6 +23183,13 @@ __metadata: languageName: node linkType: hard +"src-location@npm:^1.1.0": + version: 1.1.0 + resolution: "src-location@npm:1.1.0" + checksum: 68986114acb6891fea9226d727d24344364b65f478fb1f5c012e02757f0385727cf089a2b59ab1efccc286587eab78021f176755a32c5333949b23226d7fcfb7 + languageName: node + linkType: hard + "sshpk@npm:^1.7.0": version: 1.18.0 resolution: "sshpk@npm:1.18.0" @@ -21841,11 +23212,11 @@ __metadata: linkType: hard "ssri@npm:^10.0.0": - version: 10.0.5 - resolution: "ssri@npm:10.0.5" + version: 10.0.6 + resolution: "ssri@npm:10.0.6" dependencies: minipass: "npm:^7.0.3" - checksum: b091f2ae92474183c7ac5ed3f9811457e1df23df7a7e70c9476eaa9a0c4a0c8fc190fb45acefbf023ca9ee864dd6754237a697dc52a0fb182afe65d8e77443d8 + checksum: e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 languageName: node linkType: hard @@ -21896,7 +23267,7 @@ __metadata: languageName: node linkType: hard -"stream-shift@npm:^1.0.0": +"stream-shift@npm:^1.0.2": version: 1.0.3 resolution: "stream-shift@npm:1.0.3" checksum: 939cd1051ca750d240a0625b106a2b988c45fb5a3be0cebe9a9858cb01bc1955e8c7b9fac17a9462976bea4a7b704e317c5c2200c70f0ca715a3363b9aa4fd3b @@ -21952,7 +23323,7 @@ __metadata: languageName: node linkType: hard -"string-argv@npm:^0.3.1": +"string-argv@npm:0.3.2, string-argv@npm:^0.3.1": version: 0.3.2 resolution: "string-argv@npm:0.3.2" checksum: 75c02a83759ad1722e040b86823909d9a2fc75d15dd71ec4b537c3560746e33b5f5a07f7332d1e3f88319909f82190843aa2f0a0d8c8d591ec08e93d5b8dec82 @@ -22009,36 +23380,48 @@ __metadata: languageName: node linkType: hard -"string.prototype.trim@npm:^1.2.8, string.prototype.trim@npm:~1.2.8": - version: 1.2.8 - resolution: "string.prototype.trim@npm:1.2.8" +"string-width@npm:^7.0.0": + version: 7.1.0 + resolution: "string-width@npm:7.1.0" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 4f76c583908bcde9a71208ddff38f67f24c9ec8093631601666a0df8b52fad44dad2368c78895ce83eb2ae8e7068294cc96a02fc971ab234e4d5c9bb61ea4e34 + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 68a99fbc3bd3d8eb42886ff38dce819767dee55f606f74dfa4687a07dfd21262745d9683df0aa53bf81a5dd47c13da921a501925b974bec66a7ddd634fef0634 languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.7": - version: 1.0.7 - resolution: "string.prototype.trimend@npm:1.0.7" +"string.prototype.trim@npm:^1.2.9, string.prototype.trim@npm:~1.2.8": + version: 1.2.9 + resolution: "string.prototype.trim@npm:1.2.9" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 53c24911c7c4d8d65f5ef5322de23a3d5b6b4db73273e05871d5ab4571ae5638f38f7f19d71d09116578fb060e5a145cc6a208af2d248c8baf7a34f44d32ce57 + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.0" + es-object-atoms: "npm:^1.0.0" + checksum: dcef1a0fb61d255778155006b372dff8cc6c4394bc39869117e4241f41a2c52899c0d263ffc7738a1f9e61488c490b05c0427faa15151efad721e1a9fb2663c2 languageName: node linkType: hard -"string.prototype.trimstart@npm:^1.0.7": - version: 1.0.7 - resolution: "string.prototype.trimstart@npm:1.0.7" +"string.prototype.trimend@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimend@npm:1.0.8" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 0bcf391b41ea16d4fda9c9953d0a7075171fe090d33b4cf64849af94944c50862995672ac03e0c5dba2940a213ad7f53515a668dac859ce22a0276289ae5cf4f + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 0a0b54c17c070551b38e756ae271865ac6cc5f60dabf2e7e343cceae7d9b02e1a1120a824e090e79da1b041a74464e8477e2da43e2775c85392be30a6f60963c + languageName: node + linkType: hard + +"string.prototype.trimstart@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimstart@npm:1.0.8" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: d53af1899959e53c83b64a5fd120be93e067da740e7e75acb433849aa640782fb6c7d4cd5b84c954c84413745a3764df135a8afeb22908b86a835290788d8366 languageName: node linkType: hard @@ -22114,7 +23497,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": version: 7.1.0 resolution: "strip-ansi@npm:7.1.0" dependencies: @@ -22153,6 +23536,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce + languageName: node + linkType: hard + "strip-hex-prefix@npm:1.0.0": version: 1.0.0 resolution: "strip-hex-prefix@npm:1.0.0" @@ -22194,7 +23584,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:8.1.1": +"supports-color@npm:8.1.1, supports-color@npm:^8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -22322,15 +23712,15 @@ __metadata: linkType: hard "table@npm:^6.8.0, table@npm:^6.8.1": - version: 6.8.1 - resolution: "table@npm:6.8.1" + version: 6.8.2 + resolution: "table@npm:6.8.2" dependencies: ajv: "npm:^8.0.1" lodash.truncate: "npm:^4.4.2" slice-ansi: "npm:^4.0.0" string-width: "npm:^4.2.3" strip-ansi: "npm:^6.0.1" - checksum: 591ed84b2438b01c9bc02248e2238e21e8bfb73654bc5acca0d469053eb39be3db2f57d600dcf08ac983b6f50f80842c44612c03877567c2afee3aec4a033e5f + checksum: f8b348af38ee34e419d8ce7306ba00671ce6f20e861ccff22555f491ba264e8416086063ce278a8d81abfa8d23b736ec2cca7ac4029b5472f63daa4b4688b803 languageName: node linkType: hard @@ -22376,8 +23766,8 @@ __metadata: linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -22385,7 +23775,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + checksum: a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 languageName: node linkType: hard @@ -22562,6 +23952,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.6": + version: 0.2.10 + resolution: "tinyglobby@npm:0.2.10" + dependencies: + fdir: "npm:^6.4.2" + picomatch: "npm:^4.0.2" + checksum: ce946135d39b8c0e394e488ad59f4092e8c4ecd675ef1bcd4585c47de1b325e61ec6adfbfbe20c3c2bfa6fd674c5b06de2a2e65c433f752ae170aff11793e5ef + languageName: node + linkType: hard + "title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3" @@ -22724,12 +24124,12 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^1.0.1": - version: 1.2.1 - resolution: "ts-api-utils@npm:1.2.1" +"ts-api-utils@npm:^1.0.1, ts-api-utils@npm:^1.3.0": + version: 1.3.0 + resolution: "ts-api-utils@npm:1.3.0" peerDependencies: typescript: ">=4.2.0" - checksum: 8ddb493e7ae581d3f57a2e469142feb60b420d4ad8366ab969fe8e36531f8f301f370676b47e8d97f28b5f5fd10d6f2d55f656943a8546ef95e35ce5cf117754 + checksum: f54a0ba9ed56ce66baea90a3fa087a484002e807f28a8ccb2d070c75e76bde64bd0f6dce98b3802834156306050871b67eec325cb4e918015a360a3f0868c77c languageName: node linkType: hard @@ -22791,7 +24191,7 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.5.0, ts-node@npm:^10.8.1, ts-node@npm:^10.9.1": +"ts-node@npm:>=8.0.0, ts-node@npm:^10.5.0, ts-node@npm:^10.8.1, ts-node@npm:^10.9.1": version: 10.9.2 resolution: "ts-node@npm:10.9.2" dependencies: @@ -22829,6 +24229,26 @@ __metadata: languageName: node linkType: hard +"ts-node@npm:^8.0.0": + version: 8.10.2 + resolution: "ts-node@npm:8.10.2" + dependencies: + arg: "npm:^4.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + source-map-support: "npm:^0.5.17" + yn: "npm:3.1.1" + peerDependencies: + typescript: ">=2.7" + bin: + ts-node: dist/bin.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 628343f62fff2543b4559a93eb27005084aea7609945e77f311031c5e96c4099736646856e1792605b90e8007d2c060fe80783be21c94788d91d6f259aab92e2 + languageName: node + linkType: hard + "tsconfig-paths@npm:^4.2.0": version: 4.2.0 resolution: "tsconfig-paths@npm:4.2.0" @@ -22840,6 +24260,20 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.4.0": + version: 2.4.0 + resolution: "tslib@npm:2.4.0" + checksum: eb19bda3ae545b03caea6a244b34593468e23d53b26bf8649fbc20fce43e9b21a71127fd6d2b9662c0fe48ee6ff668ead48fd00d3b88b2b716b1c12edae25b5d + languageName: node + linkType: hard + +"tslib@npm:2.7.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 469e1d5bf1af585742128827000711efa61010b699cb040ab1800bcd3ccdd37f63ec30642c9e07c4439c1db6e46345582614275daca3e0f4abae29b0083f04a6 + languageName: node + linkType: hard + "tslib@npm:^1.11.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -22954,6 +24388,13 @@ __metadata: languageName: node linkType: hard +"type-detect@npm:^4.1.0": + version: 4.1.0 + resolution: "type-detect@npm:4.1.0" + checksum: df8157ca3f5d311edc22885abc134e18ff8ffbc93d6a9848af5b682730ca6a5a44499259750197250479c5331a8a75b5537529df5ec410622041650a7f293e2a + languageName: node + linkType: hard + "type-fest@npm:^0.13.1": version: 0.13.1 resolution: "type-fest@npm:0.13.1" @@ -23013,13 +24454,6 @@ __metadata: languageName: node linkType: hard -"type@npm:^1.0.1": - version: 1.2.0 - resolution: "type@npm:1.2.0" - checksum: 444660849aaebef8cbb9bc43b28ec2068952064cfce6a646f88db97aaa2e2d6570c5629cd79238b71ba23aa3f75146a0b96e24e198210ee0089715a6f8889bf7 - languageName: node - linkType: hard - "type@npm:^2.7.2": version: 2.7.2 resolution: "type@npm:2.7.2" @@ -23088,7 +24522,29 @@ __metadata: languageName: node linkType: hard -"typed-array-buffer@npm:^1.0.1": +"typechain@npm:^8.3.0": + version: 8.3.2 + resolution: "typechain@npm:8.3.2" + dependencies: + "@types/prettier": "npm:^2.1.1" + debug: "npm:^4.3.1" + fs-extra: "npm:^7.0.0" + glob: "npm:7.1.7" + js-sha3: "npm:^0.8.0" + lodash: "npm:^4.17.15" + mkdirp: "npm:^1.0.4" + prettier: "npm:^2.3.1" + ts-command-line-args: "npm:^2.2.0" + ts-essentials: "npm:^7.0.1" + peerDependencies: + typescript: ">=4.3.0" + bin: + typechain: dist/cli/cli.js + checksum: 1ea660cc7c699c6ac68da67b76454eb4e9395c54666d924ca67f983ae8eb5b5e7dab0a576beb55dbfad75ea784a3f68cb1ca019d332293b7291731c156ead5b5 + languageName: node + linkType: hard + +"typed-array-buffer@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-buffer@npm:1.0.2" dependencies: @@ -23099,7 +24555,7 @@ __metadata: languageName: node linkType: hard -"typed-array-byte-length@npm:^1.0.0": +"typed-array-byte-length@npm:^1.0.1": version: 1.0.1 resolution: "typed-array-byte-length@npm:1.0.1" dependencies: @@ -23112,7 +24568,7 @@ __metadata: languageName: node linkType: hard -"typed-array-byte-offset@npm:^1.0.0": +"typed-array-byte-offset@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-byte-offset@npm:1.0.2" dependencies: @@ -23126,9 +24582,9 @@ __metadata: languageName: node linkType: hard -"typed-array-length@npm:^1.0.4": - version: 1.0.5 - resolution: "typed-array-length@npm:1.0.5" +"typed-array-length@npm:^1.0.6": + version: 1.0.6 + resolution: "typed-array-length@npm:1.0.6" dependencies: call-bind: "npm:^1.0.7" for-each: "npm:^0.3.3" @@ -23136,7 +24592,7 @@ __metadata: has-proto: "npm:^1.0.3" is-typed-array: "npm:^1.1.13" possible-typed-array-names: "npm:^1.0.0" - checksum: 5cc0f79196e70a92f8f40846cfa62b3de6be51e83f73655e137116cf65e3c29a288502b18cc8faf33c943c2470a4569009e1d6da338441649a2db2f135761ad5 + checksum: 74253d7dc488eb28b6b2711cf31f5a9dcefc9c41b0681fd1c178ed0a1681b4468581a3626d39cd4df7aee3d3927ab62be06aa9ca74e5baf81827f61641445b77 languageName: node linkType: hard @@ -23157,21 +24613,22 @@ __metadata: linkType: hard "typescript-eslint@npm:^7.0.2": - version: 7.0.2 - resolution: "typescript-eslint@npm:7.0.2" + version: 7.9.0 + resolution: "typescript-eslint@npm:7.9.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:7.0.2" - "@typescript-eslint/parser": "npm:7.0.2" + "@typescript-eslint/eslint-plugin": "npm:7.9.0" + "@typescript-eslint/parser": "npm:7.9.0" + "@typescript-eslint/utils": "npm:7.9.0" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 947216bccdb392c1e5f1228772185afbe306db305f1f61343d3cb315aa6c80a928709172498af62536b50c1e7c91e263eed7886bb4b328ca8850ffb1ea71a3d9 + checksum: dacdd8b278d519eea1d980c71dd301a0b68fe1100aa8eaa9e3b80acd7089765ef50bdf369b7c11ddc5f4be6ac6d90cc9283db549003c3df8cfabbe4f44a36b53 languageName: node linkType: hard -"typescript@npm:^4.0.2, typescript@npm:^4.4.3, typescript@npm:^4.7.4": +"typescript@npm:^4.0.2, typescript@npm:^4.4.3": version: 4.9.5 resolution: "typescript@npm:4.9.5" bin: @@ -23181,17 +24638,27 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.4, typescript@npm:^5.1.6, typescript@npm:^5.3.3": - version: 5.3.3 - resolution: "typescript@npm:5.3.3" +"typescript@npm:^5.0.4, typescript@npm:^5.1.6, typescript@npm:^5.2.2, typescript@npm:^5.3.3": + version: 5.4.5 + resolution: "typescript@npm:5.4.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e + languageName: node + linkType: hard + +"typescript@npm:^5.6.3": + version: 5.6.3 + resolution: "typescript@npm:5.6.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: e33cef99d82573624fc0f854a2980322714986bc35b9cb4d1ce736ed182aeab78e2cb32b385efa493b2a976ef52c53e20d6c6918312353a91850e2b76f1ea44f + checksum: 44f61d3fb15c35359bc60399cb8127c30bae554cd555b8e2b46d68fa79d680354b83320ad419ff1b81a0bdf324197b29affe6cc28988cd6a74d4ac60c94f9799 languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^4.0.2#optional!builtin, typescript@patch:typescript@npm%3A^4.4.3#optional!builtin, typescript@patch:typescript@npm%3A^4.7.4#optional!builtin": +"typescript@patch:typescript@npm%3A^4.0.2#optional!builtin, typescript@patch:typescript@npm%3A^4.4.3#optional!builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin::version=4.9.5&hash=289587" bin: @@ -23201,13 +24668,23 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.1.6#optional!builtin, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin": - version: 5.3.3 - resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7" +"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.1.6#optional!builtin, typescript@patch:typescript@npm%3A^5.2.2#optional!builtin, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin": + version: 5.4.5 + resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=e012d7" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 9cf4c053893bcf327d101b6c024a55baf05430dc30263f9adb1bf354aeffc11306fe1f23ba2f9a0209674359f16219b5b7d229e923477b94831d07d5a33a4217 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.6.3#optional!builtin": + version: 5.6.3 + resolution: "typescript@patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=e012d7" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 1d0a5f4ce496c42caa9a30e659c467c5686eae15d54b027ee7866744952547f1be1262f2d40de911618c242b510029d51d43ff605dba8fb740ec85ca2d3f9500 + checksum: ac8307bb06bbfd08ae7137da740769b7d8c3ee5943188743bb622c621f8ad61d244767480f90fbd840277fbf152d8932aa20c33f867dea1bb5e79b187ca1a92f languageName: node linkType: hard @@ -23346,12 +24823,19 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 + languageName: node + linkType: hard + "undici@npm:^5.14.0": - version: 5.28.3 - resolution: "undici@npm:5.28.3" + version: 5.28.4 + resolution: "undici@npm:5.28.4" dependencies: "@fastify/busboy": "npm:^2.0.0" - checksum: 3c559ae50ef3104b7085251445dda6f4de871553b9e290845649d2f80b06c0c9cfcdf741b0029c6b20d36c82e6a74dc815b139fa9a26757d70728074ca6d6f5c + checksum: 08d0f2596553aa0a54ca6e8e9c7f45aef7d042c60918564e3a142d449eda165a80196f6ef19ea2ef2e6446959e293095d8e40af1236f0d67223b06afac5ecad7 languageName: node linkType: hard @@ -23440,16 +24924,16 @@ __metadata: linkType: hard "update-browserslist-db@npm:^1.0.13": - version: 1.0.13 - resolution: "update-browserslist-db@npm:1.0.13" + version: 1.0.16 + resolution: "update-browserslist-db@npm:1.0.16" dependencies: - escalade: "npm:^3.1.1" - picocolors: "npm:^1.0.0" + escalade: "npm:^3.1.2" + picocolors: "npm:^1.0.1" peerDependencies: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: e52b8b521c78ce1e0c775f356cd16a9c22c70d25f3e01180839c407a5dc787fb05a13f67560cbaf316770d26fa99f78f1acd711b1b54a4f35d4820d4ea7136e6 + checksum: 5995399fc202adbb51567e4810e146cdf7af630a92cc969365a099150cb00597e425cc14987ca7080b09a4d0cfd2a3de53fbe72eebff171aed7f9bb81f9bf405 languageName: node linkType: hard @@ -23471,7 +24955,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2": +"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: @@ -23647,9 +25131,9 @@ __metadata: linkType: hard "validator@npm:^13.7.0, validator@npm:^13.9.0": - version: 13.11.0 - resolution: "validator@npm:13.11.0" - checksum: 0107da3add5a4ebc6391dac103c55f6d8ed055bbcc29a4c9cbf89eacfc39ba102a5618c470bdc33c6487d30847771a892134a8c791f06ef0962dd4b7a60ae0f5 + version: 13.12.0 + resolution: "validator@npm:13.12.0" + checksum: 21d48a7947c9e8498790550f56cd7971e0e3d724c73388226b109c1bac2728f4f88caddfc2f7ed4b076f9b0d004316263ac786a17e9c4edf075741200718cd32 languageName: node linkType: hard @@ -23968,6 +25452,15 @@ __metadata: languageName: node linkType: hard +"web3-errors@npm:^1.2.0, web3-errors@npm:^1.3.1": + version: 1.3.1 + resolution: "web3-errors@npm:1.3.1" + dependencies: + web3-types: "npm:^1.10.0" + checksum: b763f0ae43c5c90f0fb72a0342a0d9227b68b363ab9d0b0c2948d586379129ec31b6da070c37393213022b34ed10374d3b16b86002c1280c637d3df4d29eed2a + languageName: node + linkType: hard + "web3-eth-abi@npm:1.10.0": version: 1.10.0 resolution: "web3-eth-abi@npm:1.10.0" @@ -23999,6 +25492,19 @@ __metadata: languageName: node linkType: hard +"web3-eth-abi@npm:^4.2.0": + version: 4.4.1 + resolution: "web3-eth-abi@npm:4.4.1" + dependencies: + abitype: "npm:0.7.1" + web3-errors: "npm:^1.3.1" + web3-types: "npm:^1.10.0" + web3-utils: "npm:^4.3.3" + web3-validator: "npm:^2.0.6" + checksum: fbaf2a6ef29dc8146a562a2d19823f20deb29f802abf0a82349863cb0ae884e564f756643fa76246717f89475088175e7d93fc813c24790911422e22e18e2fda + languageName: node + linkType: hard + "web3-eth-accounts@npm:1.10.0": version: 1.10.0 resolution: "web3-eth-accounts@npm:1.10.0" @@ -24480,6 +25986,13 @@ __metadata: languageName: node linkType: hard +"web3-types@npm:^1.10.0, web3-types@npm:^1.6.0": + version: 1.10.0 + resolution: "web3-types@npm:1.10.0" + checksum: e7238b48f62dd03a4eda2ca6d150f0ae7d02a0bede886b36316e57ee6535ccf9965ba938afc9dcbdd807696df782eff1a29658ed03a917d92f88798cf8db2bc7 + languageName: node + linkType: hard + "web3-utils@npm:1.10.0": version: 1.10.0 resolution: "web3-utils@npm:1.10.0" @@ -24526,7 +26039,7 @@ __metadata: languageName: node linkType: hard -"web3-utils@npm:^1.0.0-beta.31, web3-utils@npm:^1.3.0": +"web3-utils@npm:^1.0.0-beta.31, web3-utils@npm:^1.3.0, web3-utils@npm:^1.3.6": version: 1.10.4 resolution: "web3-utils@npm:1.10.4" dependencies: @@ -24542,6 +26055,32 @@ __metadata: languageName: node linkType: hard +"web3-utils@npm:^4.3.3": + version: 4.3.3 + resolution: "web3-utils@npm:4.3.3" + dependencies: + ethereum-cryptography: "npm:^2.0.0" + eventemitter3: "npm:^5.0.1" + web3-errors: "npm:^1.3.1" + web3-types: "npm:^1.10.0" + web3-validator: "npm:^2.0.6" + checksum: c56040d254ac168c4c3266ac00dbef3a16e81093cc7926e53d0c619d2c354818bc04f2b0475dd18cb60e6167262154b8bfd0540683c1f4c91045791ad2667963 + languageName: node + linkType: hard + +"web3-validator@npm:^2.0.6": + version: 2.0.6 + resolution: "web3-validator@npm:2.0.6" + dependencies: + ethereum-cryptography: "npm:^2.0.0" + util: "npm:^0.12.5" + web3-errors: "npm:^1.2.0" + web3-types: "npm:^1.6.0" + zod: "npm:^3.21.4" + checksum: 28728773b9abad2531f7a4145784db56ec9ecffeb25cc9f6fe67bedeb01a1833b1a5d1a2e0f431ce4a3c8c6f13b111f35202dd8fa0829c6e2fcd68c58d1d5658 + languageName: node + linkType: hard + "web3@npm:1.10.0": version: 1.10.0 resolution: "web3@npm:1.10.0" @@ -24587,16 +26126,16 @@ __metadata: languageName: node linkType: hard -"webcrypto-core@npm:^1.7.8": - version: 1.7.8 - resolution: "webcrypto-core@npm:1.7.8" +"webcrypto-core@npm:^1.7.9": + version: 1.7.9 + resolution: "webcrypto-core@npm:1.7.9" dependencies: "@peculiar/asn1-schema": "npm:^2.3.8" "@peculiar/json-schema": "npm:^1.1.12" asn1js: "npm:^3.0.1" pvtsutils: "npm:^1.3.5" tslib: "npm:^2.6.2" - checksum: 4c6ef9ae4ae27489f88b7c571494b058120178528f25efe87b97cbc64ead03a6468a614f6269927d13735e4f5ce1d1f7599cf4385ee9b61a13921964a5748a66 + checksum: 29075c0fd6afdd11473dcf98623b929fae6a0861a54725af109df824e3c55c00580103a2f934baabff52d588e9c6c3892db80346061fd835c55e0a83264c19f5 languageName: node linkType: hard @@ -24622,16 +26161,16 @@ __metadata: linkType: hard "websocket@npm:^1.0.31, websocket@npm:^1.0.32": - version: 1.0.34 - resolution: "websocket@npm:1.0.34" + version: 1.0.35 + resolution: "websocket@npm:1.0.35" dependencies: bufferutil: "npm:^4.0.1" debug: "npm:^2.2.0" - es5-ext: "npm:^0.10.50" + es5-ext: "npm:^0.10.63" typedarray-to-buffer: "npm:^3.1.5" utf-8-validate: "npm:^5.0.2" yaeti: "npm:^0.0.6" - checksum: a7e17d24edec685fdf055940ff9c6a15e726df5bb5e537382390bd1ab978fc8c0d71cd2842bb628e361d823aafd43934cc56aa5b979d08e52461be7da8d01eee + checksum: 8be9a68dc0228f18058c9010d1308479f05050af8f6d68b9dbc6baebd9ab484c15a24b2521a5d742a9d78e62ee19194c532992f1047a9b9adf8c3eedb0b1fcdc languageName: node linkType: hard @@ -24696,20 +26235,20 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.2": - version: 1.1.14 - resolution: "which-typed-array@npm:1.1.14" +"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.2": + version: 1.1.15 + resolution: "which-typed-array@npm:1.1.15" dependencies: - available-typed-arrays: "npm:^1.0.6" - call-bind: "npm:^1.0.5" + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.7" for-each: "npm:^0.3.3" gopd: "npm:^1.0.1" - has-tostringtag: "npm:^1.0.1" - checksum: 0960f1e77807058819451b98c51d4cd72031593e8de990b24bd3fc22e176f5eee22921d68d852297c786aec117689f0423ed20aa4fde7ce2704d680677891f56 + has-tostringtag: "npm:^1.0.2" + checksum: 4465d5348c044032032251be54d8988270e69c6b7154f8fcb2a47ff706fe36f7624b3a24246b8d9089435a8f4ec48c1c1025c5d6b499456b9e5eff4f48212983 languageName: node linkType: hard -"which@npm:^1.1.1, which@npm:^1.2.9, which@npm:^1.3.1": +"which@npm:^1.1.1, which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" dependencies: @@ -24760,7 +26299,7 @@ __metadata: languageName: node linkType: hard -"winston-transport@npm:^4.5.0": +"winston-transport@npm:^4.7.0": version: 4.7.0 resolution: "winston-transport@npm:4.7.0" dependencies: @@ -24772,8 +26311,8 @@ __metadata: linkType: hard "winston@npm:*, winston@npm:^3.3.3": - version: 3.11.0 - resolution: "winston@npm:3.11.0" + version: 3.13.0 + resolution: "winston@npm:3.13.0" dependencies: "@colors/colors": "npm:^1.6.0" "@dabh/diagnostics": "npm:^2.0.2" @@ -24785,8 +26324,8 @@ __metadata: safe-stable-stringify: "npm:^2.3.1" stack-trace: "npm:0.0.x" triple-beam: "npm:^1.3.0" - winston-transport: "npm:^4.5.0" - checksum: 7e1f8919cbdc62cfe46e6204d79a83e1364696ef61111483f3ecf204988922383fe74192c5bc9f89df9b47caf24c2d34f5420ef6f3b693f8d1286b46432e97be + winston-transport: "npm:^4.7.0" + checksum: 2c3cc7389a691e1638edcb0d4bfea72caa82d87d5681ec6131ac9bae780d94d06fb7b112edcd4ec37c8b947a1b64943941b761e34d67c6b0dac6e9c31ae4b25b languageName: node linkType: hard @@ -24813,7 +26352,7 @@ __metadata: languageName: node linkType: hard -"word-wrap@npm:~1.2.3": +"word-wrap@npm:^1.2.5, word-wrap@npm:~1.2.3": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" checksum: e0e4a1ca27599c92a6ca4c32260e8a92e8a44f4ef6ef93f803f8ed823f486e0889fc0b93be4db59c8d51b3064951d25e43d434e95dc8c960cc3a63d65d00ba20 @@ -24844,6 +26383,13 @@ __metadata: languageName: node linkType: hard +"workerpool@npm:^6.5.1": + version: 6.5.1 + resolution: "workerpool@npm:6.5.1" + checksum: 58e8e969782292cb3a7bfba823f1179a7615250a0cefb4841d5166234db1880a3d0fe83a31dd8d648329ec92c2d0cd1890ad9ec9e53674bb36ca43e9753cdeac + languageName: node + linkType: hard + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -24887,6 +26433,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^9.0.0": + version: 9.0.0 + resolution: "wrap-ansi@npm:9.0.0" + dependencies: + ansi-styles: "npm:^6.2.1" + string-width: "npm:^7.0.0" + strip-ansi: "npm:^7.1.0" + checksum: a139b818da9573677548dd463bd626a5a5286271211eb6e4e82f34a4f643191d74e6d4a9bb0a3c26ec90e6f904f679e0569674ac099ea12378a8b98e20706066 + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -24924,6 +26481,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: f4a49064afae4500be772abdc2211c8518f39e1c959640457dcee15d4488628620625c783902a52af2dd02f68558da2868fd06e6fd0e67ebcd09e6881b1b5bfe + languageName: node + linkType: hard + "ws@npm:^3.0.0": version: 3.3.3 resolution: "ws@npm:3.3.3" @@ -24960,8 +26532,8 @@ __metadata: linkType: hard "ws@npm:^8.12.0, ws@npm:^8.13.0": - version: 8.16.0 - resolution: "ws@npm:8.16.0" + version: 8.17.0 + resolution: "ws@npm:8.17.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -24970,7 +26542,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: a7783bb421c648b1e622b423409cb2a58ac5839521d2f689e84bc9dc41d59379c692dd405b15a997ea1d4c0c2e5314ad707332d0c558f15232d2bc07c0b4618a + checksum: 55241ec93a66fdfc4bf4f8bc66c8eb038fda2c7a4ee8f6f157f2ca7dc7aa76aea0c0da0bf3adb2af390074a70a0e45456a2eaf80e581e630b75df10a64b0a990 languageName: node linkType: hard @@ -25091,6 +26663,13 @@ __metadata: languageName: node linkType: hard +"yaml@npm:2.3.4": + version: 2.3.4 + resolution: "yaml@npm:2.3.4" + checksum: cf03b68f8fef5e8516b0f0b54edaf2459f1648317fc6210391cf606d247e678b449382f4bd01f77392538429e306c7cba8ff46ff6b37cac4de9a76aff33bd9e1 + languageName: node + linkType: hard + "yaml@npm:^1.10.0, yaml@npm:^1.10.2": version: 1.10.2 resolution: "yaml@npm:1.10.2" @@ -25125,7 +26704,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.3": +"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.3, yargs-parser@npm:^20.2.9": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" checksum: 0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72 @@ -25148,7 +26727,7 @@ __metadata: languageName: node linkType: hard -"yargs-unparser@npm:2.0.0": +"yargs-unparser@npm:2.0.0, yargs-unparser@npm:^2.0.0": version: 2.0.0 resolution: "yargs-unparser@npm:2.0.0" dependencies: @@ -25160,7 +26739,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:16.2.0": +"yargs@npm:16.2.0, yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" dependencies: @@ -25175,6 +26754,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:17.7.2, yargs@npm:^17.0.0, yargs@npm:^17.7.1": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard + "yargs@npm:^10.0.3": version: 10.1.2 resolution: "yargs@npm:10.1.2" @@ -25214,21 +26808,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.0.0, yargs@npm:^17.7.1": - version: 17.7.2 - resolution: "yargs@npm:17.7.2" - dependencies: - cliui: "npm:^8.0.1" - escalade: "npm:^3.1.1" - get-caller-file: "npm:^2.0.5" - require-directory: "npm:^2.1.1" - string-width: "npm:^4.2.3" - y18n: "npm:^5.0.5" - yargs-parser: "npm:^21.1.1" - checksum: ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 - languageName: node - linkType: hard - "yargs@npm:^4.7.1": version: 4.8.1 resolution: "yargs@npm:4.8.1" @@ -25275,17 +26854,17 @@ __metadata: linkType: hard "zod-to-json-schema@npm:^3.20.5": - version: 3.22.4 - resolution: "zod-to-json-schema@npm:3.22.4" + version: 3.23.0 + resolution: "zod-to-json-schema@npm:3.23.0" peerDependencies: - zod: ^3.22.4 - checksum: a949720c165347982a4abf5d612b78bebc2b5fb1217d22e20c024782ce6a9ae0baffe5deb6ba6f961ffa7b28b888c37f744bbfee6d7e9d74f2ec1f94d6968f0d + zod: ^3.23.3 + checksum: bcd966fa040765d7170a89c0c5f1717575e7d8823b84cbbb606689d494ae308c9eaadd4b71a74752e3170deef64c1f1bb2985f4663c44a0ed2e7854ff6fda724 languageName: node linkType: hard "zod@npm:^3.21.4": - version: 3.22.4 - resolution: "zod@npm:3.22.4" - checksum: 7578ab283dac0eee66a0ad0fc4a7f28c43e6745aadb3a529f59a4b851aa10872b3890398b3160f257f4b6817b4ce643debdda4fb21a2c040adda7862cab0a587 + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 8f14c87d6b1b53c944c25ce7a28616896319d95bc46a9660fe441adc0ed0a81253b02b5abdaeffedbeb23bdd25a0bf1c29d2c12dd919aef6447652dd295e3e69 languageName: node linkType: hard