Skip to content

Commit

Permalink
fix: add legacy slasher for transition period (TRST-H06)
Browse files Browse the repository at this point in the history
  • Loading branch information
Maikol committed Dec 3, 2024
1 parent 167055a commit abe3321
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ interface IHorizonStakingExtension is IRewardsIssuer {
uint256 delegationRewards
);

/**
* @dev Emitted when `indexer` was slashed for a total of `tokens` amount.
* Tracks `reward` amount of tokens given to `beneficiary`.
*/
event StakeSlashed(address indexed indexer, uint256 tokens, uint256 reward, address beneficiary);

/**
* @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 @@ -148,4 +154,14 @@ interface IHorizonStakingExtension is IRewardsIssuer {
*/
// solhint-disable-next-line func-name-mixedcase
function __DEPRECATED_getThawingPeriod() external view returns (uint64);

/**
* @notice Slash the indexer stake. Delegated tokens are not subject to slashing.
* @dev Can only be called by the slasher role.
* @param indexer Address of indexer to slash
* @param tokens Amount of tokens to slash from the indexer stake
* @param reward Amount of reward tokens to send to a beneficiary
* @param beneficiary Address of a beneficiary to receive a reward for the slashing
*/
function legacySlash(address indexer, uint256 tokens, uint256 reward, address beneficiary) external;
}
18 changes: 18 additions & 0 deletions packages/horizon/contracts/staking/HorizonStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.27;

import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol";
import { IHorizonStakingMain } from "../interfaces/internal/IHorizonStakingMain.sol";
import { IHorizonStakingExtension } from "../interfaces/internal/IHorizonStakingExtension.sol";
import { IGraphPayments } from "../interfaces/IGraphPayments.sol";

import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol";
Expand Down Expand Up @@ -433,6 +434,23 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
uint256 tokensVerifier,
address verifierDestination
) external override notPaused {
// TODO remove after the transition period
// Check if sender is authorized to slash on the deprecated list
if (__DEPRECATED_slashers[msg.sender]) {
// Forward call to staking extension
(bool success, ) = STAKING_EXTENSION_ADDRESS.delegatecall(
abi.encodeWithSelector(
IHorizonStakingExtension.legacySlash.selector,
serviceProvider,
tokens,
tokensVerifier,
verifierDestination
)
);
require(success, "Delegatecall to legacySlash failed");
return;
}

address verifier = msg.sender;
Provision storage prov = _provisions[serviceProvider][verifier];
DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier);
Expand Down
57 changes: 57 additions & 0 deletions packages/horizon/contracts/staking/HorizonStakingExtension.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension
_;
}

/**
* @dev Check if the caller is the slasher.
*/
modifier onlySlasher() {
require(__DEPRECATED_slashers[msg.sender] == true, "!slasher");
_;
}

/**
* @dev The staking contract is upgradeable however we still use the constructor to set
* a few immutable variables.
Expand Down Expand Up @@ -255,6 +263,55 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension
return __DEPRECATED_thawingPeriod;
}

function legacySlash(
address indexer,
uint256 tokens,
uint256 reward,
address beneficiary
) external override onlySlasher notPaused {
ServiceProviderInternal storage indexerStake = _serviceProviders[indexer];

// Only able to slash a non-zero number of tokens
require(tokens > 0, "!tokens");

// Rewards comes from tokens slashed balance
require(tokens >= reward, "rewards>slash");

// Cannot slash stake of an indexer without any or enough stake
require(indexerStake.tokensStaked > 0, "!stake");
require(tokens <= indexerStake.tokensStaked, "slash>stake");

// Validate beneficiary of slashed tokens
require(beneficiary != address(0), "!beneficiary");

// Slashing more tokens than freely available (over allocation condition)
// Unlock locked tokens to avoid the indexer to withdraw them
uint256 tokensAvailable = indexerStake.tokensStaked -
indexerStake.__DEPRECATED_tokensAllocated -
indexerStake.__DEPRECATED_tokensLocked;
if (tokens > tokensAvailable && indexerStake.__DEPRECATED_tokensLocked > 0) {
uint256 tokensOverAllocated = tokens - tokensAvailable;
uint256 tokensToUnlock = MathUtils.min(tokensOverAllocated, indexerStake.__DEPRECATED_tokensLocked);
indexerStake.__DEPRECATED_tokensLocked = indexerStake.__DEPRECATED_tokensLocked - tokensToUnlock;
if (indexerStake.__DEPRECATED_tokensLocked == 0) {
indexerStake.__DEPRECATED_tokensLockedUntil = 0;
}
}

// Remove tokens to slash from the stake
indexerStake.tokensStaked = indexerStake.tokensStaked - tokens;

// -- Interactions --

// Set apart the reward for the beneficiary and burn remaining slashed stake
_graphToken().burnTokens(tokens - reward);

// Give the beneficiary a reward for slashing
_graphToken().pushTokens(beneficiary, reward);

emit StakeSlashed(indexer, tokens, reward, beneficiary);
}

/**
* @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
3 changes: 2 additions & 1 deletion packages/horizon/test/GraphBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants {
operator: createUser("operator"),
gateway: createUser("gateway"),
verifier: createUser("verifier"),
delegator: createUser("delegator")
delegator: createUser("delegator"),
legacySlasher: createUser("legacySlasher")
});

// Deploy protocol contracts
Expand Down
183 changes: 183 additions & 0 deletions packages/horizon/test/staking/slash/legacySlash.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import "forge-std/Test.sol";

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

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

contract HorizonStakingLegacySlashTest is HorizonStakingTest {

/*
* MODIFIERS
*/

