Skip to content

Commit

Permalink
chore(Horizon): add redelegate option
Browse files Browse the repository at this point in the history
  • Loading branch information
Maikol committed Oct 4, 2024
1 parent f88f335 commit 6a0e8b0
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,16 @@ interface IHorizonStakingMain {
*/
error HorizonStakingInvalidBeneficiaryZeroAddress();

/**
* @notice Thrown when attempting to redelegate with a serivce provider that is the zero address.
*/
error HorizonStakingInvalidServiceProviderZeroAddress();

/**
* @notice Thrown when attempting to redelegate with a verifier that is the zero address.
*/
error HorizonStakingInvalidVerifierZeroAddress();

// -- Errors: thaw requests --

error HorizonStakingNothingThawing();
Expand Down Expand Up @@ -739,26 +749,44 @@ interface IHorizonStakingMain {

/**
* @notice Withdraw undelegated tokens from a provision after thawing.
* Tokens can be automatically re-delegated to another provision by setting `newServiceProvider`.
* @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
* requests in the event that fulfilling all of them results in a gas limit error.
*
* Requirements:
* - Must have previously initiated a thaw request using {undelegate}.
* - `newServiceProvider` must either be zero address or have previously provisioned stake to `verifier`.
*
* Emits {ThawRequestFulfilled}, {ThawRequestsFulfilled} and {DelegatedTokensWithdrawn} events.
*
* @param serviceProvider The service provider address
* @param verifier The verifier address
* @param newServiceProvider The address of a new service provider, if the delegator wants to re-delegate
* @param nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests.
*/
function withdrawDelegated(address serviceProvider, address verifier, uint256 nThawRequests) external;

/**
* @notice Re-delegate undelegated tokens from a provision after thawing to a `newServiceProvider` and `newVerifier`.
* @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
* requests in the event that fulfilling all of them results in a gas limit error.
*
* Requirements:
* - Must have previously initiated a thaw request using {undelegate}.
* - `newServiceProvider` and `newVerifier` must not be the zero address.
* - `newServiceProvider` must have previously provisioned stake to `newVerifier`.
*
* Emits {ThawRequestFulfilled}, {ThawRequestsFulfilled} and {DelegatedTokensWithdrawn} events.
*
* @param oldServiceProvider The old service provider address
* @param oldVerifier The old verifier address
* @param newServiceProvider The address of a new service provider
* @param newVerifier The address of a new verifier
* @param minSharesForNewProvider The minimum amount of shares to accept for the new service provider
* @param nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests.
*/
function withdrawDelegated(
address serviceProvider,
address verifier,
function redelegate(
address oldServiceProvider,
address oldVerifier,
address newServiceProvider,
address newVerifier,
uint256 minSharesForNewProvider,
uint256 nThawRequests
) external;
Expand Down
37 changes: 33 additions & 4 deletions packages/horizon/contracts/staking/HorizonStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,32 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
function withdrawDelegated(
address serviceProvider,
address verifier,
uint256 nThawRequests
) external override notPaused {
_withdrawDelegated(serviceProvider, verifier, address(0), address(0), 0, nThawRequests);
}

/**
* @notice See {IHorizonStakingMain-redelegate}.
*/
function redelegate(
address oldServiceProvider,
address oldVerifier,
address newServiceProvider,
address newVerifier,
uint256 minSharesForNewProvider,
uint256 nThawRequests
) external override notPaused {
_withdrawDelegated(serviceProvider, verifier, newServiceProvider, minSharesForNewProvider, nThawRequests);
require(newServiceProvider != address(0), HorizonStakingInvalidServiceProviderZeroAddress());
require(newVerifier != address(0), HorizonStakingInvalidVerifierZeroAddress());
_withdrawDelegated(
oldServiceProvider,
oldVerifier,
newServiceProvider,
newVerifier,
minSharesForNewProvider,
nThawRequests
);
}

/**
Expand Down Expand Up @@ -360,7 +381,14 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
* @notice See {IHorizonStakingMain-withdrawDelegated}.
*/
function withdrawDelegated(address serviceProvider, address newServiceProvider) external override notPaused {
_withdrawDelegated(serviceProvider, SUBGRAPH_DATA_SERVICE_ADDRESS, newServiceProvider, 0, 0);
_withdrawDelegated(
serviceProvider,
SUBGRAPH_DATA_SERVICE_ADDRESS,
newServiceProvider,
SUBGRAPH_DATA_SERVICE_ADDRESS,
0,
0
);
}

/*
Expand Down Expand Up @@ -805,6 +833,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
address _serviceProvider,
address _verifier,
address _newServiceProvider,
address _newVerifier,
uint256 _minSharesForNewProvider,
uint256 _nThawRequests
) private {
Expand All @@ -828,8 +857,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
pool.tokensThawing = tokensThawing;

if (tokensThawed != 0) {
if (_newServiceProvider != address(0)) {
_delegate(_newServiceProvider, _verifier, tokensThawed, _minSharesForNewProvider);
if (_newServiceProvider != address(0) && _newVerifier != address(0)) {
_delegate(_newServiceProvider, _newVerifier, tokensThawed, _minSharesForNewProvider);
} else {
_graphToken().pushTokens(msg.sender, tokensThawed);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -951,24 +951,42 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
}

function _withdrawDelegated(
address serviceProvider,
address verifier,
uint256 nThawRequests
) internal {
__withdrawDelegated(
serviceProvider,
verifier,
address(0),
address(0),
0,
nThawRequests,
false
);
}

function _redelegate(
address serviceProvider,
address verifier,
address newServiceProvider,
address newVerifier,
uint256 minSharesForNewProvider,
uint256 nThawRequests
) internal {
__withdrawDelegated(
serviceProvider,
verifier,
newServiceProvider,
newVerifier,
minSharesForNewProvider,
nThawRequests,
false
);
}

function _withdrawDelegated(address serviceProvider, address newServiceProvider) internal {
__withdrawDelegated(serviceProvider, subgraphDataServiceLegacyAddress, newServiceProvider, 0, 0, true);
__withdrawDelegated(serviceProvider, subgraphDataServiceLegacyAddress, newServiceProvider, subgraphDataServiceLegacyAddress, 0, 0, true);
}

struct BeforeValues_WithdrawDelegated {
Expand All @@ -991,19 +1009,20 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
address _serviceProvider,
address _verifier,
address _newServiceProvider,
address _newVerifier,
uint256 _minSharesForNewProvider,
uint256 _nThawRequests,
bool legacy
) private {
(, address msgSender, ) = vm.readCallers();

bool reDelegate = _newServiceProvider != address(0);
bool reDelegate = _newServiceProvider != address(0) && _newVerifier != address(0);

// before
BeforeValues_WithdrawDelegated memory beforeValues;
beforeValues.pool = _getStorage_DelegationPoolInternal(_serviceProvider, _verifier, legacy);
beforeValues.newPool = _getStorage_DelegationPoolInternal(_newServiceProvider, _verifier, legacy);
beforeValues.newDelegation = _getStorage_Delegation(_serviceProvider, _verifier, msgSender, legacy);
beforeValues.newPool = _getStorage_DelegationPoolInternal(_newServiceProvider, _newVerifier, legacy);
beforeValues.newDelegation = _getStorage_Delegation(_newServiceProvider, _newVerifier, msgSender, legacy);
beforeValues.thawRequestList = staking.getThawRequestList(_serviceProvider, _verifier, msgSender);
beforeValues.senderBalance = token.balanceOf(msgSender);
beforeValues.stakingBalance = token.balanceOf(address(staking));
Expand Down Expand Up @@ -1038,26 +1057,33 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
if (calcValues.tokensThawed != 0) {
vm.expectEmit();
if (reDelegate) {
emit IHorizonStakingMain.TokensDelegated(_newServiceProvider, _verifier, msgSender, calcValues.tokensThawed);
emit IHorizonStakingMain.TokensDelegated(_newServiceProvider, _newVerifier, msgSender, calcValues.tokensThawed);
} else {
emit Transfer(address(staking), msgSender, calcValues.tokensThawed);
}
}
vm.expectEmit();
emit IHorizonStakingMain.DelegatedTokensWithdrawn(_serviceProvider, _verifier, msgSender, calcValues.tokensThawed);
staking.withdrawDelegated(
_serviceProvider,
_verifier,
_newServiceProvider,
_minSharesForNewProvider,
_nThawRequests
);
if (legacy) {
staking.withdrawDelegated(_serviceProvider, _newServiceProvider);
} else if (reDelegate) {
staking.redelegate(
_serviceProvider,
_verifier,
_newServiceProvider,
_newVerifier,
_minSharesForNewProvider,
_nThawRequests
);
} else {
staking.withdrawDelegated(_serviceProvider, _verifier, _nThawRequests);
}

// after
AfterValues_WithdrawDelegated memory afterValues;
afterValues.pool = _getStorage_DelegationPoolInternal(_serviceProvider, _verifier, legacy);
afterValues.newPool = _getStorage_DelegationPoolInternal(_newServiceProvider, _verifier, legacy);
afterValues.newDelegation = _getStorage_Delegation(_newServiceProvider, _verifier, msgSender, legacy);
afterValues.newPool = _getStorage_DelegationPoolInternal(_newServiceProvider, _newVerifier, legacy);
afterValues.newDelegation = _getStorage_Delegation(_newServiceProvider, _newVerifier, msgSender, legacy);
afterValues.thawRequestList = staking.getThawRequestList(_serviceProvider, _verifier, msgSender);
afterValues.senderBalance = token.balanceOf(msgSender);
afterValues.stakingBalance = token.balanceOf(address(staking));
Expand Down
146 changes: 146 additions & 0 deletions packages/horizon/test/staking/delegation/redelegate.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import "forge-std/Test.sol";

import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol";

import { HorizonStakingTest } from "../HorizonStaking.t.sol";

contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest {

/*
* MODIFIERS
*/

modifier useUndelegate(uint256 shares) {
resetPrank(users.delegator);
DelegationInternal memory delegation = _getStorage_Delegation(users.indexer, subgraphDataServiceAddress, users.delegator, false);
shares = bound(shares, 1, delegation.shares);

_undelegate(users.indexer, subgraphDataServiceAddress, shares);
_;
}

/*
* HELPERS
*/

function _setupNewIndexer(uint256 tokens) private returns(address) {
(, address msgSender,) = vm.readCallers();

address newIndexer = createUser("newIndexer");
vm.startPrank(newIndexer);
_createProvision(newIndexer, subgraphDataServiceAddress, tokens, 0, MAX_THAWING_PERIOD);

vm.startPrank(msgSender);
return newIndexer;
}

function _setupNewIndexerAndVerifier(uint256 tokens) private returns(address, address) {
(, address msgSender,) = vm.readCallers();

address newIndexer = createUser("newIndexer");
address newVerifier = makeAddr("newVerifier");
vm.startPrank(newIndexer);
_createProvision(newIndexer, newVerifier, tokens, 0, MAX_THAWING_PERIOD);

vm.startPrank(msgSender);
return (newIndexer, newVerifier);
}

/*
* TESTS
*/

function testRedelegate_MoveToNewServiceProvider(
uint256 delegationAmount,
uint256 withdrawShares
)
public
useIndexer
useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD)
useDelegation(delegationAmount)
useUndelegate(withdrawShares)
{
skip(MAX_THAWING_PERIOD + 1);

// Setup new service provider
address newIndexer = _setupNewIndexer(10_000_000 ether);
_redelegate(users.indexer, subgraphDataServiceAddress, newIndexer, subgraphDataServiceAddress, 0, 0);
}

function testRedelegate_MoveToNewServiceProviderAndNewVerifier(
uint256 delegationAmount,
uint256 withdrawShares
)
public
useIndexer
useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD)
useDelegation(delegationAmount)
useUndelegate(withdrawShares)
{
skip(MAX_THAWING_PERIOD + 1);

// Setup new service provider
(address newIndexer, address newVerifier) = _setupNewIndexerAndVerifier(10_000_000 ether);
_redelegate(users.indexer, subgraphDataServiceAddress, newIndexer, newVerifier, 0, 0);
}

function testRedelegate_RevertWhen_VerifierZeroAddress(
uint256 delegationAmount
)
public
useIndexer
useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD)
useDelegation(delegationAmount)
useUndelegate(delegationAmount)
{
skip(MAX_THAWING_PERIOD + 1);

// Setup new service provider
address newIndexer = _setupNewIndexer(10_000_000 ether);
vm.expectRevert(abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidVerifierZeroAddress.selector));
staking.redelegate(users.indexer, subgraphDataServiceAddress, newIndexer, address(0), 0, 0);
}

