Skip to content

Commit

Permalink
fix: add missing legacy withdraw delegated (TRST-H07)
Browse files Browse the repository at this point in the history
  • Loading branch information
Maikol committed Dec 4, 2024
1 parent abe3321 commit f254897
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ interface IHorizonStakingExtension is IRewardsIssuer {
*/
event StakeSlashed(address indexed indexer, uint256 tokens, uint256 reward, address beneficiary);

/**
* @dev Emitted when `delegator` withdrew delegated `tokens` from `indexer` using `legacyWithdrawDelegated`.
*/
event StakeDelegatedWithdrawn(address indexed indexer, address indexed delegator, uint256 tokens);

/**
* @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 @@ -164,4 +169,13 @@ interface IHorizonStakingExtension is IRewardsIssuer {
* @param beneficiary Address of a beneficiary to receive a reward for the slashing
*/
function legacySlash(address indexer, uint256 tokens, uint256 reward, address beneficiary) external;

/**
* @notice Withdraw undelegated tokens once the unbonding period has passed.
* @param _indexer Withdraw available tokens delegated to indexer
*/
function legacyWithdrawDelegated(
address _indexer,
address /* _newIndexer, deprecated */
) external returns (uint256);
}
37 changes: 37 additions & 0 deletions packages/horizon/contracts/staking/HorizonStakingExtension.sol
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,43 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension
emit StakeSlashed(indexer, tokens, reward, beneficiary);
}

/**
* @notice Withdraw undelegated tokens once the unbonding period has passed.
* @param indexer Withdraw available tokens delegated to indexer
*/
function legacyWithdrawDelegated(
address indexer,
address // newIndexer, deprecated
) external override notPaused returns (uint256) {
// Get the delegation pool of the indexer
address delegator = msg.sender;
DelegationPoolInternal storage pool = _legacyDelegationPools[indexer];
DelegationInternal storage delegation = pool.delegators[delegator];

// Validation
uint256 tokensToWithdraw = 0;
uint256 currentEpoch = _graphEpochManager().currentEpoch();
if (
delegation.__DEPRECATED_tokensLockedUntil > 0 && currentEpoch >= delegation.__DEPRECATED_tokensLockedUntil
) {
tokensToWithdraw = delegation.__DEPRECATED_tokensLocked;
}
require(tokensToWithdraw > 0, "!tokens");

// Reset lock
delegation.__DEPRECATED_tokensLocked = 0;
delegation.__DEPRECATED_tokensLockedUntil = 0;

emit StakeDelegatedWithdrawn(indexer, delegator, tokensToWithdraw);

// -- Interactions --

// Return tokens to the delegator
_graphToken().pushTokens(delegator, tokensToWithdraw);

return tokensToWithdraw;
}

/**
* @notice (Legacy) Return true if operator is allowed for the service provider on the subgraph data service.
* @dev TODO: Delete after the transition period
Expand Down
98 changes: 98 additions & 0 deletions packages/horizon/test/staking/delegation/legacyWithdraw.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import "forge-std/Test.sol";

import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol";
import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol";
import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol";
import { LinkedList } from "../../../contracts/libraries/LinkedList.sol";

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

contract HorizonStakingLegacyWithdrawDelegationTest is HorizonStakingTest {
/*
* MODIFIERS
*/

modifier useDelegator() {
resetPrank(users.delegator);
_;
}

/*
* HELPERS
*/

function _setLegacyDelegation(
address _indexer,
address _delegator,
uint256 _shares,
uint256 __DEPRECATED_tokensLocked,
uint256 __DEPRECATED_tokensLockedUntil
) public {
// Calculate the base storage slot for the serviceProvider in the mapping
bytes32 baseSlot = keccak256(abi.encode(_indexer, uint256(20)));

// Calculate the slot for the delegator's DelegationInternal struct
bytes32 delegatorSlot = keccak256(abi.encode(_delegator, bytes32(uint256(baseSlot) + 4)));

// Use vm.store to set each field of the struct
vm.store(address(staking), bytes32(uint256(delegatorSlot)), bytes32(_shares));
vm.store(address(staking), bytes32(uint256(delegatorSlot) + 1), bytes32(__DEPRECATED_tokensLocked));
vm.store(address(staking), bytes32(uint256(delegatorSlot) + 2), bytes32(__DEPRECATED_tokensLockedUntil));
}

/*
* ACTIONS
*/

function _legacyWithdrawDelegated(address _indexer) internal {
(, address delegator, ) = vm.readCallers();
IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool(_indexer, subgraphDataServiceLegacyAddress);
uint256 beforeStakingBalance = token.balanceOf(address(staking));
uint256 beforeDelegatorBalance = token.balanceOf(users.delegator);

vm.expectEmit(address(staking));
emit IHorizonStakingExtension.StakeDelegatedWithdrawn(_indexer, delegator, pool.tokens);
staking.legacyWithdrawDelegated(users.indexer, address(0));

uint256 afterStakingBalance = token.balanceOf(address(staking));
uint256 afterDelegatorBalance = token.balanceOf(users.delegator);

assertEq(afterStakingBalance, beforeStakingBalance - pool.tokens);
assertEq(afterDelegatorBalance - pool.tokens, beforeDelegatorBalance);

DelegationInternal memory delegation = _getStorage_Delegation(
_indexer,
subgraphDataServiceLegacyAddress,
delegator,
true
);
assertEq(delegation.shares, 0);
assertEq(delegation.__DEPRECATED_tokensLocked, 0);
assertEq(delegation.__DEPRECATED_tokensLockedUntil, 0);
}

/*
* TESTS
*/

function testWithdraw_Legacy(uint256 tokensLocked) public useDelegator {
vm.assume(tokensLocked > 0);

_setStorage_DelegationPool(users.indexer, tokensLocked, 0, 0);
_setLegacyDelegation(users.indexer, users.delegator, 0, tokensLocked, 1);
token.transfer(address(staking), tokensLocked);

_legacyWithdrawDelegated(users.indexer);
}

function testWithdraw_Legacy_RevertWhen_NoTokens() public useDelegator {
_setStorage_DelegationPool(users.indexer, 0, 0, 0);
_setLegacyDelegation(users.indexer, users.delegator, 0, 0, 0);

vm.expectRevert("!tokens");
staking.legacyWithdrawDelegated(users.indexer, address(0));
}
}

0 comments on commit f254897

Please sign in to comment.