diff --git a/contracts/child/validator/ValidatorSet.sol b/contracts/child/validator/ValidatorSet.sol index 771372e3..f53be05e 100644 --- a/contracts/child/validator/ValidatorSet.sol +++ b/contracts/child/validator/ValidatorSet.sol @@ -14,6 +14,8 @@ contract ValidatorSet is IValidatorSet, ERC20VotesUpgradeable, System { bytes32 private constant STAKE_SIG = keccak256("STAKE"); bytes32 private constant UNSTAKE_SIG = keccak256("UNSTAKE"); bytes32 private constant SLASH_SIG = keccak256("SLASH"); + uint256 public constant SLASHING_PERCENTAGE = 50; // to be read through NetworkParams later + uint256 public constant SLASH_INCENTIVE_PERCENTAGE = 30; // exitor reward, to be read through NetworkParams later IStateSender private stateSender; address private stateReceiver; @@ -25,6 +27,7 @@ contract ValidatorSet is IValidatorSet, ERC20VotesUpgradeable, System { mapping(uint256 => uint256) private _commitBlockNumbers; mapping(address => WithdrawalQueue) private withdrawals; mapping(uint256 => Epoch) public epochs; + mapping(uint256 => bool) public slashProcessed; uint256[] public epochEndBlocks; function initialize( @@ -68,11 +71,27 @@ contract ValidatorSet is IValidatorSet, ERC20VotesUpgradeable, System { emit NewEpoch(id, epoch.startBlock, epoch.endBlock, epoch.epochRoot); } + /** + * @inheritdoc IValidatorSet + */ + function slash(address[] calldata validators) external onlySystemCall { + stateSender.syncState( + rootChainManager, + abi.encode(SLASH_SIG, validators, SLASHING_PERCENTAGE, SLASH_INCENTIVE_PERCENTAGE) + ); + } + function onStateReceive(uint256 /*counter*/, address sender, bytes calldata data) external override { require(msg.sender == stateReceiver && sender == rootChainManager, "INVALID_SENDER"); if (bytes32(data[:32]) == STAKE_SIG) { (address validator, uint256 amount) = abi.decode(data[32:], (address, uint256)); _stake(validator, amount); + } else if (bytes32(data[:32]) == SLASH_SIG) { + (, uint256 exitEventId, address[] memory validatorsToSlash, uint256 slashingPercentage) = abi.decode( + data, + (bytes32, uint256, address[], uint256) + ); + _slash(exitEventId, validatorsToSlash, slashingPercentage); // reuse slashingPercentage present during this slash's initiation } } @@ -131,16 +150,21 @@ contract ValidatorSet is IValidatorSet, ERC20VotesUpgradeable, System { emit WithdrawalRegistered(account, amount); } - /// @dev no public facing slashing function implemented yet - // slither-disable-next-line dead-code - function _slash(address validator) internal { - // unstake validator - _burn(validator, balanceOf(validator)); - // remove pending withdrawals - // slither-disable-next-line mapping-deletion - delete withdrawals[validator]; - // slash validator - stateSender.syncState(rootChainManager, abi.encode(SLASH_SIG, validator)); + function _slash(uint256 exitEventId, address[] memory validatorsToSlash, uint256 slashingPercentage) internal { + require(!slashProcessed[exitEventId], "SLASH_ALREADY_PROCESSED"); // sanity check + slashProcessed[exitEventId] = true; + uint256 length = validatorsToSlash.length; + uint256[] memory slashedAmounts = new uint256[](length); + for (uint256 i = 0; i < length; ) { + slashedAmounts[i] = (balanceOf(validatorsToSlash[i]) * slashingPercentage) / 100; + _burn(validatorsToSlash[i], slashedAmounts[i]); // partially unstake validator + // slither-disable-next-line mapping-deletion + delete withdrawals[validatorsToSlash[i]]; // remove pending withdrawals + unchecked { + ++i; + } + } + emit Slashed(exitEventId, validatorsToSlash, slashedAmounts); } function _stake(address validator, uint256 amount) internal { diff --git a/contracts/interfaces/child/validator/IValidatorSet.sol b/contracts/interfaces/child/validator/IValidatorSet.sol index d9fefdcd..2b91adb9 100644 --- a/contracts/interfaces/child/validator/IValidatorSet.sol +++ b/contracts/interfaces/child/validator/IValidatorSet.sol @@ -22,7 +22,7 @@ struct Epoch { */ interface IValidatorSet is IStateReceiver { event NewEpoch(uint256 indexed id, uint256 indexed startBlock, uint256 indexed endBlock, bytes32 epochRoot); - event Slashed(uint256 indexed validator, uint256 amount); + event Slashed(uint256 indexed exitId, address[] validators, uint256[] amounts); event WithdrawalRegistered(address indexed account, uint256 amount); event Withdrawal(address indexed account, uint256 amount); @@ -30,6 +30,13 @@ interface IValidatorSet is IStateReceiver { /// @dev system call function commitEpoch(uint256 id, Epoch calldata epoch, uint256 epochSize) external; + /// @notice initialises slashing process + /// @dev system call, + /// @dev given list of validators are slashed on L2 + /// subsequently after their stake is slashed on L1 + /// @param validators list of validators to be slashed + function slash(address[] calldata validators) external; + /// @notice allows a validator to announce their intention to withdraw a given amount of tokens /// @dev initializes a waiting period before the tokens can be withdrawn function unstake(uint256 amount) external; diff --git a/contracts/interfaces/root/IExitHelper.sol b/contracts/interfaces/root/IExitHelper.sol index 64fdbf12..92a8c047 100644 --- a/contracts/interfaces/root/IExitHelper.sol +++ b/contracts/interfaces/root/IExitHelper.sol @@ -14,6 +14,13 @@ interface IExitHelper { bytes32[] proof; } + /** + * @notice Returns the address that called the exit function + * @dev only available in the context of the exit function + * @return address of the caller + */ + function caller() external view returns (address); + /** * @notice Perform an exit for one event * @param blockNumber Block number of the exit event on L2 diff --git a/contracts/root/ExitHelper.sol b/contracts/root/ExitHelper.sol index e3e31782..dd0bd630 100644 --- a/contracts/root/ExitHelper.sol +++ b/contracts/root/ExitHelper.sol @@ -8,6 +8,7 @@ import "../interfaces/root/IExitHelper.sol"; contract ExitHelper is IExitHelper, Initializable { mapping(uint256 => bool) public processedExits; ICheckpointManager public checkpointManager; + address public caller; event ExitProcessed(uint256 indexed id, bool indexed success, bytes returnData); @@ -83,10 +84,14 @@ contract ExitHelper is IExitHelper, Initializable { processedExits[id] = true; - // slither-disable-next-line calls-loop,low-level-calls,reentrancy-events,reentrancy-no-eth + // slither-disable-next-line costly-loop + caller = msg.sender; + // slither-disable-next-line calls-loop,low-level-calls,reentrancy-events,reentrancy-no-eth,reentrancy-benign (bool success, bytes memory returnData) = receiver.call( abi.encodeWithSignature("onL2StateReceive(uint256,address,bytes)", id, sender, data) ); + // slither-disable-next-line costly-loop + caller = address(0); // if state sync fails, revert flag if (!success) processedExits[id] = false; diff --git a/contracts/root/staking/CustomSupernetManager.sol b/contracts/root/staking/CustomSupernetManager.sol index 195bb14a..54d7ee23 100644 --- a/contracts/root/staking/CustomSupernetManager.sol +++ b/contracts/root/staking/CustomSupernetManager.sol @@ -8,6 +8,7 @@ import "./SupernetManager.sol"; import "../../interfaces/common/IBLS.sol"; import "../../interfaces/IStateSender.sol"; import "../../interfaces/root/staking/ICustomSupernetManager.sol"; +import "../../interfaces/root/IExitHelper.sol"; contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeable, SupernetManager { using SafeERC20 for IERC20; @@ -16,7 +17,6 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl bytes32 private constant STAKE_SIG = keccak256("STAKE"); bytes32 private constant UNSTAKE_SIG = keccak256("UNSTAKE"); bytes32 private constant SLASH_SIG = keccak256("SLASH"); - uint256 public constant SLASHING_PERCENTAGE = 50; IBLS private bls; IStateSender private stateSender; @@ -121,8 +121,9 @@ 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, uint256 slashingPercentage, uint256 slashIncentivePercentage) = abi + .decode(data, (bytes32, address[], uint256, uint256)); + _slash(id, validatorsToSlash, slashingPercentage, slashIncentivePercentage); } } @@ -157,12 +158,34 @@ 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, + uint256 slashingPercentage, + uint256 slashIncentivePercentage + ) internal { + uint256 length = validatorsToSlash.length; + uint256 totalSlashedAmount; + for (uint256 i = 0; i < length; ) { + uint256 slashedAmount = (stakeManager.stakeOf(validatorsToSlash[i], id) * slashingPercentage) / 100; + // slither-disable-next-line reentrancy-benign,reentrancy-events,reentrancy-no-eth + 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 * slashIncentivePercentage) / 100; + matic.safeTransfer(IExitHelper(exitHelper).caller(), rewardAmount); + + // complete slashing on child chain + stateSender.syncState( + childValidatorSet, + abi.encode(SLASH_SIG, exitEventId, validatorsToSlash, slashingPercentage) + ); } function _verifyValidatorRegistration( diff --git a/docs/child/validator/ValidatorSet.md b/docs/child/validator/ValidatorSet.md index 276ca98f..1b423fca 100644 --- a/docs/child/validator/ValidatorSet.md +++ b/docs/child/validator/ValidatorSet.md @@ -140,6 +140,40 @@ function READ_ADDRESSLIST_GAS() external view returns (uint256) +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### SLASHING_PERCENTAGE + +```solidity +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 | @@ -764,6 +798,44 @@ function permit(address owner, address spender, uint256 value, uint256 deadline, | r | bytes32 | undefined | | s | bytes32 | undefined | +### slash + +```solidity +function slash(address[] validators) external nonpayable +``` + +initialises slashing process + +*system call,given list of validators are slashed on L2 subsequently after their stake is slashed on L1* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validators | address[] | list of validators to be slashed | + +### slashProcessed + +```solidity +function slashProcessed(uint256) external view returns (bool) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + ### symbol ```solidity @@ -1045,7 +1117,7 @@ event NewEpoch(uint256 indexed id, uint256 indexed startBlock, uint256 indexed e ### Slashed ```solidity -event Slashed(uint256 indexed validator, uint256 amount) +event Slashed(uint256 indexed exitId, address[] validators, uint256[] amounts) ``` @@ -1056,8 +1128,9 @@ event Slashed(uint256 indexed validator, uint256 amount) | Name | Type | Description | |---|---|---| -| validator `indexed` | uint256 | undefined | -| amount | uint256 | undefined | +| exitId `indexed` | uint256 | undefined | +| validators | address[] | undefined | +| amounts | uint256[] | undefined | ### Transfer diff --git a/docs/interfaces/child/validator/IValidatorSet.md b/docs/interfaces/child/validator/IValidatorSet.md index 5138289c..e4d6bd37 100644 --- a/docs/interfaces/child/validator/IValidatorSet.md +++ b/docs/interfaces/child/validator/IValidatorSet.md @@ -91,6 +91,22 @@ Calculates how much is yet to become withdrawable for account. |---|---|---| | _0 | uint256 | Amount not yet withdrawable (in MATIC wei) | +### slash + +```solidity +function slash(address[] validators) external nonpayable +``` + +initialises slashing process + +*system call,given list of validators are slashed on L2 subsequently after their stake is slashed on L1* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validators | address[] | list of validators to be slashed | + ### totalBlocks ```solidity @@ -210,7 +226,7 @@ event NewEpoch(uint256 indexed id, uint256 indexed startBlock, uint256 indexed e ### Slashed ```solidity -event Slashed(uint256 indexed validator, uint256 amount) +event Slashed(uint256 indexed exitId, address[] validators, uint256[] amounts) ``` @@ -221,8 +237,9 @@ event Slashed(uint256 indexed validator, uint256 amount) | Name | Type | Description | |---|---|---| -| validator `indexed` | uint256 | undefined | -| amount | uint256 | undefined | +| exitId `indexed` | uint256 | undefined | +| validators | address[] | undefined | +| amounts | uint256[] | undefined | ### Withdrawal diff --git a/docs/interfaces/root/IExitHelper.md b/docs/interfaces/root/IExitHelper.md index d84afed9..119be1f1 100644 --- a/docs/interfaces/root/IExitHelper.md +++ b/docs/interfaces/root/IExitHelper.md @@ -26,6 +26,23 @@ function batchExit(IExitHelper.BatchExitInput[] inputs) external nonpayable |---|---|---| | inputs | IExitHelper.BatchExitInput[] | undefined | +### caller + +```solidity +function caller() external view returns (address) +``` + +Returns the address that called the exit function + +*only available in the context of the exit function* + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | address of the caller | + ### exit ```solidity diff --git a/docs/root/ExitHelper.md b/docs/root/ExitHelper.md index 671eddb3..e0593be2 100644 --- a/docs/root/ExitHelper.md +++ b/docs/root/ExitHelper.md @@ -26,6 +26,23 @@ function batchExit(IExitHelper.BatchExitInput[] inputs) external nonpayable |---|---|---| | inputs | IExitHelper.BatchExitInput[] | undefined | +### caller + +```solidity +function caller() external view returns (address) +``` + +Returns the address that called the exit function + +*only available in the context of the exit function* + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | address of the caller | + ### checkpointManager ```solidity diff --git a/docs/root/staking/CustomSupernetManager.md b/docs/root/staking/CustomSupernetManager.md index 4e446511..b3135f74 100644 --- a/docs/root/staking/CustomSupernetManager.md +++ b/docs/root/staking/CustomSupernetManager.md @@ -10,23 +10,6 @@ ## Methods -### SLASHING_PERCENTAGE - -```solidity -function SLASHING_PERCENTAGE() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### acceptOwnership ```solidity diff --git a/test/forge/child/validator/ValidatorSet.t.sol b/test/forge/child/validator/ValidatorSet.t.sol index 55e1ae3c..a1fab267 100644 --- a/test/forge/child/validator/ValidatorSet.t.sol +++ b/test/forge/child/validator/ValidatorSet.t.sol @@ -213,3 +213,55 @@ contract ValidatorSet_WithdrawStake is Committed { assertEq(validatorSet.withdrawable(address(this)), 0); } } + +contract ValidatorSet_Slash is Committed { + bytes32 private constant SLASH_SIG = keccak256("SLASH"); + event L2StateSynced(uint256 indexed id, address indexed sender, address indexed receiver, bytes data); + event Slashed(uint256 indexed exitId, address[] validators, uint256[] amounts); + + function test_InitilizeSlashOnlySystemCall() public { + vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, "SYSTEMCALL")); + validatorSet.slash(new address[](0)); + } + + function test_InitialiseSlash(address[] memory validators) public { + vm.assume(validators.length <= (stateSender.MAX_LENGTH() - 160) / 32); + uint256 slashingPercentage = validatorSet.SLASHING_PERCENTAGE(); + uint256 slashIncentivePercentage = validatorSet.SLASH_INCENTIVE_PERCENTAGE(); + vm.prank(SYSTEM); + vm.expectEmit(true, true, true, true); + emit L2StateSynced(1, address(validatorSet), rootChainManager, abi.encode(SLASH_SIG, validators, slashingPercentage, slashIncentivePercentage)); + validatorSet.slash(validators); + } + + function test_FinalizeSlash() public { + uint256 exitEventId = 1; + uint256 slashingPercentage = validatorSet.SLASHING_PERCENTAGE(); + address[] memory validatorsToSlash = new address[](2); + validatorsToSlash[0] = alice; + validatorsToSlash[1] = address(this); + uint256[] memory slashedAmounts = new uint256[](2); + slashedAmounts[0] = (validatorSet.balanceOf(alice) * slashingPercentage) / 100; + slashedAmounts[1] = (validatorSet.balanceOf(address(this)) * slashingPercentage) / 100; + + assertEq(validatorSet.balanceOf(alice), 100); + assertEq(validatorSet.balanceOf(address(this)), 300); + + vm.expectEmit(true, true, true, true); + emit Slashed(exitEventId, validatorsToSlash, slashedAmounts); + vm.prank(stateReceiver); + validatorSet.onStateReceive(1 /* StateSyncCounter */, rootChainManager, abi.encode(SLASH_SIG, exitEventId, validatorsToSlash, slashingPercentage)); + + assertEq(validatorSet.balanceOf(alice), 100 - slashedAmounts[0]); + assertEq(validatorSet.balanceOf(address(this)), 300 - slashedAmounts[1]); + } + + function test_FinalizeSlashAlreadyProcessedSanityCheck() public { + uint256 exitEventId = 1; + vm.startPrank(stateReceiver); + validatorSet.onStateReceive(1 /* StateSyncCounter */, rootChainManager, abi.encode(SLASH_SIG, exitEventId, new address[](0), 0)); + vm.expectRevert("SLASH_ALREADY_PROCESSED"); + validatorSet.onStateReceive(1 /* StateSyncCounter */, rootChainManager, abi.encode(SLASH_SIG, exitEventId, new address[](0), 0)); + vm.stopPrank(); + } +} diff --git a/test/forge/root/staking/CustomSupernetManager.t.sol b/test/forge/root/staking/CustomSupernetManager.t.sol index b58268a4..83797d23 100644 --- a/test/forge/root/staking/CustomSupernetManager.t.sol +++ b/test/forge/root/staking/CustomSupernetManager.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.19; import "@utils/Test.sol"; import "contracts/common/BLS.sol"; import "contracts/root/StateSender.sol"; +import {ExitHelper} from "contracts/root/ExitHelper.sol"; import {StakeManager} from "contracts/root/staking/StakeManager.sol"; import {CustomSupernetManager, Validator, GenesisValidator} from "contracts/root/staking/CustomSupernetManager.sol"; import {MockERC20} from "contracts/mocks/MockERC20.sol"; @@ -15,6 +16,7 @@ abstract contract Uninitialized is Test { address childValidatorSet; address exitHelper; string constant DOMAIN = "CUSTOM_SUPERNET_MANAGER"; + bytes32 internal constant callerSlotOnExitHelper = bytes32(uint256(3)); MockERC20 token; StakeManager stakeManager; CustomSupernetManager supernetManager; @@ -23,7 +25,7 @@ abstract contract Uninitialized is Test { bls = new BLS(); stateSender = new StateSender(); childValidatorSet = makeAddr("childValidatorSet"); - exitHelper = makeAddr("exitHelper"); + exitHelper = address(new ExitHelper()); token = new MockERC20(); stakeManager = new StakeManager(); supernetManager = new CustomSupernetManager(); @@ -144,12 +146,18 @@ abstract contract EnabledStaking is FinalizedGenesis { abstract contract Slashed is EnabledStaking { bytes32 private constant SLASH_SIG = keccak256("SLASH"); + uint256 internal slashingPercentage = 50; // sent from ValidatorSet + uint256 internal slashIncentivePercentage = 30; // sent from ValidatorSet 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, slashingPercentage, slashIncentivePercentage); + vm.store(exitHelper, callerSlotOnExitHelper, bytes32(uint256(uint160(makeAddr("MEV"))))); // simulate caller of exit() vm.prank(exitHelper); supernetManager.onL2StateReceive(1, childValidatorSet, callData); + vm.store(exitHelper, callerSlotOnExitHelper, bytes32(0)); } } @@ -376,16 +384,39 @@ 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); + + uint256 private slashingPercentage = 50; // sent from ValidatorSet + uint256 private slashIncentivePercentage = 30; // sent from ValidatorSet function test_SuccessfulFullWithdrawal() public { - uint256 slashingPercentage = supernetManager.SLASHING_PERCENTAGE(); - bytes memory callData = abi.encode(SLASH_SIG, address(this)); + uint256 exitEventId = 1; + + address[] memory validatorsToSlash = new address[](1); + validatorsToSlash[0] = address(this); + bytes memory callData = abi.encode(SLASH_SIG, validatorsToSlash, slashingPercentage, slashIncentivePercentage); + 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, slashingPercentage)); + vm.store(exitHelper, callerSlotOnExitHelper, bytes32(uint256(uint160(mev)))); // simulate caller of exit() vm.prank(exitHelper); - supernetManager.onL2StateReceive(1, childValidatorSet, callData); + supernetManager.onL2StateReceive(exitEventId, childValidatorSet, callData); + vm.store(exitHelper, callerSlotOnExitHelper, bytes32(0)); + assertEq(stakeManager.stakeOf(address(this), 1), 0, "should unstake all"); assertEq( stakeManager.withdrawableStake(address(this)), @@ -394,6 +425,62 @@ 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, slashingPercentage, slashIncentivePercentage); + uint256 slashedAmount = (amount * slashingPercentage) / 100; + uint256 exitorReward = (slashedAmount * slashIncentivePercentage) / 100; + + assertEq(token.balanceOf(mev), 0); // balance before + vm.expectEmit(true, true, true, true); + emit Transfer(address(supernetManager), mev, exitorReward); + vm.store(exitHelper, callerSlotOnExitHelper, bytes32(uint256(uint160(mev)))); // simulate caller of exit() + vm.prank(exitHelper); + supernetManager.onL2StateReceive(exitEventId, childValidatorSet, callData); + vm.store(exitHelper, callerSlotOnExitHelper, bytes32(0)); + assertEq(token.balanceOf(mev), exitorReward, "should transfer slashing reward"); + } + + function test_SlashEntireValidatorSet() external { + uint256 aliceStakedAmount = amount << 3; + token.mint(alice, aliceStakedAmount); + vm.prank(alice); + stakeManager.stakeFor(1, aliceStakedAmount); + + uint256 aliceSlashedAmount = (aliceStakedAmount * slashingPercentage) / 100; + uint256 thisSlashedAmount = (amount * slashingPercentage) / 100; + + address[] memory validatorsToSlash = new address[](2); + validatorsToSlash[0] = alice; + validatorsToSlash[1] = address(this); + bytes memory callData = abi.encode(SLASH_SIG, validatorsToSlash, slashingPercentage, slashIncentivePercentage); + + vm.store(exitHelper, callerSlotOnExitHelper, bytes32(uint256(uint160(mev)))); // simulate caller of exit() + vm.prank(exitHelper); + supernetManager.onL2StateReceive(1, childValidatorSet, callData); + vm.store(exitHelper, callerSlotOnExitHelper, bytes32(0)); + + 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) * slashIncentivePercentage) / 100; + assertEq(token.balanceOf(mev), exitorReward, "should transfer slashing reward"); + } } contract CustomSupernetManager_WithdrawSlash is Slashed { @@ -406,10 +493,12 @@ contract CustomSupernetManager_WithdrawSlash is Slashed { } function test_WithdrawSlashedAmount() public { - uint256 slashedAmount = amount / 2; - assertEq(token.balanceOf(address(supernetManager)), slashedAmount); + uint256 slashedAmount = (amount * slashingPercentage) / 100; + uint256 slashingReward = (slashedAmount * slashIncentivePercentage) / 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); }