From 4994dfd41339f19d608a5ebbae34a3ea91b65426 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Thu, 17 Aug 2023 03:35:04 +0530 Subject: [PATCH] feat: rework slash implementation on CustomSupernetManager --- .../root/staking/CustomSupernetManager.sol | 32 +++++-- docs/root/staking/CustomSupernetManager.md | 17 ++++ .../root/staking/CustomSupernetManager.t.sol | 88 +++++++++++++++++-- 3 files changed, 123 insertions(+), 14 deletions(-) diff --git a/contracts/root/staking/CustomSupernetManager.sol b/contracts/root/staking/CustomSupernetManager.sol index 195bb14a..5d6a9821 100644 --- a/contracts/root/staking/CustomSupernetManager.sol +++ b/contracts/root/staking/CustomSupernetManager.sol @@ -17,6 +17,7 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl bytes32 private constant UNSTAKE_SIG = keccak256("UNSTAKE"); bytes32 private constant SLASH_SIG = keccak256("SLASH"); uint256 public constant SLASHING_PERCENTAGE = 50; + uint256 public constant SLASH_INCENTIVE_PERCENTAGE = 30; IBLS private bls; IStateSender private stateSender; @@ -121,8 +122,8 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl (address validator, uint256 amount) = abi.decode(data[32:], (address, uint256)); _unstake(validator, amount); } else if (bytes32(data[:32]) == SLASH_SIG) { - address validator = abi.decode(data[32:], (address)); - _slash(validator); + (, address[] memory validatorsToSlash) = abi.decode(data, (bytes32, address[])); + _slash(id, validatorsToSlash); } } @@ -157,12 +158,27 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl _removeIfValidatorUnstaked(validator); } - function _slash(address validator) internal { - uint256 stake = stakeManager.stakeOf(validator, id); - uint256 slashedAmount = (stake * SLASHING_PERCENTAGE) / 100; - // slither-disable-next-line reentrancy-benign,reentrancy-events - stakeManager.slashStakeOf(validator, slashedAmount); - _removeIfValidatorUnstaked(validator); + function _slash(uint256 exitEventId, address[] memory validatorsToSlash) internal { + uint256 length = validatorsToSlash.length; + uint256 totalSlashedAmount; + for (uint256 i = 0; i < length; ) { + uint256 slashedAmount = (stakeManager.stakeOf(validatorsToSlash[i], id) * SLASHING_PERCENTAGE) / 100; + // slither-disable-next-line reentrancy-benign,reentrancy-events + stakeManager.slashStakeOf(validatorsToSlash[i], slashedAmount); + _removeIfValidatorUnstaked(validatorsToSlash[i]); + totalSlashedAmount += slashedAmount; + unchecked { + ++i; + } + } + + // contract will always have enough balance since slashStakeOf returns entire slashed amt + uint256 rewardAmount = (totalSlashedAmount * SLASH_INCENTIVE_PERCENTAGE) / 100; + // solhint-disable avoid-tx-origin + matic.safeTransfer(tx.origin, rewardAmount); + + // complete slashing on child chain + stateSender.syncState(childValidatorSet, abi.encode(SLASH_SIG, exitEventId, validatorsToSlash)); } function _verifyValidatorRegistration( diff --git a/docs/root/staking/CustomSupernetManager.md b/docs/root/staking/CustomSupernetManager.md index 4e446511..70136a9d 100644 --- a/docs/root/staking/CustomSupernetManager.md +++ b/docs/root/staking/CustomSupernetManager.md @@ -21,6 +21,23 @@ function SLASHING_PERCENTAGE() external view returns (uint256) +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### SLASH_INCENTIVE_PERCENTAGE + +```solidity +function SLASH_INCENTIVE_PERCENTAGE() external view returns (uint256) +``` + + + + + + #### Returns | Name | Type | Description | diff --git a/test/forge/root/staking/CustomSupernetManager.t.sol b/test/forge/root/staking/CustomSupernetManager.t.sol index b58268a4..dd8e92ae 100644 --- a/test/forge/root/staking/CustomSupernetManager.t.sol +++ b/test/forge/root/staking/CustomSupernetManager.t.sol @@ -147,7 +147,9 @@ abstract contract Slashed is EnabledStaking { function setUp() public virtual override { super.setUp(); - bytes memory callData = abi.encode(SLASH_SIG, address(this)); + address[] memory validatorsToSlash = new address[](1); + validatorsToSlash[0] = address(this); + bytes memory callData = abi.encode(SLASH_SIG, validatorsToSlash); vm.prank(exitHelper); supernetManager.onL2StateReceive(1, childValidatorSet, callData); } @@ -376,16 +378,35 @@ contract CustomSupernetManager_Unstake is EnabledStaking { } contract CustomSupernetManager_Slash is EnabledStaking { + address private mev = makeAddr("MEV"); bytes32 private constant SLASH_SIG = keccak256("SLASH"); event ValidatorDeactivated(address indexed validator); + event StateSynced(uint256 indexed id, address indexed sender, address indexed receiver, bytes data); + event StakeRemoved(uint256 indexed id, address indexed validator, uint256 amount); + event StakeWithdrawn(address indexed validator, address indexed recipient, uint256 amount); + event Transfer(address indexed from, address indexed to, uint256 value); function test_SuccessfulFullWithdrawal() public { + uint256 exitEventId = 1; uint256 slashingPercentage = supernetManager.SLASHING_PERCENTAGE(); - bytes memory callData = abi.encode(SLASH_SIG, address(this)); + + address[] memory validatorsToSlash = new address[](1); + validatorsToSlash[0] = address(this); + bytes memory callData = abi.encode(SLASH_SIG, validatorsToSlash); + uint256 slashedAmount = (amount * slashingPercentage) / 100; + + vm.expectEmit(true, true, true, true); + emit StakeWithdrawn(address(this), address(supernetManager), slashedAmount); + vm.expectEmit(true, true, true, true); + emit StakeRemoved(exitEventId, address(this), amount); vm.expectEmit(true, true, true, true); emit ValidatorDeactivated(address(this)); + // emits state sync event to complete slashing on child chain + vm.expectEmit(true, true, true, true); + emit StateSynced(exitEventId, address(supernetManager), childValidatorSet, abi.encode(SLASH_SIG, exitEventId, validatorsToSlash)); vm.prank(exitHelper); - supernetManager.onL2StateReceive(1, childValidatorSet, callData); + supernetManager.onL2StateReceive(exitEventId, childValidatorSet, callData); + assertEq(stakeManager.stakeOf(address(this), 1), 0, "should unstake all"); assertEq( stakeManager.withdrawableStake(address(this)), @@ -394,6 +415,59 @@ contract CustomSupernetManager_Slash is EnabledStaking { ); assertEq(supernetManager.getValidator(address(this)).isActive, false, "should deactivate"); } + + + function test_SlashIncentiveDistribution() external { + uint256 exitEventId = 1; + address[] memory validatorsToSlash = new address[](1); + validatorsToSlash[0] = address(this); + bytes memory callData = abi.encode(SLASH_SIG, validatorsToSlash); + uint256 slashedAmount = (amount * supernetManager.SLASHING_PERCENTAGE()) / 100; + uint256 exitorReward = (slashedAmount * supernetManager.SLASH_INCENTIVE_PERCENTAGE()) / 100; + + assertEq(token.balanceOf(mev), 0); // balance before + vm.expectEmit(true, true, true, true); + emit Transfer(address(supernetManager), mev, exitorReward); + vm.prank(exitHelper, mev /* tx.origin */); + supernetManager.onL2StateReceive(exitEventId, childValidatorSet, callData); + assertEq(token.balanceOf(mev), exitorReward, "should transfer slashing reward"); + } + + function test_SlashEntireValidatorSet() external { + uint256 slashingPercentage = supernetManager.SLASHING_PERCENTAGE(); + uint256 aliceStakedAmount = amount << 3; + token.mint(alice, aliceStakedAmount); + vm.prank(alice); + stakeManager.stakeFor(1, aliceStakedAmount); + + uint256 thisSlashedAmount = (amount * slashingPercentage) / 100; + uint256 aliceSlashedAmount = (aliceStakedAmount * slashingPercentage) / 100; + + address[] memory validatorsToSlash = new address[](2); + validatorsToSlash[0] = alice; + validatorsToSlash[1] = address(this); + bytes memory callData = abi.encode(SLASH_SIG, validatorsToSlash); + + vm.prank(exitHelper, mev /* tx.origin */); + supernetManager.onL2StateReceive(1, childValidatorSet, callData); + + assertEq(stakeManager.stakeOf(address(this), 1), 0, "should unstake all"); + assertEq(stakeManager.stakeOf(alice, 1), 0, "should unstake all"); + assertEq( + stakeManager.withdrawableStake(address(this)), + amount - thisSlashedAmount, + "should slash" + ); + assertEq( + stakeManager.withdrawableStake(alice), + aliceStakedAmount - aliceSlashedAmount, + "should slash" + ); + assertEq(supernetManager.getValidator(address(this)).isActive, false, "should deactivate"); + assertEq(supernetManager.getValidator(alice).isActive, false, "should deactivate"); + uint256 exitorReward = ((thisSlashedAmount + aliceSlashedAmount) * supernetManager.SLASH_INCENTIVE_PERCENTAGE()) / 100; + assertEq(token.balanceOf(mev), exitorReward, "should transfer slashing reward"); + } } contract CustomSupernetManager_WithdrawSlash is Slashed { @@ -406,10 +480,12 @@ contract CustomSupernetManager_WithdrawSlash is Slashed { } function test_WithdrawSlashedAmount() public { - uint256 slashedAmount = amount / 2; - assertEq(token.balanceOf(address(supernetManager)), slashedAmount); + uint256 slashedAmount = (amount * supernetManager.SLASHING_PERCENTAGE()) / 100; + uint256 slashingReward = (slashedAmount * supernetManager.SLASH_INCENTIVE_PERCENTAGE()) / 100; // given to exitor after slash + uint256 withdrawableAmount = slashedAmount - slashingReward; + assertEq(token.balanceOf(address(supernetManager)), withdrawableAmount); vm.expectEmit(true, true, true, true); - emit Transfer(address(supernetManager), alice, slashedAmount); + emit Transfer(address(supernetManager), alice, withdrawableAmount); supernetManager.withdrawSlashedStake(alice); assertEq(token.balanceOf(address(supernetManager)), 0); }