From e4eb0304e513b66912ef8ce8b0918d6cc50b4c04 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Sat, 13 Apr 2024 02:19:10 -0300 Subject: [PATCH] chore: wip on splitting for code size --- .../contracts/governance/IManaged.sol | 2 +- .../contracts/l2/staking/IL2Staking.sol | 1 + .../contracts/l2/staking/IL2StakingTypes.sol | 2 + .../contracts/l2/staking/L2Staking.sol | 23 ++- .../contracts/staking/IHorizonStaking.sol | 149 ------------------ .../contracts/contracts/staking/L1Staking.sol | 13 +- packages/horizon/contracts/HorizonStaking.sol | 135 ++++++++++------ ...bility.sol => HorizonStakingExtension.sol} | 51 +++++- .../horizon/contracts/IHorizonStaking.sol | 32 ++-- packages/horizon/contracts/Managed.sol | 3 +- packages/horizon/contracts/SimpleTest.sol | 10 -- .../StakingBackwardsCompatibility.sol | 14 +- .../contracts/mocks/ControllerMock.sol | 120 ++++++++++++++ ...Exponential.sol => ExponentialRebates.sol} | 6 +- packages/horizon/package.json | 2 +- packages/horizon/test/HorizonStaking.t.sol | 29 ++++ packages/horizon/test/HorizonStaking.ts | 28 ++++ packages/horizon/test/SimpleTest.t.sol | 17 -- packages/horizon/test/SimpleTest.ts | 23 --- 19 files changed, 371 insertions(+), 289 deletions(-) delete mode 100644 packages/contracts/contracts/staking/IHorizonStaking.sol rename packages/horizon/contracts/{L2StakingBackwardsCompatibility.sol => HorizonStakingExtension.sol} (69%) delete mode 100644 packages/horizon/contracts/SimpleTest.sol create mode 100644 packages/horizon/contracts/mocks/ControllerMock.sol rename packages/horizon/contracts/utils/{LibExponential.sol => ExponentialRebates.sol} (96%) create mode 100644 packages/horizon/test/HorizonStaking.t.sol create mode 100644 packages/horizon/test/HorizonStaking.ts delete mode 100644 packages/horizon/test/SimpleTest.t.sol delete mode 100644 packages/horizon/test/SimpleTest.ts diff --git a/packages/contracts/contracts/governance/IManaged.sol b/packages/contracts/contracts/governance/IManaged.sol index 76f05e0fb..4cedf9c0b 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.6.12 <0.9.0; import { IController } from "./IController.sol"; diff --git a/packages/contracts/contracts/l2/staking/IL2Staking.sol b/packages/contracts/contracts/l2/staking/IL2Staking.sol index a12a35b1b..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 diff --git a/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol b/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol index 7081c0e88..41d3fe445 100644 --- a/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol +++ b/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + pragma solidity >=0.6.12 <0.9.0; interface IL2StakingTypes { 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/staking/IHorizonStaking.sol b/packages/contracts/contracts/staking/IHorizonStaking.sol deleted file mode 100644 index a4101ff52..000000000 --- a/packages/contracts/contracts/staking/IHorizonStaking.sol +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.7.6 <0.9.0; -pragma abicoder v2; - -interface Test { - function test() external returns (uint256); -} - -interface IHorizonStaking { - struct Provision { - // Service provider that created the provision - address serviceProvider; - // tokens in the provision - uint256 tokens; - // tokens that are being thawed (and will stop being slashable soon) - uint256 tokensThawing; - // timestamp of provision creation - uint64 createdAt; - // authority to slash the provision - address verifier; - // max amount that can be taken by the verifier when slashing, expressed in parts-per-million of the amount slashed - uint32 maxVerifierCut; - // time, in seconds, tokens must thaw before being withdrawn - uint64 thawingPeriod; - } - - // the new "Indexer" struct - struct ServiceProviderInternal { - // Tokens on the Service Provider stake (staked by the provider) - uint256 tokensStaked; - // Tokens used in allocations - uint256 __DEPRECATED_tokensAllocated; - // Tokens locked for withdrawal subject to thawing period - uint256 __DEPRECATED_tokensLocked; - // Block when locked tokens can be withdrawn - uint256 __DEPRECATED_tokensLockedUntil; - // tokens used in a provision - uint256 tokensProvisioned; - // tokens that initiated a thawing in any one of the provider's provisions - uint256 tokensRequestedThaw; - // tokens that have been removed from any one of the provider's provisions after thawing - uint256 tokensFulfilledThaw; - // provisions that take priority for undelegation force thawing - bytes32[] forceThawProvisions; - } - - struct ServiceProvider { - // Tokens on the provider stake (staked by the provider) - uint256 tokensStaked; - // tokens used in a provision - uint256 tokensProvisioned; - // tokens that initiated a thawing in any one of the provider's provisions - uint256 tokensRequestedThaw; - // tokens that have been removed from any one of the provider's provisions after thawing - uint256 tokensFulfilledThaw; - // provisions that take priority for undelegation force thawing - bytes32[] forceThawProvisions; - } - - struct DelegationPool { - uint32 __DEPRECATED_cooldownBlocks; // solhint-disable-line var-name-mixedcase - uint32 __DEPRECATED_indexingRewardCut; // in PPM - uint32 __DEPRECATED_queryFeeCut; // in PPM - uint256 __DEPRECATED_updatedAtBlock; // Block when the pool was last updated - uint256 tokens; // Total tokens as pool reserves - uint256 shares; // Total shares minted in the pool - mapping(address => Delegation) delegators; // Mapping of delegator => Delegation - } - - struct Delegation { - // shares owned by the delegator in the pool - uint256 shares; - // tokens delegated to the pool - uint256 tokens; - // Timestamp when locked tokens can be undelegated (after the timelock) - uint256 tokensLockedUntil; - } - - struct ThawRequest { - // tokens that are being thawed by this request - uint256 tokens; - // the provision id to which this request corresponds to - bytes32 provisionId; - // the address that initiated the thaw request, allowed to remove the funds once thawed - address owner; - // the timestamp when the thawed funds can be removed from the provision - uint64 thawingUntil; - // the value of `ServiceProvider.tokensRequestedThaw` the moment the thaw request is created - uint256 tokensRequestedThawSnapshot; - } - - // whitelist/deny a verifier - function allowVerifier(address verifier, bool allow) external; - - // deposit stake - function stake(uint256 tokens) external; - - // create a provision - function provision(uint256 tokens, address verifier, uint256 maxVerifierCut, uint256 thawingPeriod) external; - - // initiate a thawing to remove tokens from a provision - function thaw(bytes32 provisionId, uint256 tokens) external returns (bytes32 thawRequestId); - - // moves thawed stake from a provision back into the provider's available stake - function deprovision(bytes32 thawRequestId) external; - - // moves thawed stake from one provision into another provision - function reprovision(bytes32 thawRequestId, bytes32 provisionId) external; - - // moves thawed stake back to the owner's account - stake is removed from the protocol - function withdraw(bytes32 thawRequestId) external; - - // delegate tokens to a provider - function delegate(address serviceProvider, uint256 tokens) external; - - // undelegate tokens - function undelegate( - address serviceProvider, - uint256 tokens, - bytes32[] calldata provisions - ) external returns (bytes32 thawRequestId); - - // slash a service provider - function slash(bytes32 provisionId, uint256 tokens, uint256 verifierAmount) external; - - // set the Service Provider's preferred provisions to be force thawed - function setForceThawProvisions(bytes32[] calldata provisions) external; - - // total staked tokens to the provider - // `ServiceProvider.tokensStaked + DelegationPool.serviceProvider.tokens` - function getStake(address serviceProvider) external view returns (uint256 tokens); - - // staked tokens that are currently not provisioned, aka idle stake - // `getStake(serviceProvider) - ServiceProvider.tokensProvisioned` - function getIdleStake(address serviceProvider) external view returns (uint256 tokens); - - // staked tokens the provider can provision before hitting the delegation cap - // `ServiceProvider.tokensStaked * Staking.delegationRatio - Provision.tokensProvisioned` - function getCapacity(address serviceProvider) external view returns (uint256 tokens); - - // provisioned tokens that are not being used - // `Provision.tokens - Provision.tokensThawing` - function getTokensAvailable(bytes32 provision) external view returns (uint256 tokens); - - function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory); - - function getProvision(bytes32 provision) external view returns (Provision memory); -} 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/horizon/contracts/HorizonStaking.sol b/packages/horizon/contracts/HorizonStaking.sol index 81f054e2a..6c50dae4e 100644 --- a/packages/horizon/contracts/HorizonStaking.sol +++ b/packages/horizon/contracts/HorizonStaking.sol @@ -2,14 +2,16 @@ pragma solidity 0.8.24; +import { GraphUpgradeable } from "@graphprotocol/contracts/contracts/upgrades/GraphUpgradeable.sol"; + import { IHorizonStaking } from "./IHorizonStaking.sol"; -import { L2StakingBackwardsCompatibility } from "./L2StakingBackwardsCompatibility.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 L2StakingBackwardsCompatibility, IHorizonStaking { +contract HorizonStaking is HorizonStakingV1Storage, IHorizonStaking, 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. @@ -26,6 +28,9 @@ contract HorizonStaking is L2StakingBackwardsCompatibility, IHorizonStaking { /// Minimum delegation size uint256 public constant MINIMUM_DELEGATION = 1e18; + address public immutable L2_STAKING_BACKWARDS_COMPATIBILITY; + address public immutable SUBGRAPH_DATA_SERVICE_ADDRESS; + error HorizonStakingInvalidVerifier(address verifier); error HorizonStakingVerifierAlreadyAllowed(address verifier); error HorizonStakingVerifierNotAllowed(address verifier); @@ -36,9 +41,48 @@ contract HorizonStaking is L2StakingBackwardsCompatibility, IHorizonStaking { error HorizonStakingInsufficientCapacity(); constructor( - address _subgraphDataServiceAddress, - address _controller - ) L2StakingBackwardsCompatibility(_subgraphDataServiceAddress) Managed(_controller) {} + address _controller, + address _l2StakingBackwardsCompatibility, + address _subgraphDataServiceAddress + ) Managed(_controller) { + L2_STAKING_BACKWARDS_COMPATIBILITY = _l2StakingBackwardsCompatibility; + SUBGRAPH_DATA_SERVICE_ADDRESS = _subgraphDataServiceAddress; + } + + /** + * @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 { + require(_implementation() != address(0), "only through proxy"); + address extensionImpl = L2_STAKING_BACKWARDS_COMPATIBILITY; + // 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) + } + } + } /** * @notice Allow verifier for stake provisions. @@ -184,7 +228,7 @@ contract HorizonStaking is L2StakingBackwardsCompatibility, IHorizonStaking { * @param _serviceProvider The service provider address * @param _verifier The verifier address for which the tokens are provisioned */ - function getThawedTokens(address _serviceProvider, address _verifier) external view override returns (uint256) { + function getThawedTokens(address _serviceProvider, address _verifier) external view returns (uint256) { Provision storage prov = provisions[_serviceProvider][_verifier]; if (prov.nThawRequests == 0) { return 0; @@ -339,10 +383,26 @@ contract HorizonStaking is L2StakingBackwardsCompatibility, IHorizonStaking { } } - // total staked tokens to the provider - // `ServiceProvider.tokensStaked - function getStake(address serviceProvider) public view override returns (uint256 tokens) { - return serviceProviders[serviceProvider].tokensStaked; + /** + * @notice Check if an operator is authorized for the caller on a specific verifier / data service. + * @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 + ) private view returns (bool) { + if (_operator == _serviceProvider) { + return true; + } + if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { + return legacyOperatorAuth[_serviceProvider][_operator] || globalOperatorAuth[_serviceProvider][_operator]; + } else { + return + operatorAuth[_serviceProvider][_verifier][_operator] || globalOperatorAuth[_serviceProvider][_operator]; + } } // staked tokens that are currently not provisioned, aka idle stake @@ -359,46 +419,10 @@ contract HorizonStaking is L2StakingBackwardsCompatibility, IHorizonStaking { function getProviderTokensAvailable( address _serviceProvider, address _verifier - ) public view override returns (uint256) { + ) public 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 override returns (uint256) { - if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { - return - legacyDelegationPools[_serviceProvider].tokens - - (legacyDelegationPools[_serviceProvider].tokensThawing); - } - return - delegationPools[_serviceProvider][_verifier].tokens - - (delegationPools[_serviceProvider][_verifier].tokensThawing); - } - - // provisioned tokens that are not being thawed (including provider tokens and delegation) - function getTokensAvailable(address _serviceProvider, address _verifier) public view override returns (uint256) { - return - getProviderTokensAvailable(_serviceProvider, _verifier) + - getDelegatedTokensAvailable(_serviceProvider, _verifier); - } - - function getServiceProvider(address serviceProvider) public view override returns (ServiceProvider memory) { - ServiceProvider memory sp; - ServiceProviderInternal storage spInternal = serviceProviders[serviceProvider]; - sp.tokensStaked = spInternal.tokensStaked; - sp.tokensProvisioned = spInternal.tokensProvisioned; - sp.nextThawRequestNonce = spInternal.nextThawRequestNonce; - return sp; - } - - function getProvision(address _serviceProvider, address _verifier) public view override returns (Provision memory) { - return provisions[_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 @@ -699,4 +723,21 @@ contract HorizonStaking is L2StakingBackwardsCompatibility, IHorizonStaking { _tokens; emit ProvisionIncreased(_serviceProvider, _verifier, _tokens); } + + /** + * @dev Stake tokens on the service provider. + * TODO: Move to HorizonStaking after the transition period + * @param _serviceProvider Address of staking party + * @param _tokens Amount of tokens to stake + */ + function _stake(address _serviceProvider, uint256 _tokens) internal { + // Deposit tokens into the indexer stake + serviceProviders[_serviceProvider].tokensStaked = serviceProviders[_serviceProvider].tokensStaked + _tokens; + + emit StakeDeposited(_serviceProvider, _tokens); + } + + function _graphToken() internal view returns (IGraphToken) { + return IGraphToken(GRAPH_TOKEN); + } } diff --git a/packages/horizon/contracts/L2StakingBackwardsCompatibility.sol b/packages/horizon/contracts/HorizonStakingExtension.sol similarity index 69% rename from packages/horizon/contracts/L2StakingBackwardsCompatibility.sol rename to packages/horizon/contracts/HorizonStakingExtension.sol index 63e04dc70..7bd6bfdea 100644 --- a/packages/horizon/contracts/L2StakingBackwardsCompatibility.sol +++ b/packages/horizon/contracts/HorizonStakingExtension.sol @@ -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. */ -abstract contract L2StakingBackwardsCompatibility is StakingBackwardsCompatibility, IL2StakingBase { +contract HorizonStakingExtension is StakingBackwardsCompatibility, IL2StakingBase { /// @dev Minimum amount of tokens that can be delegated uint256 private constant MINIMUM_DELEGATION = 1e18; @@ -25,7 +25,7 @@ abstract contract L2StakingBackwardsCompatibility is StakingBackwardsCompatibili _; } - constructor(address _subgraphDataServiceAddress) StakingBackwardsCompatibility(_subgraphDataServiceAddress) {} + constructor(address _controller, address _subgraphDataServiceAddress) StakingBackwardsCompatibility(_controller, _subgraphDataServiceAddress) {} /** * @notice Receive ETH into the L2Staking contract: this will always revert @@ -35,6 +35,53 @@ abstract contract L2StakingBackwardsCompatibility is StakingBackwardsCompatibili revert("RECEIVE_ETH_NOT_ALLOWED"); } + // total staked tokens to the provider + // `ServiceProvider.tokensStaked + function getStake(address serviceProvider) external view returns (uint256 tokens) { + 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) { + if (_verifier == SUBGRAPH_DATA_SERVICE_ADDRESS) { + return + legacyDelegationPools[_serviceProvider].tokens - + (legacyDelegationPools[_serviceProvider].tokensThawing); + } + return + delegationPools[_serviceProvider][_verifier].tokens - + (delegationPools[_serviceProvider][_verifier].tokensThawing); + } + + // provisioned tokens that are not being thawed (including provider tokens and delegation) + function getTokensAvailable(address _serviceProvider, address _verifier) external view returns (uint256) { + return + _getProviderTokensAvailable(_serviceProvider, _verifier) + + getDelegatedTokensAvailable(_serviceProvider, _verifier); + } + + function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory) { + ServiceProvider memory sp; + ServiceProviderInternal storage spInternal = serviceProviders[serviceProvider]; + sp.tokensStaked = spInternal.tokensStaked; + sp.tokensProvisioned = spInternal.tokensProvisioned; + sp.nextThawRequestNonce = spInternal.nextThawRequestNonce; + return sp; + } + /** * @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/IHorizonStaking.sol b/packages/horizon/contracts/IHorizonStaking.sol index 4281e47d4..4758a48e8 100644 --- a/packages/horizon/contracts/IHorizonStaking.sol +++ b/packages/horizon/contracts/IHorizonStaking.sol @@ -6,6 +6,22 @@ pragma abicoder v2; import { IHorizonStakingTypes } from "./IHorizonStakingTypes.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 */ @@ -157,26 +173,10 @@ interface IHorizonStaking is IHorizonStakingTypes { address _verifierCutDestination ) external; - // total staked tokens to the provider - // `ServiceProvider.tokensStaked - function getStake(address _serviceProvider) external view returns (uint256 tokens); - // staked tokens that are currently not provisioned, aka idle stake // `getStake(serviceProvider) - ServiceProvider.tokensProvisioned` function getIdleStake(address _serviceProvider) external view returns (uint256 tokens); - function getDelegatedTokensAvailable(address _serviceProvider, address _verifier) external view returns (uint256); - - function getProviderTokensAvailable(address _serviceProvider, address _verifier) external view returns (uint256); - - function getTokensAvailable(address _serviceProvider, address _verifier) external view returns (uint256 tokens); - - function getThawedTokens(address _serviceProvider, address _verifier) external view returns (uint256); - - function getServiceProvider(address _serviceProvider) external view returns (ServiceProvider memory); - - function getProvision(address _serviceProvider, address _verifier) external view returns (Provision memory); - /** * @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 diff --git a/packages/horizon/contracts/Managed.sol b/packages/horizon/contracts/Managed.sol index 78d8bab5b..858156bbd 100644 --- a/packages/horizon/contracts/Managed.sol +++ b/packages/horizon/contracts/Managed.sol @@ -103,9 +103,8 @@ abstract contract Managed is IManaged, GraphDirectory { /** * @notice Set Controller. Deprecated, will revert. - * @param _controller Controller contract address */ - function setController(address _controller) external override onlyController { + function setController(address) external view override onlyController { revert ManagedSetControllerDeprecated(); } } diff --git a/packages/horizon/contracts/SimpleTest.sol b/packages/horizon/contracts/SimpleTest.sol deleted file mode 100644 index 5cc28d9cc..000000000 --- a/packages/horizon/contracts/SimpleTest.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.4.0 <0.9.0; - -import { Test } from "@graphprotocol/contracts/contracts/staking/IHorizonStaking.sol"; - -contract SimpleTest is Test { - function test() external pure returns (uint256) { - return 42; - } -} diff --git a/packages/horizon/contracts/StakingBackwardsCompatibility.sol b/packages/horizon/contracts/StakingBackwardsCompatibility.sol index 3ebe4be00..6b47702b0 100644 --- a/packages/horizon/contracts/StakingBackwardsCompatibility.sol +++ b/packages/horizon/contracts/StakingBackwardsCompatibility.sol @@ -14,7 +14,7 @@ import { Managed } from "./Managed.sol"; import { ICuration } from "@graphprotocol/contracts/contracts/curation/ICuration.sol"; import { IRewardsManager } from "@graphprotocol/contracts/contracts/rewards/IRewardsManager.sol"; import { IEpochManager } from "@graphprotocol/contracts/contracts/epochs/IEpochManager.sol"; -import { LibExponential } from "./utils/LibExponential.sol"; +import { ExponentialRebates } from "./utils/ExponentialRebates.sol"; import { IStakingBackwardsCompatibility } from "./IStakingBackwardsCompatibility.sol"; /** @@ -31,15 +31,19 @@ abstract contract StakingBackwardsCompatibility is HorizonStakingV1Storage, GraphUpgradeable, Multicall, - IStakingBackwardsCompatibility + IStakingBackwardsCompatibility, + ExponentialRebates { /// @dev 100% in parts per million uint32 internal constant MAX_PPM = 1000000; address public immutable SUBGRAPH_DATA_SERVICE_ADDRESS; - constructor(address _subgraphDataServiceAddress) { + address public immutable EXPONENTIAL_REBATES_ADDRESS; + + constructor(address _controller, address _subgraphDataServiceAddress, address _exponentialRebatesAddress) Managed(_controller) { SUBGRAPH_DATA_SERVICE_ADDRESS = _subgraphDataServiceAddress; + EXPONENTIAL_REBATES_ADDRESS = _exponentialRebatesAddress; } /** @@ -52,7 +56,7 @@ abstract contract StakingBackwardsCompatibility is address _operator, address _serviceProvider, address _verifier - ) public view override returns (bool) { + ) internal view override returns (bool) { if (_operator == _serviceProvider) { return true; } @@ -143,7 +147,7 @@ abstract contract StakingBackwardsCompatibility is // No rebates if indexer has no stake or if lambda is zero uint256 newRebates = (alloc.tokens == 0 || __DEPRECATED_lambdaNumerator == 0) ? 0 - : LibExponential.exponentialRebates( + : ExponentialRebates(EXPONENTIAL_REBATES_ADDRESS).exponentialRebates( alloc.collectedFees, alloc.tokens, __DEPRECATED_alphaNumerator, diff --git a/packages/horizon/contracts/mocks/ControllerMock.sol b/packages/horizon/contracts/mocks/ControllerMock.sol new file mode 100644 index 000000000..c6832e595 --- /dev/null +++ b/packages/horizon/contracts/mocks/ControllerMock.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.24; + +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 => address) 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(address _governor) { + governor = _governor; + } + + /** + * @notice Getter to access governor + */ + function getGovernor() external view override returns (address) { + return 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 Get contract proxy address by its id + * @param _id Contract id + * @return Address of the proxy contract for the provided id + */ + function getContractProxy(bytes32 _id) external view override returns (address) { + return _registry[_id]; + } + + /** + * @notice Update contract's controller + * @param _id Contract id (keccak256 hash of contract name) + * @param _controller 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 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/utils/LibExponential.sol b/packages/horizon/contracts/utils/ExponentialRebates.sol similarity index 96% rename from packages/horizon/contracts/utils/LibExponential.sol rename to packages/horizon/contracts/utils/ExponentialRebates.sol index d03b66cce..12cf317a3 100644 --- a/packages/horizon/contracts/utils/LibExponential.sol +++ b/packages/horizon/contracts/utils/ExponentialRebates.sol @@ -5,10 +5,10 @@ pragma solidity 0.8.24; import { LibFixedMath } from "./LibFixedMath.sol"; /** - * @title LibExponential library + * @title Exponential * @notice A library to compute query fee rebates using an exponential formula */ -library LibExponential { +contract ExponentialRebates { /// @dev Maximum value of the exponent for which to compute the exponential before clamping to zero. uint32 private constant MAX_EXPONENT = 15; @@ -34,7 +34,7 @@ library LibExponential { uint32 alphaDenominator, uint32 lambdaNumerator, uint32 lambdaDenominator - ) public pure returns (uint256) { + ) external pure returns (uint256) { // If alpha is zero indexer gets 100% fees rebate int256 alpha = LibFixedMath.toFixed(int32(alphaNumerator), int32(alphaDenominator)); if (alpha == 0) { diff --git a/packages/horizon/package.json b/packages/horizon/package.json index ec23f62cb..15be1e936 100644 --- a/packages/horizon/package.json +++ b/packages/horizon/package.json @@ -10,7 +10,7 @@ "lint": "yarn lint:ts && yarn lint:sol", "clean": "rm -rf build cache typechain-types", "build": "forge build && hardhat compile", - "test": "forge test && hardhat test" + "test": "forge test -vvv && hardhat test" }, "devDependencies": { "@graphprotocol/contracts": "workspace:^7.0.0", diff --git a/packages/horizon/test/HorizonStaking.t.sol b/packages/horizon/test/HorizonStaking.t.sol new file mode 100644 index 000000000..68750f820 --- /dev/null +++ b/packages/horizon/test/HorizonStaking.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.24; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +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"; + +contract HorizonStakingTest is Test { + ExponentialRebates rebates; + HorizonStakingExtension ext; + HorizonStaking staking; + ControllerMock controller; + + function setUp() public { + console.log("Deploying Controller mock"); + controller = new ControllerMock(address(0x1)); + 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)); + } + + function test_MinimumDelegationConstant() public view { + assertEq(staking.MINIMUM_DELEGATION(), 1e18); + } +} diff --git a/packages/horizon/test/HorizonStaking.ts b/packages/horizon/test/HorizonStaking.ts new file mode 100644 index 000000000..bb79147ab --- /dev/null +++ b/packages/horizon/test/HorizonStaking.ts @@ -0,0 +1,28 @@ +import hardhat from 'hardhat' + +import { expect } from 'chai' +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers' +import { ZeroAddress } from 'ethers' + +const ethers = hardhat.ethers + +describe('HorizonStaking', function () { + async function deployFixture() { + const [owner] = await ethers.getSigners() + const ControllerMock = await ethers.getContractFactory('ControllerMock') + const controller = await ControllerMock.deploy(owner.address) + await controller.waitForDeployment() + const HorizonStaking = await ethers.getContractFactory('HorizonStaking') + const horizonStaking = await HorizonStaking.deploy(ZeroAddress, controller.target) + await horizonStaking.waitForDeployment() + 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) + }) + }) +}) diff --git a/packages/horizon/test/SimpleTest.t.sol b/packages/horizon/test/SimpleTest.t.sol deleted file mode 100644 index 1435c313f..000000000 --- a/packages/horizon/test/SimpleTest.t.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.10; - -import "forge-std/Test.sol"; -import { SimpleTest } from "../contracts/SimpleTest.sol"; - -contract ContractTest is Test { - SimpleTest simpleTest; - - function setUp() public { - simpleTest = new SimpleTest(); - } - - function test_NumberIs42() public view { - assertEq(simpleTest.test(), 42); - } -} diff --git a/packages/horizon/test/SimpleTest.ts b/packages/horizon/test/SimpleTest.ts deleted file mode 100644 index cfcfb1443..000000000 --- a/packages/horizon/test/SimpleTest.ts +++ /dev/null @@ -1,23 +0,0 @@ -import hardhat from 'hardhat' - -import { expect } from 'chai' -import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers' - -const ethers = hardhat.ethers - -describe('SimpleTest', function () { - async function deployFixture() { - const [owner] = await ethers.getSigners() - const SimpleTest = await ethers.getContractFactory('SimpleTest') - const simpleTest = await SimpleTest.deploy() - return { simpleTest, owner } - } - - describe('Deployment', function () { - it('Should return 42', async function () { - const { simpleTest } = await loadFixture(deployFixture) - - expect(await simpleTest.test()).to.equal(42) - }) - }) -})