modifier useLegacySlasher(address slasher) {
bytes32 storageKey = keccak256(abi.encode(slasher, 18));
vm.store(address(staking), storageKey, bytes32(uint256(1)));
_;
}

/*
* HELPERS
*/

function _setIndexer(
address _indexer,
uint256 _tokensStaked,
uint256 _tokensAllocated,
uint256 _tokensLocked,
uint256 _tokensLockedUntil
) public {
bytes32 baseSlot = keccak256(abi.encode(_indexer, 14));

vm.store(address(staking), bytes32(uint256(baseSlot)), bytes32(_tokensStaked));
vm.store(address(staking), bytes32(uint256(baseSlot) + 1), bytes32(_tokensAllocated));
vm.store(address(staking), bytes32(uint256(baseSlot) + 2), bytes32(_tokensLocked));
vm.store(address(staking), bytes32(uint256(baseSlot) + 3), bytes32(_tokensLockedUntil));
}

/*
* ACTIONS
*/

function _legacySlash(address _indexer, uint256 _tokens, uint256 _rewards, address _beneficiary) internal {
// before
uint256 beforeStakingBalance = token.balanceOf(address(staking));
uint256 beforeRewardsDestinationBalance = token.balanceOf(_beneficiary);

// slash
vm.expectEmit(address(staking));
emit IHorizonStakingExtension.StakeSlashed(_indexer, _tokens, _rewards, _beneficiary);
staking.slash(_indexer, _tokens, _rewards, _beneficiary);

// after
uint256 afterStakingBalance = token.balanceOf(address(staking));
uint256 afterRewardsDestinationBalance = token.balanceOf(_beneficiary);

assertEq(beforeStakingBalance - _tokens, afterStakingBalance);
assertEq(beforeRewardsDestinationBalance, afterRewardsDestinationBalance - _rewards);
}

/*
* TESTS
*/

function testSlash_Legacy(
uint256 tokens,
uint256 slashTokens,
uint256 reward
) public useIndexer useLegacySlasher(users.legacySlasher) {
vm.assume(tokens > 1);
slashTokens = bound(slashTokens, 1, tokens);
reward = bound(reward, 0, slashTokens);

_createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0);

resetPrank(users.legacySlasher);
_legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman"));
}

function testSlash_Legacy_UsingLockedTokens(
uint256 tokens,
uint256 slashTokens,
uint256 reward
) public useIndexer useLegacySlasher(users.legacySlasher) {
vm.assume(tokens > 1);
slashTokens = bound(slashTokens, 1, tokens);
reward = bound(reward, 0, slashTokens);

_setIndexer(users.indexer, tokens, 0, tokens, block.timestamp + 1);
// Send tokens manually to staking
token.transfer(address(staking), tokens);

resetPrank(users.legacySlasher);
_legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman"));
}

function testSlash_Legacy_UsingAllocatedTokens(
uint256 tokens,
uint256 slashTokens,
uint256 reward
) public useIndexer useLegacySlasher(users.legacySlasher) {
vm.assume(tokens > 1);
slashTokens = bound(slashTokens, 1, tokens);
reward = bound(reward, 0, slashTokens);

_setIndexer(users.indexer, tokens, 0, tokens, 0);
// Send tokens manually to staking
token.transfer(address(staking), tokens);

resetPrank(users.legacySlasher);
staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman"));
}

function testSlash_Legacy_RevertWhen_CallerNotSlasher(
uint256 tokens,
uint256 slashTokens,
uint256 reward
) public useIndexer {
vm.assume(tokens > 0);
_createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0);

vm.expectRevert("!slasher");
staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman"));
}

function testSlash_Legacy_RevertWhen_RewardsOverSlashTokens(
uint256 tokens,
uint256 slashTokens,
uint256 reward
) public useIndexer useLegacySlasher(users.legacySlasher) {
vm.assume(tokens > 0);
vm.assume(slashTokens > 0);
vm.assume(reward > slashTokens);

_createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0);

resetPrank(users.legacySlasher);
vm.expectRevert("rewards>slash");
staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman"));
}

function testSlash_Legacy_RevertWhen_NoStake(
uint256 slashTokens,
uint256 reward
) public useLegacySlasher(users.legacySlasher) {
vm.assume(slashTokens > 0);
reward = bound(reward, 0, slashTokens);

resetPrank(users.legacySlasher);
vm.expectRevert("!stake");
staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman"));
}

function testSlash_Legacy_RevertWhen_ZeroTokens(
uint256 tokens
) public useIndexer useLegacySlasher(users.legacySlasher) {
vm.assume(tokens > 0);

_createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0);

resetPrank(users.legacySlasher);
vm.expectRevert("!tokens");
staking.legacySlash(users.indexer, 0, 0, makeAddr("fisherman"));
}

function testSlash_Legacy_RevertWhen_NoBeneficiary(
uint256 tokens,
uint256 slashTokens,
uint256 reward
) public useIndexer useLegacySlasher(users.legacySlasher) {
vm.assume(tokens > 0);
slashTokens = bound(slashTokens, 1, tokens);
reward = bound(reward, 0, slashTokens);

_createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0);

resetPrank(users.legacySlasher);
vm.expectRevert("!beneficiary");
staking.legacySlash(users.indexer, slashTokens, reward, address(0));
}
}
1 change: 1 addition & 0 deletions packages/horizon/test/utils/Users.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ struct Users {
address gateway;
address verifier;
address delegator;
address legacySlasher;
}

0 comments on commit abe3321

Please sign in to comment.