From 2053f65caf77f7e13d9dbe411989700dfa6f00a0 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Fri, 26 Apr 2024 19:40:15 -0300 Subject: [PATCH] chore: somehow it fits in 24kB now --- packages/horizon/contracts/HorizonStaking.sol | 105 ++------- .../contracts/HorizonStakingExtension.sol | 103 +++++++-- .../contracts/HorizonStakingStorage.sol | 6 +- .../horizon/contracts/IHorizonStaking.sol | 206 +----------------- .../horizon/contracts/IHorizonStakingBase.sol | 169 ++++++++++++++ .../contracts/IHorizonStakingExtension.sol | 68 ++++++ .../IStakingBackwardsCompatibility.sol | 9 - .../StakingBackwardsCompatibility.sol | 15 +- packages/horizon/foundry.toml | 4 +- packages/horizon/test/HorizonStaking.t.sol | 20 +- packages/horizon/test/HorizonStaking.ts | 23 +- 11 files changed, 390 insertions(+), 338 deletions(-) create mode 100644 packages/horizon/contracts/IHorizonStakingBase.sol create mode 100644 packages/horizon/contracts/IHorizonStakingExtension.sol diff --git a/packages/horizon/contracts/HorizonStaking.sol b/packages/horizon/contracts/HorizonStaking.sol index 6c50dae4e..a2f0d0a38 100644 --- a/packages/horizon/contracts/HorizonStaking.sol +++ b/packages/horizon/contracts/HorizonStaking.sol @@ -4,32 +4,32 @@ pragma solidity 0.8.24; import { GraphUpgradeable } from "@graphprotocol/contracts/contracts/upgrades/GraphUpgradeable.sol"; -import { IHorizonStaking } from "./IHorizonStaking.sol"; +import { IHorizonStakingBase } from "./IHorizonStakingBase.sol"; import { TokenUtils } from "./utils/TokenUtils.sol"; import { MathUtils } from "./utils/MathUtils.sol"; import { Managed } from "./Managed.sol"; import { IGraphToken } from "./IGraphToken.sol"; import { HorizonStakingV1Storage } from "./HorizonStakingStorage.sol"; -contract HorizonStaking is HorizonStakingV1Storage, IHorizonStaking, GraphUpgradeable { +contract HorizonStaking is HorizonStakingV1Storage, IHorizonStakingBase, GraphUpgradeable { /// Maximum value that can be set as the maxVerifierCut in a provision. /// It is equivalent to 50% in parts-per-million, to protect delegators from /// service providers using a malicious verifier. - uint32 public constant MAX_MAX_VERIFIER_CUT = 500000; // 50% + uint32 private constant MAX_MAX_VERIFIER_CUT = 500000; // 50% /// Minimum size of a provision - uint256 public constant MIN_PROVISION_SIZE = 1e18; + uint256 private constant MIN_PROVISION_SIZE = 1e18; /// Maximum number of simultaneous stake thaw requests or undelegations - uint256 public constant MAX_THAW_REQUESTS = 100; + uint256 private constant MAX_THAW_REQUESTS = 100; - uint256 public constant FIXED_POINT_PRECISION = 1e18; + uint256 private constant FIXED_POINT_PRECISION = 1e18; /// Minimum delegation size - uint256 public constant MINIMUM_DELEGATION = 1e18; + uint256 private constant MINIMUM_DELEGATION = 1e18; - address public immutable L2_STAKING_BACKWARDS_COMPATIBILITY; - address public immutable SUBGRAPH_DATA_SERVICE_ADDRESS; + address private immutable STAKING_EXTENSION_ADDRESS; + address private immutable SUBGRAPH_DATA_SERVICE_ADDRESS; error HorizonStakingInvalidVerifier(address verifier); error HorizonStakingVerifierAlreadyAllowed(address verifier); @@ -42,12 +42,12 @@ contract HorizonStaking is HorizonStakingV1Storage, IHorizonStaking, GraphUpgrad constructor( address _controller, - address _l2StakingBackwardsCompatibility, + address _stakingExtensionAddress, address _subgraphDataServiceAddress - ) Managed(_controller) { - L2_STAKING_BACKWARDS_COMPATIBILITY = _l2StakingBackwardsCompatibility; + ) Managed(_controller) { + STAKING_EXTENSION_ADDRESS = _stakingExtensionAddress; SUBGRAPH_DATA_SERVICE_ADDRESS = _subgraphDataServiceAddress; - } + } /** * @notice Delegates the current call to the StakingExtension implementation. @@ -56,8 +56,8 @@ contract HorizonStaking is HorizonStakingV1Storage, IHorizonStaking, GraphUpgrad */ // solhint-disable-next-line payable-fallback, no-complex-fallback fallback() external { - require(_implementation() != address(0), "only through proxy"); - address extensionImpl = L2_STAKING_BACKWARDS_COMPATIBILITY; + //require(_implementation() != address(0), "only through proxy"); + address extensionImpl = STAKING_EXTENSION_ADDRESS; // solhint-disable-next-line no-inline-assembly assembly { // (a) get free memory pointer @@ -84,38 +84,6 @@ contract HorizonStaking is HorizonStakingV1Storage, IHorizonStaking, GraphUpgrad } } - /** - * @notice Allow verifier for stake provisions. - * After calling this, and a timelock period, the service provider will - * be allowed to provision stake that is slashable by the verifier. - * @param _verifier The address of the contract that can slash the provision - */ - function allowVerifier(address _verifier) external override { - if (_verifier == address(0)) { - revert HorizonStakingInvalidVerifier(_verifier); - } - if (verifierAllowlist[msg.sender][_verifier]) { - revert HorizonStakingVerifierAlreadyAllowed(_verifier); - } - verifierAllowlist[msg.sender][_verifier] = true; - emit VerifierAllowed(msg.sender, _verifier); - } - - /** - * @notice Deny a verifier for stake provisions. - * After calling this, the service provider will immediately - * be unable to provision any stake to the verifier. - * Any existing provisions will be unaffected. - * @param _verifier The address of the contract that can slash the provision - */ - function denyVerifier(address _verifier) external override { - if (!verifierAllowlist[msg.sender][_verifier]) { - revert HorizonStakingVerifierNotAllowed(_verifier); - } - verifierAllowlist[msg.sender][_verifier] = false; - emit VerifierDenied(msg.sender, _verifier); - } - /** * @notice Deposit tokens on the caller's stake. * @param _tokens Amount of tokens to stake @@ -389,11 +357,7 @@ contract HorizonStaking is HorizonStakingV1Storage, IHorizonStaking, GraphUpgrad * @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 */ - function isAuthorized( - address _operator, - address _serviceProvider, - address _verifier - ) private view returns (bool) { + function isAuthorized(address _operator, address _serviceProvider, address _verifier) private view returns (bool) { if (_operator == _serviceProvider) { return true; } @@ -416,40 +380,10 @@ contract HorizonStaking is HorizonStakingV1Storage, IHorizonStaking, GraphUpgrad // provisioned tokens from the service provider that are not being thawed // `Provision.tokens - Provision.tokensThawing` - function getProviderTokensAvailable( - address _serviceProvider, - address _verifier - ) public view returns (uint256) { + function getProviderTokensAvailable(address _serviceProvider, address _verifier) public view returns (uint256) { return provisions[_serviceProvider][_verifier].tokens - provisions[_serviceProvider][_verifier].tokensThawing; } - /** - * @notice Authorize or unauthorize an address to be an operator for the caller on a data service. - * @param _operator Address to authorize or unauthorize - * @param _verifier The verifier / data service on which they'll be allowed to operate - * @param _allowed Whether the operator is authorized or not - */ - function setOperator(address _operator, address _verifier, bool _allowed) external override { - require(_operator != msg.sender, "operator == sender"); - if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { - legacyOperatorAuth[msg.sender][_operator] = _allowed; - } else { - operatorAuth[msg.sender][_verifier][_operator] = _allowed; - } - emit OperatorSet(msg.sender, _operator, _verifier, _allowed); - } - - /** - * @notice Authorize or unauthorize an address to be an operator for the caller on all data services. - * @param _operator Address to authorize or unauthorize - * @param _allowed Whether the operator is authorized or not - */ - function setGlobalOperator(address _operator, bool _allowed) external override { - require(_operator != msg.sender, "operator == sender"); - globalOperatorAuth[msg.sender][_operator] = _allowed; - emit GlobalOperatorSet(msg.sender, _operator, _allowed); - } - /** * @notice Check if an operator is authorized for the caller on all their allowlisted verifiers and global stake. * @param _operator The address to check for auth @@ -536,7 +470,10 @@ contract HorizonStaking is HorizonStakingV1Storage, IHorizonStaking, GraphUpgrad 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 >= MINIMUM_DELEGATION, "!minimum-delegation"); + } bytes32 thawRequestId = keccak256( abi.encodePacked(_serviceProvider, _verifier, msg.sender, delegation.nextThawRequestNonce) ); diff --git a/packages/horizon/contracts/HorizonStakingExtension.sol b/packages/horizon/contracts/HorizonStakingExtension.sol index 7bd6bfdea..2571a9928 100644 --- a/packages/horizon/contracts/HorizonStakingExtension.sol +++ b/packages/horizon/contracts/HorizonStakingExtension.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.24; import { StakingBackwardsCompatibility } from "./StakingBackwardsCompatibility.sol"; import { IL2StakingBase } from "@graphprotocol/contracts/contracts/l2/staking/IL2StakingBase.sol"; import { IL2StakingTypes } from "@graphprotocol/contracts/contracts/l2/staking/IL2StakingTypes.sol"; -import { IHorizonStaking } from "./IHorizonStaking.sol"; +import { IHorizonStakingExtension } from "./IHorizonStakingExtension.sol"; /** * @title L2Staking contract @@ -13,7 +13,7 @@ import { IHorizonStaking } from "./IHorizonStaking.sol"; * to receive an indexer's stake or delegation from L1. Note that this contract inherits Staking, * which uses a StakingExtension contract to implement the full IStaking interface through delegatecalls. */ -contract HorizonStakingExtension is StakingBackwardsCompatibility, IL2StakingBase { +contract HorizonStakingExtension is StakingBackwardsCompatibility, IHorizonStakingExtension, IL2StakingBase { /// @dev Minimum amount of tokens that can be delegated uint256 private constant MINIMUM_DELEGATION = 1e18; @@ -25,10 +25,18 @@ contract HorizonStakingExtension is StakingBackwardsCompatibility, IL2StakingBas _; } - constructor(address _controller, address _subgraphDataServiceAddress) StakingBackwardsCompatibility(_controller, _subgraphDataServiceAddress) {} + error HorizonStakingInvalidVerifier(address verifier); + error HorizonStakingVerifierAlreadyAllowed(address verifier); + error HorizonStakingVerifierNotAllowed(address verifier); + + constructor( + address _controller, + address _subgraphDataServiceAddress, + address _exponentialRebates + ) StakingBackwardsCompatibility(_controller, _subgraphDataServiceAddress, _exponentialRebates) {} /** - * @notice Receive ETH into the L2Staking contract: this will always revert + * @notice Receive ETH into the Staking contract: this will always revert * @dev This function is only here to prevent ETH from being sent to the contract */ receive() external payable { @@ -37,25 +45,16 @@ contract HorizonStakingExtension is StakingBackwardsCompatibility, IL2StakingBas // total staked tokens to the provider // `ServiceProvider.tokensStaked - function getStake(address serviceProvider) external view returns (uint256 tokens) { + function getStake(address serviceProvider) external view override returns (uint256) { return serviceProviders[serviceProvider].tokensStaked; } - // provisioned tokens from the service provider that are not being thawed - // `Provision.tokens - Provision.tokensThawing` - function _getProviderTokensAvailable( - address _serviceProvider, - address _verifier - ) private view returns (uint256) { - return provisions[_serviceProvider][_verifier].tokens - provisions[_serviceProvider][_verifier].tokensThawing; - } - // provisioned tokens from delegators that are not being thawed // `Provision.delegatedTokens - Provision.delegatedTokensThawing` function getDelegatedTokensAvailable( address _serviceProvider, address _verifier - ) public view returns (uint256) { + ) public view override returns (uint256) { if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { return legacyDelegationPools[_serviceProvider].tokens - @@ -67,13 +66,14 @@ contract HorizonStakingExtension is StakingBackwardsCompatibility, IL2StakingBas } // provisioned tokens that are not being thawed (including provider tokens and delegation) - function getTokensAvailable(address _serviceProvider, address _verifier) external view returns (uint256) { + function getTokensAvailable(address _serviceProvider, address _verifier) external view override returns (uint256) { return - _getProviderTokensAvailable(_serviceProvider, _verifier) + + provisions[_serviceProvider][_verifier].tokens - + provisions[_serviceProvider][_verifier].tokensThawing + getDelegatedTokensAvailable(_serviceProvider, _verifier); } - function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory) { + function getServiceProvider(address serviceProvider) external view override returns (ServiceProvider memory) { ServiceProvider memory sp; ServiceProviderInternal storage spInternal = serviceProviders[serviceProvider]; sp.tokensStaked = spInternal.tokensStaked; @@ -82,6 +82,73 @@ contract HorizonStakingExtension is StakingBackwardsCompatibility, IL2StakingBas return sp; } + /** + * @notice Allow verifier for stake provisions. + * After calling this, and a timelock period, the service provider will + * be allowed to provision stake that is slashable by the verifier. + * @param _verifier The address of the contract that can slash the provision + */ + function allowVerifier(address _verifier) external override { + if (_verifier == address(0)) { + revert HorizonStakingInvalidVerifier(_verifier); + } + if (verifierAllowlist[msg.sender][_verifier]) { + revert HorizonStakingVerifierAlreadyAllowed(_verifier); + } + verifierAllowlist[msg.sender][_verifier] = true; + emit VerifierAllowed(msg.sender, _verifier); + } + + /** + * @notice Deny a verifier for stake provisions. + * After calling this, the service provider will immediately + * be unable to provision any stake to the verifier. + * Any existing provisions will be unaffected. + * @param _verifier The address of the contract that can slash the provision + */ + function denyVerifier(address _verifier) external { + if (!verifierAllowlist[msg.sender][_verifier]) { + revert HorizonStakingVerifierNotAllowed(_verifier); + } + verifierAllowlist[msg.sender][_verifier] = false; + emit VerifierDenied(msg.sender, _verifier); + } + + /** + * @notice Authorize or unauthorize an address to be an operator for the caller on all data services. + * @param _operator Address to authorize or unauthorize + * @param _allowed Whether the operator is authorized or not + */ + function setGlobalOperator(address _operator, bool _allowed) external override { + require(_operator != msg.sender, "operator == sender"); + globalOperatorAuth[msg.sender][_operator] = _allowed; + emit GlobalOperatorSet(msg.sender, _operator, _allowed); + } + + function isAllowedVerifier(address _serviceProvider, address _verifier) external view override returns (bool) { + return _verifier == SUBGRAPH_DATA_SERVICE_ADDRESS || verifierAllowlist[_serviceProvider][_verifier]; + } + + /** + * @notice Authorize or unauthorize an address to be an operator for the caller on a data service. + * @param _operator Address to authorize or unauthorize + * @param _verifier The verifier / data service on which they'll be allowed to operate + * @param _allowed Whether the operator is authorized or not + */ + function setOperator(address _operator, address _verifier, bool _allowed) external override { + require(_operator != msg.sender, "operator == sender"); + if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { + legacyOperatorAuth[msg.sender][_operator] = _allowed; + } else { + operatorAuth[msg.sender][_verifier][_operator] = _allowed; + } + emit OperatorSet(msg.sender, _operator, _verifier, _allowed); + } + + function getMaxThawingPeriod() external view override returns (uint64) { + return maxThawingPeriod; + } + /** * @notice Receive tokens with a callhook from the bridge. * @dev The encoded _data can contain information about an indexer's stake diff --git a/packages/horizon/contracts/HorizonStakingStorage.sol b/packages/horizon/contracts/HorizonStakingStorage.sol index bc66e2748..cec775a94 100644 --- a/packages/horizon/contracts/HorizonStakingStorage.sol +++ b/packages/horizon/contracts/HorizonStakingStorage.sol @@ -9,8 +9,6 @@ import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; /** * @title HorizonStakingV1Storage * @notice This contract holds all the storage variables for the Staking contract, version 1 - * @dev Note that we use a double underscore prefix for variable names; this prefix identifies - * variables that used to be public but are now internal, getters can be found on StakingExtension.sol. */ // solhint-disable-next-line max-states-count abstract contract HorizonStakingV1Storage is Managed, IHorizonStakingTypes { @@ -119,10 +117,10 @@ abstract contract HorizonStakingV1Storage is Managed, IHorizonStakingTypes { /// Verifier allowlist by service provider /// serviceProvider => verifier => allowed - mapping(address => mapping(address => bool)) public verifierAllowlist; + mapping(address => mapping(address => bool)) internal verifierAllowlist; /// Maximum thawing period, in seconds, for a provision - uint64 public maxThawingPeriod; + uint64 internal maxThawingPeriod; /// @dev Provisions from each service provider for each data service /// ServiceProvider => Verifier => Provision diff --git a/packages/horizon/contracts/IHorizonStaking.sol b/packages/horizon/contracts/IHorizonStaking.sol index 4758a48e8..2c9dab033 100644 --- a/packages/horizon/contracts/IHorizonStaking.sol +++ b/packages/horizon/contracts/IHorizonStaking.sol @@ -3,207 +3,7 @@ pragma solidity >=0.6.12 <0.9.0; pragma abicoder v2; -import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; +import { IHorizonStakingBase } from "./IHorizonStakingBase.sol"; +import { IHorizonStakingExtension } from "./IHorizonStakingExtension.sol"; -interface IHorizonStaking is IHorizonStakingTypes { - - /** - * @dev Emitted when `serviceProvider` stakes `tokens` amount. - */ - event StakeDeposited(address indexed serviceProvider, uint256 tokens); - - /** - * @dev Emitted when `serviceProvider` withdraws `tokens` amount. - */ - event StakeWithdrawn(address indexed serviceProvider, uint256 tokens); - - /** - * @dev Emitted when `serviceProvider` locks `tokens` amount until `until`. - */ - event StakeLocked(address indexed serviceProvider, uint256 tokens, uint256 until); - - /** - * @dev Emitted when serviceProvider allows a verifier - */ - event VerifierAllowed(address indexed serviceProvider, address indexed verifier); - - /** - * @dev Emitted when serviceProvider denies a verifier - */ - event VerifierDenied(address indexed serviceProvider, address indexed verifier); - - /** - * @dev Emitted when an operator is allowed or denied by a service provider for a particular data service - */ - event OperatorSet(address indexed serviceProvider, address indexed operator, address verifier, bool allowed); - - /** - * @dev Emitted when a global operator (for all data services) is allowed or denied by a service provider - */ - event GlobalOperatorSet(address indexed serviceProvider, address indexed operator, bool allowed); - - /** - * @dev Emitted when a service provider provisions staked tokens to a verifier - */ - event ProvisionCreated( - address indexed serviceProvider, - address indexed verifier, - uint256 tokens, - uint32 maxVerifierCut, - uint64 thawingPeriod - ); - - /** - * @dev Emitted when a service provider increases the tokens in a provision - */ - event ProvisionIncreased(address indexed serviceProvider, address indexed verifier, uint256 tokens); - - /** - * @dev Emitted when a thawing request is initiated by a service provider - */ - event ProvisionThawInitiated( - address indexed serviceProvider, - address indexed verifier, - uint256 tokens, - uint64 thawingUntil, - bytes32 indexed thawRequestId - ); - - /** - * @dev Emitted when a service provider removes tokens from a provision after thawing - */ - event ProvisionThawFulfilled( - address indexed serviceProvider, - address indexed verifier, - uint256 tokens, - bytes32 indexed thawRequestId - ); - - event ProvisionSlashed(address indexed serviceProvider, address indexed verifier, uint256 tokens); - - event DelegationSlashed(address indexed serviceProvider, address indexed verifier, uint256 tokens); - - event DelegationSlashingSkipped(address indexed serviceProvider, address indexed verifier, uint256 tokens); - - event VerifierCutSent( - address indexed serviceProvider, - address indexed verifier, - address indexed destination, - uint256 tokens - ); - - event TokensDelegated( - address indexed serviceProvider, - address indexed verifier, - address indexed delegator, - uint256 tokens - ); - - event TokensUndelegated( - address indexed serviceProvider, - address indexed verifier, - address indexed delegator, - uint256 tokens - ); - - event DelegatedTokensWithdrawn( - address indexed serviceProvider, - address indexed verifier, - address indexed delegator, - uint256 tokens - ); - - event DelegationSlashingEnabled(bool enabled); - - // whitelist/deny a verifier - function allowVerifier(address _verifier) external; - - function denyVerifier(address _verifier) external; - - // deposit stake - function stake(uint256 _tokens) external; - - function stakeTo(address _serviceProvider, uint256 _tokens) external; - - // can be called by anyone if the indexer has provisioned stake to this verifier - function stakeToProvision(address _serviceProvider, address _verifier, uint256 _tokens) external; - - // create a provision - function provision( - address _serviceProvider, - address _verifier, - uint256 _tokens, - uint32 _maxVerifierCut, - uint64 _thawingPeriod - ) external; - - // initiate a thawing to remove tokens from a provision - function thaw(address _serviceProvider, address _verifier, uint256 _tokens) external returns (bytes32); - - // add more tokens from idle stake to an existing provision - function addToProvision(address _serviceProvider, address _verifier, uint256 _tokens) external; - - // moves thawed stake from a provision back into the provider's available stake - function deprovision(address _serviceProvider, address _verifier, uint256 _tokens) external; - - // moves thawed stake from one provision into another provision - function reprovision( - address _serviceProvider, - address _oldVerifier, - address _newVerifier, - uint256 _tokens - ) external; - - // moves thawed stake back to the owner's account - stake is removed from the protocol - function unstake(address _serviceProvider, uint256 _tokens) external; - - // delegate tokens to a provider on a data service - function delegate(address _serviceProvider, address _verifier, uint256 _tokens) external; - - // undelegate (thaw) delegated tokens from a provision - function undelegate(address _serviceProvider, address _verifier, uint256 _shares) external; - - // withdraw delegated tokens after thawing - function withdrawDelegated(address _serviceProvider, address _verifier, address _newServiceProvider) external; - - function slash( - address _serviceProvider, - uint256 _tokens, - uint256 _verifierCutAmount, - address _verifierCutDestination - ) external; - - // staked tokens that are currently not provisioned, aka idle stake - // `getStake(serviceProvider) - ServiceProvider.tokensProvisioned` - function getIdleStake(address _serviceProvider) external view returns (uint256 tokens); - - /** - * @notice Authorize or unauthorize an address to be an operator for the caller on a specific verifier / data service. - * @param _operator Address to authorize or unauthorize - * @param _allowed Whether the operator is authorized or not - */ - function setOperator(address _operator, address _verifier, bool _allowed) external; - - /** - * @notice Authorize or unauthorize an address to be an operator for the caller on all provisions. - * @param _operator Address to authorize or unauthorize - * @param _allowed Whether the operator is authorized or not - */ - function setGlobalOperator(address _operator, bool _allowed) external; - - /** - * @notice Check if an operator is authorized for the caller on all their allowlisted verifiers and global stake. - * @param _operator The address to check for auth - * @param _serviceProvider The service provider on behalf of whom they're claiming to act - */ - function isGlobalAuthorized(address _operator, address _serviceProvider) external view returns (bool); - - /** - * @notice Withdraw indexer tokens once the thawing period has passed. - * @dev This is only needed during the transition period while we still have - * a global lock. After that, unstake() will also withdraw. - */ - function withdrawLocked(address _serviceProvider) external; - - function setDelegationSlashingEnabled(bool _enabled) external; -} +interface IHorizonStaking is IHorizonStakingBase, IHorizonStakingExtension {} diff --git a/packages/horizon/contracts/IHorizonStakingBase.sol b/packages/horizon/contracts/IHorizonStakingBase.sol new file mode 100644 index 000000000..b014bd93b --- /dev/null +++ b/packages/horizon/contracts/IHorizonStakingBase.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.6.12 <0.9.0; +pragma abicoder v2; + +import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; + +interface IHorizonStakingBase is IHorizonStakingTypes { + /** + * @dev Emitted when `serviceProvider` stakes `tokens` amount. + */ + event StakeDeposited(address indexed serviceProvider, uint256 tokens); + + /** + * @dev Emitted when `serviceProvider` withdraws `tokens` amount. + */ + event StakeWithdrawn(address indexed serviceProvider, uint256 tokens); + + /** + * @dev Emitted when `serviceProvider` locks `tokens` amount until `until`. + */ + event StakeLocked(address indexed serviceProvider, uint256 tokens, uint256 until); + + /** + * @dev Emitted when a service provider provisions staked tokens to a verifier + */ + event ProvisionCreated( + address indexed serviceProvider, + address indexed verifier, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ); + + /** + * @dev Emitted when a service provider increases the tokens in a provision + */ + event ProvisionIncreased(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + /** + * @dev Emitted when a thawing request is initiated by a service provider + */ + event ProvisionThawInitiated( + address indexed serviceProvider, + address indexed verifier, + uint256 tokens, + uint64 thawingUntil, + bytes32 indexed thawRequestId + ); + + /** + * @dev Emitted when a service provider removes tokens from a provision after thawing + */ + event ProvisionThawFulfilled( + address indexed serviceProvider, + address indexed verifier, + uint256 tokens, + bytes32 indexed thawRequestId + ); + + event ProvisionSlashed(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + event DelegationSlashed(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + event DelegationSlashingSkipped(address indexed serviceProvider, address indexed verifier, uint256 tokens); + + event VerifierCutSent( + address indexed serviceProvider, + address indexed verifier, + address indexed destination, + uint256 tokens + ); + + event TokensDelegated( + address indexed serviceProvider, + address indexed verifier, + address indexed delegator, + uint256 tokens + ); + + event TokensUndelegated( + address indexed serviceProvider, + address indexed verifier, + address indexed delegator, + uint256 tokens + ); + + event DelegatedTokensWithdrawn( + address indexed serviceProvider, + address indexed verifier, + address indexed delegator, + uint256 tokens + ); + + event DelegationSlashingEnabled(bool enabled); + + // deposit stake + function stake(uint256 _tokens) external; + + function stakeTo(address _serviceProvider, uint256 _tokens) external; + + // can be called by anyone if the indexer has provisioned stake to this verifier + function stakeToProvision(address _serviceProvider, address _verifier, uint256 _tokens) external; + + // create a provision + function provision( + address _serviceProvider, + address _verifier, + uint256 _tokens, + uint32 _maxVerifierCut, + uint64 _thawingPeriod + ) external; + + // initiate a thawing to remove tokens from a provision + function thaw(address _serviceProvider, address _verifier, uint256 _tokens) external returns (bytes32); + + // add more tokens from idle stake to an existing provision + function addToProvision(address _serviceProvider, address _verifier, uint256 _tokens) external; + + // moves thawed stake from a provision back into the provider's available stake + function deprovision(address _serviceProvider, address _verifier, uint256 _tokens) external; + + // moves thawed stake from one provision into another provision + function reprovision( + address _serviceProvider, + address _oldVerifier, + address _newVerifier, + uint256 _tokens + ) external; + + // moves thawed stake back to the owner's account - stake is removed from the protocol + function unstake(address _serviceProvider, uint256 _tokens) external; + + // delegate tokens to a provider on a data service + function delegate(address _serviceProvider, address _verifier, uint256 _tokens) external; + + // undelegate (thaw) delegated tokens from a provision + function undelegate(address _serviceProvider, address _verifier, uint256 _shares) external; + + // withdraw delegated tokens after thawing + function withdrawDelegated(address _serviceProvider, address _verifier, address _newServiceProvider) external; + + function slash( + address _serviceProvider, + uint256 _tokens, + uint256 _verifierCutAmount, + address _verifierCutDestination + ) external; + + // staked tokens that are currently not provisioned, aka idle stake + // `getStake(serviceProvider) - ServiceProvider.tokensProvisioned` + function getIdleStake(address _serviceProvider) external view returns (uint256 tokens); + + /** + * @notice Check if an operator is authorized for the caller on all their allowlisted verifiers and global stake. + * @param _operator The address to check for auth + * @param _serviceProvider The service provider on behalf of whom they're claiming to act + */ + function isGlobalAuthorized(address _operator, address _serviceProvider) external view returns (bool); + + /** + * @notice Withdraw indexer tokens once the thawing period has passed. + * @dev This is only needed during the transition period while we still have + * a global lock. After that, unstake() will also withdraw. + */ + function withdrawLocked(address _serviceProvider) external; + + function setDelegationSlashingEnabled(bool _enabled) external; +} diff --git a/packages/horizon/contracts/IHorizonStakingExtension.sol b/packages/horizon/contracts/IHorizonStakingExtension.sol new file mode 100644 index 000000000..013147abc --- /dev/null +++ b/packages/horizon/contracts/IHorizonStakingExtension.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.6.12 <0.9.0; +pragma abicoder v2; + +import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; + +interface IHorizonStakingExtension { + /** + * @dev Emitted when serviceProvider allows a verifier + */ + event VerifierAllowed(address indexed serviceProvider, address indexed verifier); + + /** + * @dev Emitted when serviceProvider denies a verifier + */ + event VerifierDenied(address indexed serviceProvider, address indexed verifier); + + /** + * @dev Emitted when a global operator (for all data services) is allowed or denied by a service provider + */ + event GlobalOperatorSet(address indexed serviceProvider, address indexed operator, bool allowed); + + /** + * @dev Emitted when an operator is allowed or denied by a service provider for a particular data service + */ + event OperatorSet(address indexed serviceProvider, address indexed operator, address verifier, bool allowed); + + function getStake(address serviceProvider) external view returns (uint256); + + function getDelegatedTokensAvailable(address _serviceProvider, address _verifier) external view returns (uint256); + + function getTokensAvailable(address _serviceProvider, address _verifier) external view returns (uint256); + + function getServiceProvider( + address serviceProvider + ) external view returns (IHorizonStakingTypes.ServiceProvider memory); + + function allowVerifier(address _verifier) external; + + /** + * @notice Deny a verifier for stake provisions. + * After calling this, the service provider will immediately + * be unable to provision any stake to the verifier. + * Any existing provisions will be unaffected. + * @param _verifier The address of the contract that can slash the provision + */ + function denyVerifier(address _verifier) external; + + /** + * @notice Authorize or unauthorize an address to be an operator for the caller on all data services. + * @param _operator Address to authorize or unauthorize + * @param _allowed Whether the operator is authorized or not + */ + function setGlobalOperator(address _operator, bool _allowed) external; + + /** + * @notice Authorize or unauthorize an address to be an operator for the caller on a data service. + * @param _operator Address to authorize or unauthorize + * @param _verifier The verifier / data service on which they'll be allowed to operate + * @param _allowed Whether the operator is authorized or not + */ + function setOperator(address _operator, address _verifier, bool _allowed) external; + + function isAllowedVerifier(address _serviceProvider, address _verifier) external view returns (bool); + + function getMaxThawingPeriod() external view returns (uint64); +} diff --git a/packages/horizon/contracts/IStakingBackwardsCompatibility.sol b/packages/horizon/contracts/IStakingBackwardsCompatibility.sol index afba76338..43c93662a 100644 --- a/packages/horizon/contracts/IStakingBackwardsCompatibility.sol +++ b/packages/horizon/contracts/IStakingBackwardsCompatibility.sol @@ -186,13 +186,4 @@ interface IStakingBackwardsCompatibility { * @return Total tokens allocated to subgraph */ function getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentID) external view returns (uint256); - - /** - * @notice Check if an operator is authorized for the caller on a specific verifier / data service. - * @dev They might be the service provider, a global operator or a verifier-specific operator. - * @param _operator The address to check for auth - * @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 - */ - function isAuthorized(address _operator, address _serviceProvider, address _verifier) external view returns (bool); } diff --git a/packages/horizon/contracts/StakingBackwardsCompatibility.sol b/packages/horizon/contracts/StakingBackwardsCompatibility.sol index 6b47702b0..dcc4b3c46 100644 --- a/packages/horizon/contracts/StakingBackwardsCompatibility.sol +++ b/packages/horizon/contracts/StakingBackwardsCompatibility.sol @@ -31,8 +31,7 @@ abstract contract StakingBackwardsCompatibility is HorizonStakingV1Storage, GraphUpgradeable, Multicall, - IStakingBackwardsCompatibility, - ExponentialRebates + IStakingBackwardsCompatibility { /// @dev 100% in parts per million uint32 internal constant MAX_PPM = 1000000; @@ -41,7 +40,11 @@ abstract contract StakingBackwardsCompatibility is address public immutable EXPONENTIAL_REBATES_ADDRESS; - constructor(address _controller, address _subgraphDataServiceAddress, address _exponentialRebatesAddress) Managed(_controller) { + constructor( + address _controller, + address _subgraphDataServiceAddress, + address _exponentialRebatesAddress + ) Managed(_controller) { SUBGRAPH_DATA_SERVICE_ADDRESS = _subgraphDataServiceAddress; EXPONENTIAL_REBATES_ADDRESS = _exponentialRebatesAddress; } @@ -52,11 +55,7 @@ abstract contract StakingBackwardsCompatibility is * @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 */ - function isAuthorized( - address _operator, - address _serviceProvider, - address _verifier - ) internal view override returns (bool) { + function isAuthorized(address _operator, address _serviceProvider, address _verifier) internal view returns (bool) { if (_operator == _serviceProvider) { return true; } diff --git a/packages/horizon/foundry.toml b/packages/horizon/foundry.toml index 55f7ffd31..38d41a40d 100644 --- a/packages/horizon/foundry.toml +++ b/packages/horizon/foundry.toml @@ -3,4 +3,6 @@ src = 'contracts' out = 'build' libs = ['node_modules', 'lib'] test = 'test' -cache_path = 'cache_forge' \ No newline at end of file +cache_path = 'cache_forge' +optimizer = true +optimizer-runs = 200 diff --git a/packages/horizon/test/HorizonStaking.t.sol b/packages/horizon/test/HorizonStaking.t.sol index 68750f820..68005bb5f 100644 --- a/packages/horizon/test/HorizonStaking.t.sol +++ b/packages/horizon/test/HorizonStaking.t.sol @@ -7,11 +7,12 @@ import { HorizonStaking } from "../contracts/HorizonStaking.sol"; import { ControllerMock } from "../contracts/mocks/ControllerMock.sol"; import { HorizonStakingExtension } from "../contracts/HorizonStakingExtension.sol"; import { ExponentialRebates } from "../contracts/utils/ExponentialRebates.sol"; +import { IHorizonStaking } from "../contracts/IHorizonStaking.sol"; contract HorizonStakingTest is Test { ExponentialRebates rebates; HorizonStakingExtension ext; - HorizonStaking staking; + IHorizonStaking staking; ControllerMock controller; function setUp() public { @@ -20,10 +21,21 @@ contract HorizonStakingTest is Test { console.log("Deploying HorizonStaking"); rebates = new ExponentialRebates(); ext = new HorizonStakingExtension(address(controller), address(0x1), address(rebates)); - staking = new HorizonStaking(address(controller), address(ext), address(0x1)); + staking = IHorizonStaking(address(new HorizonStaking(address(controller), address(ext), address(0x1)))); } - function test_MinimumDelegationConstant() public view { - assertEq(staking.MINIMUM_DELEGATION(), 1e18); + function test_AllowVerifier() public { + address verifier = address(0x1337); + address serviceProvider = address(this); + HorizonStakingExtension(payable(address(staking))).allowVerifier(verifier); + assertTrue(staking.isAllowedVerifier(serviceProvider, verifier)); + } + + function test_SetGlobalOperator() public { + address operator = address(0x1337); + address serviceProvider = address(this); + + staking.setGlobalOperator(operator, true); + assertTrue(staking.isGlobalAuthorized(operator, serviceProvider)); } } diff --git a/packages/horizon/test/HorizonStaking.ts b/packages/horizon/test/HorizonStaking.ts index bb79147ab..ead17a92c 100644 --- a/packages/horizon/test/HorizonStaking.ts +++ b/packages/horizon/test/HorizonStaking.ts @@ -3,6 +3,7 @@ import hardhat from 'hardhat' import { expect } from 'chai' import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers' import { ZeroAddress } from 'ethers' +import { IHorizonStaking } from '../typechain-types' const ethers = hardhat.ethers @@ -12,17 +13,25 @@ describe('HorizonStaking', function () { const ControllerMock = await ethers.getContractFactory('ControllerMock') const controller = await ControllerMock.deploy(owner.address) await controller.waitForDeployment() + const ExponentialRebates = await ethers.getContractFactory('ExponentialRebates') + const exponentialRebates = await ExponentialRebates.deploy() + await exponentialRebates.waitForDeployment() + const HorizonStakingExtension = await ethers.getContractFactory('HorizonStakingExtension') + const horizonStakingExtension = await HorizonStakingExtension.deploy(controller.target, ZeroAddress, exponentialRebates.target) + await horizonStakingExtension.waitForDeployment() const HorizonStaking = await ethers.getContractFactory('HorizonStaking') - const horizonStaking = await HorizonStaking.deploy(ZeroAddress, controller.target) - await horizonStaking.waitForDeployment() + const horizonStakingContract = await HorizonStaking.deploy(controller.target, horizonStakingExtension.target, ZeroAddress) + await horizonStakingContract.waitForDeployment() + const horizonStaking = (await ethers.getContractAt('IHorizonStaking', horizonStakingContract.target)) as unknown as IHorizonStaking return { horizonStaking, owner } } - describe('Deployment', function () { - it('Should have a constant max verifier cut', async function () { - const { horizonStaking } = await loadFixture(deployFixture) - - expect(await horizonStaking.MAX_MAX_VERIFIER_CUT()).to.equal(500000) + describe('Verifier allowlist', function () { + it('adds a verifier to the allowlist', async function () { + const { horizonStaking, owner } = await loadFixture(deployFixture) + const verifier = ethers.Wallet.createRandom().address + await horizonStaking.connect(owner).allowVerifier(verifier) + expect(await horizonStaking.isAllowedVerifier(owner, verifier)).to.be.true }) }) })