Skip to content

Commit

Permalink
feat: remove transfer tools from horizon (#1053)
Browse files Browse the repository at this point in the history
* feat: remove transfer tools from horizon

Signed-off-by: Tomás Migone <[email protected]>

* fix: deprecate storage variables

Signed-off-by: Tomás Migone <[email protected]>

* fix: remove minimum delegation requirement from delegation flows

Signed-off-by: Tomás Migone <[email protected]>

* fix: minor changes

Signed-off-by: Tomás Migone <[email protected]>

---------

Signed-off-by: Tomás Migone <[email protected]>
  • Loading branch information
tmigone authored Oct 3, 2024
1 parent 570b21e commit 4e41890
Show file tree
Hide file tree
Showing 19 changed files with 30 additions and 515 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
pragma solidity 0.8.27;

import { IRewardsIssuer } from "@graphprotocol/contracts/contracts/rewards/IRewardsIssuer.sol";
import { IL2StakingBase } from "@graphprotocol/contracts/contracts/l2/staking/IL2StakingBase.sol";

/**
* @title Interface for {HorizonStakingExtension} contract.
* @notice Provides functions for managing legacy allocations and transfer tools.
* @notice Provides functions for managing legacy allocations.
*/
interface IHorizonStakingExtension is IRewardsIssuer, IL2StakingBase {
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()
Expand Down Expand Up @@ -39,15 +38,6 @@ interface IHorizonStakingExtension is IRewardsIssuer, IL2StakingBase {
Closed
}

/**
* @notice Emitted when a delegator delegates through the Graph Token Gateway using the transfer tools.
* @dev TODO(after transfer tools): delete
* @param serviceProvider The address of the service provider.
* @param delegator The address of the delegator.
* @param tokens The amount of tokens delegated.
*/
event StakeDelegated(address indexed serviceProvider, address indexed delegator, uint256 tokens, uint256 shares);

/**
* @dev Emitted when `indexer` close an allocation in `epoch` for `allocationID`.
* An amount of `tokens` get unallocated from `subgraphDeploymentID`.
Expand Down Expand Up @@ -87,15 +77,6 @@ interface IHorizonStakingExtension is IRewardsIssuer, IL2StakingBase {
uint256 delegationRewards
);

event CounterpartStakingAddressSet(address indexed counterpart);

/**
* @notice Set the address of the counterpart (L1 or L2) staking contract.
* @dev This function can only be called by the governor.
* @param counterpart Address of the counterpart staking contract in the other chain, without any aliasing.
*/
function setCounterpartStakingAddress(address counterpart) external;

/**
* @notice Close an allocation and free the staked tokens.
* To be eligible for rewards a proof of indexing must be presented.
Expand Down
18 changes: 1 addition & 17 deletions packages/horizon/contracts/staking/HorizonStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ import { HorizonStakingBase } from "./HorizonStakingBase.sol";
* 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 and the transfer tools. Both can be eventually removed without affecting the main
* staking contract.
* the legacy staking functions. It can be eventually removed without affecting the main staking contract.
* @custom:security-contact Please email [email protected] if you find any
* bugs. We may have an active bug bounty program.
*/
Expand All @@ -42,11 +41,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
/// @dev Maximum number of simultaneous stake thaw requests (per provision) or undelegations (per delegation)
uint256 private constant MAX_THAW_REQUESTS = 100;

/// @dev Minimum amount of delegation to prevent rounding attacks.
/// TODO: remove this after L2 transfer tool for delegation is removed
/// (delegation on L2 has its own slippage protection)
uint256 private constant MIN_DELEGATION = 1e18;

/// @dev Address of the staking extension contract
address private immutable STAKING_EXTENSION_ADDRESS;

Expand Down Expand Up @@ -741,8 +735,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
* have been done before calling this function.
*/
function _delegate(address _serviceProvider, address _verifier, uint256 _tokens, uint256 _minSharesOut) private {
// TODO: remove this after L2 transfer tool for delegation is removed
require(_tokens >= MIN_DELEGATION, HorizonStakingInsufficientTokens(_tokens, MIN_DELEGATION));
require(
_provisions[_serviceProvider][_verifier].createdAt != 0,
HorizonStakingInvalidProvision(_serviceProvider, _verifier)
Expand Down Expand Up @@ -795,14 +787,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
pool.sharesThawing = pool.sharesThawing + thawingShares;

delegation.shares = delegation.shares - _shares;
// TODO: remove this when L2 transfer tools are removed
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(
_serviceProvider,
Expand Down
123 changes: 3 additions & 120 deletions packages/horizon/contracts/staking/HorizonStakingExtension.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ 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 { IHorizonStakingTypes } from "../interfaces/internal/IHorizonStakingTypes.sol";
import { IL2StakingTypes } from "@graphprotocol/contracts/contracts/l2/staking/IL2StakingTypes.sol";
import { IL2StakingBase } from "@graphprotocol/contracts/contracts/l2/staking/IL2StakingBase.sol";

import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol";
import { MathUtils } from "../libraries/MathUtils.sol";
Expand All @@ -22,18 +19,15 @@ import { HorizonStakingBase } from "./HorizonStakingBase.sol";
* 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 TODO: Once the transition period and the transfer tools are deemed not necessary this contract
* can be removed. It's expected the transition period to last for a full allocation cycle (28 epochs).
* @dev TODO: Once the transition period passes this contract can be removed. It's expected the transition period to
* last for a full allocation cycle (28 epochs).
* @custom:security-contact Please email [email protected] if you find any
* bugs. We may have an active bug bounty program.
*/
contract HorizonStakingExtension is HorizonStakingBase, IL2StakingBase, IHorizonStakingExtension {
contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension {
using TokenUtils for IGraphToken;
using PPMMath for uint256;

/// @dev Minimum amount of tokens that can be delegated
uint256 private constant MINIMUM_DELEGATION = 1e18;

/**
* @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller.
*/
Expand All @@ -53,52 +47,6 @@ contract HorizonStakingExtension is HorizonStakingBase, IL2StakingBase, IHorizon
address subgraphDataServiceAddress
) HorizonStakingBase(controller, subgraphDataServiceAddress) {}

/**
* @notice Receive tokens with a callhook from the bridge.
* @dev The encoded _data can contain information about an service provider's stake
* or a delegator's delegation.
* See L1MessageCodes in IL2Staking for the supported messages.
* @dev "indexer" in this context refers to a service provider (legacy terminology for the bridge)
* @param from Token sender in L1
* @param tokens Amount of tokens that were transferred
* @param data ABI-encoded callhook data which must include a uint8 code and either a ReceiveIndexerStakeData or ReceiveDelegationData struct.
*/
function onTokenTransfer(
address from,
uint256 tokens,
bytes calldata data
) external override notPaused onlyL2Gateway {
require(from == _counterpartStakingAddress, "ONLY_L1_STAKING_THROUGH_BRIDGE");
(uint8 code, bytes memory functionData) = abi.decode(data, (uint8, bytes));

if (code == uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_INDEXER_STAKE_CODE)) {
IL2StakingTypes.ReceiveIndexerStakeData memory indexerData = abi.decode(
functionData,
(IL2StakingTypes.ReceiveIndexerStakeData)
);
_receiveIndexerStake(tokens, indexerData);
} else if (code == uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_DELEGATION_CODE)) {
IL2StakingTypes.ReceiveDelegationData memory delegationData = abi.decode(
functionData,
(IL2StakingTypes.ReceiveDelegationData)
);
_receiveDelegation(tokens, delegationData);
} else {
revert("INVALID_CODE");
}
}

/**
* @notice Set the address of the counterpart (L1 or L2) staking contract.
* @dev This function can only be called by the governor.
* TODO: Remove after L2 transition period
* @param counterpart Address of the counterpart staking contract in the other chain, without any aliasing.
*/
function setCounterpartStakingAddress(address counterpart) external override onlyGovernor {
_counterpartStakingAddress = counterpart;
emit CounterpartStakingAddressSet(counterpart);
}

/**
* @notice Close an allocation and free the staked tokens.
* To be eligible for rewards a proof of indexing must be presented.
Expand Down Expand Up @@ -318,71 +266,6 @@ contract HorizonStakingExtension is HorizonStakingBase, IL2StakingBase, IHorizon
return _legacyOperatorAuth[serviceProvider][operator];
}

/**
* @dev Receive an Indexer's stake from L1.
* The specified amount is added to the indexer's stake; the indexer's
* address is specified in the _indexerData struct.
* @param _tokens Amount of tokens that were transferred
* @param _indexerData struct containing the indexer's address
*/
function _receiveIndexerStake(
uint256 _tokens,
IL2StakingTypes.ReceiveIndexerStakeData memory _indexerData
) private {
address indexer = _indexerData.indexer;
// Deposit tokens into the indexer stake
_stake(indexer, _tokens);
}

/**
* @dev Receive a Delegator's delegation from L1.
* The specified amount is added to the delegator's delegation; the delegator's
* address and the indexer's address are specified in the _delegationData struct.
* Note that no delegation tax is applied here.
* @dev Note that L1 staking contract only allows delegation transfer if the indexer has already transferred,
* this means the corresponding delegation pool exists.
* @param _tokens Amount of tokens that were transferred
* @param _delegationData struct containing the delegator's address and the indexer's address
*/
function _receiveDelegation(
uint256 _tokens,
IL2StakingTypes.ReceiveDelegationData memory _delegationData
) private {
require(_provisions[_delegationData.indexer][SUBGRAPH_DATA_SERVICE_ADDRESS].createdAt != 0, "!provision");
// Get the delegation pool of the indexer
DelegationPoolInternal storage pool = _legacyDelegationPools[_delegationData.indexer];
IHorizonStakingTypes.DelegationInternal storage delegation = pool.delegators[_delegationData.delegator];

// If pool is in an invalid state, return the tokens to the delegator
if (pool.tokens == 0 && (pool.shares != 0 || pool.sharesThawing != 0)) {
_graphToken().transfer(_delegationData.delegator, _tokens);
emit TransferredDelegationReturnedToDelegator(_delegationData.indexer, _delegationData.delegator, _tokens);
return;
}

// Calculate shares to issue (without applying any delegation tax)
uint256 shares = (pool.tokens == 0 || pool.tokens == pool.tokensThawing)
? _tokens
: ((_tokens * pool.shares) / (pool.tokens - pool.tokensThawing));

if (shares == 0 || _tokens < MINIMUM_DELEGATION) {
// If no shares would be issued (probably a rounding issue or attack),
// or if the amount is under the minimum delegation (which could be part of a rounding attack),
// return the tokens to the delegator
_graphToken().pushTokens(_delegationData.delegator, _tokens);
emit TransferredDelegationReturnedToDelegator(_delegationData.indexer, _delegationData.delegator, _tokens);
} else {
// Update the delegation pool
pool.tokens = pool.tokens + _tokens;
pool.shares = pool.shares + shares;

// Update the individual delegation
delegation.shares = delegation.shares + shares;

emit StakeDelegated(_delegationData.indexer, _delegationData.delegator, _tokens, shares);
}
}

/**
* @dev Collect tax to burn for an amount of tokens.
* @param _tokens Total tokens received used to calculate the amount of tax to collect
Expand Down
4 changes: 2 additions & 2 deletions packages/horizon/contracts/staking/HorizonStakingStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ abstract contract HorizonStakingV1Storage {
mapping(address serviceProvider => address rewardsDestination) internal __DEPRECATED_rewardsDestination;

/// @dev Address of the counterpart Staking contract on L1/L2
/// Used for the transfer tools.
address internal _counterpartStakingAddress;
/// 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.
Expand Down
6 changes: 1 addition & 5 deletions packages/horizon/test/GraphBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants {

address subgraphDataServiceLegacyAddress = makeAddr("subgraphDataServiceLegacyAddress");
address subgraphDataServiceAddress = makeAddr("subgraphDataServiceAddress");

// We use these addresses to mock calls from the counterpart staking contract

address graphTokenGatewayAddress = makeAddr("GraphTokenGateway");
address counterpartStaking = makeAddr("counterpartStaking");

/* Users */

Expand Down Expand Up @@ -188,8 +186,6 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants {
proxyAdmin.upgrade(stakingProxy, address(stakingBase));
proxyAdmin.acceptProxy(stakingBase, stakingProxy);
staking = IHorizonStaking(address(stakingProxy));

staking.setCounterpartStakingAddress(address(counterpartStaking));
}

function setupProtocol() private {
Expand Down
3 changes: 1 addition & 2 deletions packages/horizon/test/escrow/collect.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
uint256 tokensDelegatoion = tokens * delegationFeeCut / MAX_PPM;
vm.assume(tokensDataService < tokens - tokensProtocol - tokensDelegatoion);

vm.assume(delegationTokens > MIN_DELEGATION);
vm.assume(delegationTokens <= MAX_STAKING_TOKENS);
delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS);
resetPrank(users.delegator);
_delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0);

Expand Down
3 changes: 1 addition & 2 deletions packages/horizon/test/payments/GraphPayments.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ contract GraphPaymentsTest is HorizonStakingSharedTest {
address escrowAddress = address(escrow);

// Delegate tokens
vm.assume(tokensDelegate > MIN_DELEGATION);
vm.assume(tokensDelegate <= MAX_STAKING_TOKENS);
tokensDelegate = bound(tokensDelegate, 1, MAX_STAKING_TOKENS);
vm.startPrank(users.delegator);
_delegate(users.indexer, subgraphDataServiceAddress, tokensDelegate, 0);

Expand Down
Loading

0 comments on commit 4e41890

Please sign in to comment.