function testRedelegate_RevertWhen_ServiceProviderZeroAddress(
uint256 delegationAmount
)
public
useIndexer
useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD)
useDelegation(delegationAmount)
useUndelegate(delegationAmount)
{
skip(MAX_THAWING_PERIOD + 1);

// Setup new verifier
address newVerifier = makeAddr("newVerifier");
vm.expectRevert(abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidServiceProviderZeroAddress.selector));
staking.redelegate(users.indexer, subgraphDataServiceAddress, address(0), newVerifier, 0, 0);
}

function testRedelegate_MoveZeroTokensToNewServiceProviderAndVerifier(
uint256 delegationAmount,
uint256 withdrawShares
)
public
useIndexer
useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD)
useDelegation(delegationAmount)
useUndelegate(withdrawShares)
{
// Setup new service provider
(address newIndexer, address newVerifier) = _setupNewIndexerAndVerifier(10_000_000 ether);

uint256 previousBalance = token.balanceOf(users.delegator);
_redelegate(users.indexer, subgraphDataServiceAddress, newIndexer, newVerifier, 0, 0);

uint256 newBalance = token.balanceOf(users.delegator);
assertEq(newBalance, previousBalance);

uint256 delegatedTokens = staking.getDelegatedTokensAvailable(newIndexer, newVerifier);
assertEq(delegatedTokens, 0);
}
}
Loading

0 comments on commit 6a0e8b0

Please sign in to comment.