diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 915350471e89..58fd80366297 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -255,7 +255,8 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali _config.slashAmounts, _config.targetCommitteeSize, _config.aztecEpochDuration, - _config.slashingOffsetInRounds + _config.slashingOffsetInRounds, + _config.slashingDisableDuration ); } else { slasher = EmpireSlasherDeploymentExtLib.deployEmpireSlasher( @@ -265,7 +266,8 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali _config.slashingQuorum, _config.slashingRoundSize, _config.slashingLifetimeInRounds, - _config.slashingExecutionDelayInRounds + _config.slashingExecutionDelayInRounds, + _config.slashingDisableDuration ); } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index da3ae4d5d48b..f50f0f97dd0c 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -67,6 +67,7 @@ struct RollupConfigInput { uint256 slashingOffsetInRounds; SlasherFlavor slasherFlavor; address slashingVetoer; + uint256 slashingDisableDuration; uint256 manaTarget; uint256 exitDelaySeconds; uint32 version; diff --git a/l1-contracts/src/core/interfaces/ISlasher.sol b/l1-contracts/src/core/interfaces/ISlasher.sol index 2e78ac42cfd9..6547e8582334 100644 --- a/l1-contracts/src/core/interfaces/ISlasher.sol +++ b/l1-contracts/src/core/interfaces/ISlasher.sol @@ -12,7 +12,10 @@ enum SlasherFlavor { interface ISlasher { event VetoedPayload(address indexed payload); + event SlashingDisabled(uint256 disabledUntil); function slash(IPayload _payload) external returns (bool); function vetoPayload(IPayload _payload) external returns (bool); + function setSlashingEnabled(bool _enabled) external; + function isSlashingEnabled() external view returns (bool); } diff --git a/l1-contracts/src/core/libraries/rollup/EmpireSlasherDeploymentExtLib.sol b/l1-contracts/src/core/libraries/rollup/EmpireSlasherDeploymentExtLib.sol index 724e7a9b3299..421f84a2c77f 100644 --- a/l1-contracts/src/core/libraries/rollup/EmpireSlasherDeploymentExtLib.sol +++ b/l1-contracts/src/core/libraries/rollup/EmpireSlasherDeploymentExtLib.sol @@ -24,10 +24,11 @@ library EmpireSlasherDeploymentExtLib { uint256 _quorumSize, uint256 _roundSize, uint256 _lifetimeInRounds, - uint256 _executionDelayInRounds + uint256 _executionDelayInRounds, + uint256 _slashingDisableDuration ) external returns (ISlasher) { // Deploy slasher first - Slasher slasher = new Slasher(_vetoer, _governance); + Slasher slasher = new Slasher(_vetoer, _governance, _slashingDisableDuration); // Deploy proposer with slasher address EmpireSlashingProposer proposer = new EmpireSlashingProposer( diff --git a/l1-contracts/src/core/libraries/rollup/TallySlasherDeploymentExtLib.sol b/l1-contracts/src/core/libraries/rollup/TallySlasherDeploymentExtLib.sol index 1bc769acac49..0516e141dd77 100644 --- a/l1-contracts/src/core/libraries/rollup/TallySlasherDeploymentExtLib.sol +++ b/l1-contracts/src/core/libraries/rollup/TallySlasherDeploymentExtLib.sol @@ -27,10 +27,11 @@ library TallySlasherDeploymentExtLib { uint256[3] calldata _slashAmounts, uint256 _committeeSize, uint256 _epochDuration, - uint256 _slashOffsetInRounds + uint256 _slashOffsetInRounds, + uint256 _slashingDisableDuration ) external returns (ISlasher) { // Deploy slasher first - Slasher slasher = new Slasher(_vetoer, _governance); + Slasher slasher = new Slasher(_vetoer, _governance, _slashingDisableDuration); // Deploy proposer with slasher address TallySlashingProposer proposer = new TallySlashingProposer( diff --git a/l1-contracts/src/core/slashing/Slasher.sol b/l1-contracts/src/core/slashing/Slasher.sol index 1701830d9353..67fe1dbad086 100644 --- a/l1-contracts/src/core/slashing/Slasher.sol +++ b/l1-contracts/src/core/slashing/Slasher.sol @@ -8,9 +8,12 @@ import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; contract Slasher is ISlasher { address public immutable GOVERNANCE; address public immutable VETOER; + uint256 public immutable SLASHING_DISABLE_DURATION; // solhint-disable-next-line var-name-mixedcase address public PROPOSER; + uint256 public slashingDisabledUntil = 0; + mapping(address payload => bool vetoed) public vetoedPayloads; error Slasher__SlashFailed(address target, bytes data, bytes returnData); @@ -19,10 +22,12 @@ contract Slasher is ISlasher { error Slasher__PayloadVetoed(address payload); error Slasher__AlreadyInitialized(); error Slasher__ProposerZeroAddress(); + error Slasher__SlashingDisabled(); - constructor(address _vetoer, address _governance) { + constructor(address _vetoer, address _governance, uint256 _slashingDisableDuration) { GOVERNANCE = _governance; VETOER = _vetoer; + SLASHING_DISABLE_DURATION = _slashingDisableDuration; } // solhint-disable-next-line comprehensive-interface @@ -39,8 +44,19 @@ contract Slasher is ISlasher { return true; } + function setSlashingEnabled(bool _enabled) external override(ISlasher) { + require(msg.sender == VETOER, Slasher__CallerNotVetoer(msg.sender, VETOER)); + if (!_enabled) { + slashingDisabledUntil = block.timestamp + SLASHING_DISABLE_DURATION; + } else { + slashingDisabledUntil = 0; + } + emit SlashingDisabled(slashingDisabledUntil); + } + function slash(IPayload _payload) external override(ISlasher) returns (bool) { require(msg.sender == PROPOSER || msg.sender == GOVERNANCE, Slasher__CallerNotAuthorizedToSlash(msg.sender)); + require(block.timestamp >= slashingDisabledUntil, Slasher__SlashingDisabled()); require(!vetoedPayloads[address(_payload)], Slasher__PayloadVetoed(address(_payload))); IPayload.Action[] memory actions = _payload.getActions(); @@ -51,4 +67,8 @@ contract Slasher is ISlasher { return true; } + + function isSlashingEnabled() external view override(ISlasher) returns (bool) { + return block.timestamp >= slashingDisabledUntil; + } } diff --git a/l1-contracts/test/governance/scenario/slashing/TallySlashing.t.sol b/l1-contracts/test/governance/scenario/slashing/TallySlashing.t.sol index d0b3f065b1d5..03a1375f80b2 100644 --- a/l1-contracts/test/governance/scenario/slashing/TallySlashing.t.sol +++ b/l1-contracts/test/governance/scenario/slashing/TallySlashing.t.sol @@ -205,7 +205,7 @@ contract SlashingTest is TestBase { _setupCommitteeForSlashing(_lifetimeInRounds, _executionDelayInRounds); address[] memory attesters = rollup.getEpochCommittee(Epoch.wrap(INITIAL_EPOCH)); - uint96 slashAmount = 20e18; + uint96 slashAmount = uint96(slashingProposer.SLASH_AMOUNT_SMALL()); SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, attesters.length); uint256 firstExecutableSlot = @@ -232,7 +232,7 @@ contract SlashingTest is TestBase { _lifetimeInRounds = bound(_lifetimeInRounds, _executionDelayInRounds + 1, 127); // Must be < ROUNDABOUT_SIZE _setupCommitteeForSlashing(_lifetimeInRounds, _executionDelayInRounds); - uint96 slashAmount = 20e18; + uint96 slashAmount = uint96(slashingProposer.SLASH_AMOUNT_SMALL()); SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, COMMITTEE_SIZE); uint256 firstExecutableSlot = @@ -275,7 +275,7 @@ contract SlashingTest is TestBase { _setupCommitteeForSlashing(_lifetimeInRounds, _executionDelayInRounds); address[] memory attesters = rollup.getEpochCommittee(Epoch.wrap(INITIAL_EPOCH)); - uint96 slashAmount = 20e18; + uint96 slashAmount = uint96(slashingProposer.SLASH_AMOUNT_SMALL()); SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, attesters.length); // For tally slashing, we need to predict the payload address and veto it @@ -307,6 +307,81 @@ contract SlashingTest is TestBase { slashingProposer.executeRound(firstSlashingRound, committees); } + function test_SlashingDisableTimestamp() public { + _setupCommitteeForSlashing(); + address[] memory attesters = rollup.getEpochCommittee(Epoch.wrap(INITIAL_EPOCH)); + uint96 slashAmount = uint96(slashingProposer.SLASH_AMOUNT_SMALL()); + SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, attesters.length); + + // Initially slashing should be enabled + assertEq(slasher.isSlashingEnabled(), true, "Slashing should be enabled initially"); + + // Disable slashing temporarily + vm.prank(address(slasher.VETOER())); + slasher.setSlashingEnabled(false); + + // Should be disabled now + assertEq(slasher.isSlashingEnabled(), false, "Slashing should be disabled after setting to false"); + uint256 disableDuration = slasher.SLASHING_DISABLE_DURATION(); + + // Fast forward time but not past the disable duration (still disabled) + vm.warp(block.timestamp + disableDuration - 10 minutes); + assertEq(slasher.isSlashingEnabled(), false, "Slashing should still be disabled after 30 minutes"); + + // Fast forward time beyond the disable duration (should be enabled again) + vm.warp(block.timestamp + disableDuration + 1 minutes); + assertEq(slasher.isSlashingEnabled(), true, "Slashing should be enabled again after disable duration expires"); + + // Re-enable manually by calling setSlashingEnabled(true) + vm.prank(address(slasher.VETOER())); + slasher.setSlashingEnabled(false); + assertEq(slasher.isSlashingEnabled(), false, "Slashing should be disabled again"); + + vm.prank(address(slasher.VETOER())); + slasher.setSlashingEnabled(true); + assertEq(slasher.isSlashingEnabled(), true, "Slashing should be enabled after manual re-enable"); + } + + function test_CannotSlashIfDisabled() public { + // Use fixed values for a deterministic test + uint256 _lifetimeInRounds = 5; + uint256 _executionDelayInRounds = 1; + + _setupCommitteeForSlashing(_lifetimeInRounds, _executionDelayInRounds); + address[] memory attesters = rollup.getEpochCommittee(Epoch.wrap(INITIAL_EPOCH)); + uint96 slashAmount = uint96(slashingProposer.SLASH_AMOUNT_SMALL()); + SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, attesters.length); + + // Calculate executable slot + uint256 firstExecutableSlot = + (SlashRound.unwrap(firstSlashingRound) + _executionDelayInRounds + 1) * slashingProposer.ROUND_SIZE(); + + // Setup committees + address[][] memory committees = new address[][](slashingProposer.ROUND_SIZE_IN_EPOCHS()); + for (uint256 i = 0; i < committees.length; i++) { + Epoch epochSlashed = slashingProposer.getSlashTargetEpoch(firstSlashingRound, i); + committees[i] = rollup.getEpochCommittee(epochSlashed); + } + + // Jump to executable slot + timeCheater.cheat__jumpToSlot(firstExecutableSlot); + + // Disable slashing - this should prevent execution for 1 hour + vm.prank(address(slasher.VETOER())); + slasher.setSlashingEnabled(false); + + // Should fail while slashing is disabled + vm.expectRevert(abi.encodeWithSelector(Slasher.Slasher__SlashingDisabled.selector)); + slashingProposer.executeRound(firstSlashingRound, committees); + + // Re-enable manually by calling setSlashingEnabled(true) + vm.prank(address(slasher.VETOER())); + slasher.setSlashingEnabled(true); + + // Should now work since slashing was re-enabled + slashingProposer.executeRound(firstSlashingRound, committees); + } + function test_SlashingSmallAmount() public { _setupCommitteeForSlashing(); uint256 howManyToSlash = HOW_MANY_SLASHED; diff --git a/l1-contracts/test/harnesses/TestConstants.sol b/l1-contracts/test/harnesses/TestConstants.sol index c0b9a520b1cc..cf3988d66ca5 100644 --- a/l1-contracts/test/harnesses/TestConstants.sol +++ b/l1-contracts/test/harnesses/TestConstants.sol @@ -26,6 +26,7 @@ library TestConstants { uint256 internal constant AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS = 0; uint256 internal constant AZTEC_SLASHING_OFFSET_IN_ROUNDS = 2; address internal constant AZTEC_SLASHING_VETOER = address(0); + uint256 internal constant AZTEC_SLASHING_DISABLE_DURATION = 5 days; uint256 internal constant AZTEC_SLASH_AMOUNT_SMALL = 20e18; uint256 internal constant AZTEC_SLASH_AMOUNT_MEDIUM = 40e18; uint256 internal constant AZTEC_SLASH_AMOUNT_LARGE = 60e18; @@ -103,6 +104,7 @@ library TestConstants { slashingExecutionDelayInRounds: AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS, slashingOffsetInRounds: AZTEC_SLASHING_OFFSET_IN_ROUNDS, slashingVetoer: AZTEC_SLASHING_VETOER, + slashingDisableDuration: AZTEC_SLASHING_DISABLE_DURATION, manaTarget: AZTEC_MANA_TARGET, exitDelaySeconds: AZTEC_EXIT_DELAY_SECONDS, provingCostPerMana: AZTEC_PROVING_COST_PER_MANA, diff --git a/yarn-project/cli/src/config/chain_l2_config.ts b/yarn-project/cli/src/config/chain_l2_config.ts index 674a943c900b..5c58d46ff9d1 100644 --- a/yarn-project/cli/src/config/chain_l2_config.ts +++ b/yarn-project/cli/src/config/chain_l2_config.ts @@ -45,6 +45,8 @@ const DefaultSlashConfig = { slashingOffsetInRounds: 2, /** No slash vetoer */ slashingVetoer: EthAddress.ZERO, + /** Use default disable duration */ + slashingDisableDuration: DefaultL1ContractsConfig.slashingDisableDuration, /** Use default slash amounts */ slashAmountSmall: DefaultL1ContractsConfig.slashAmountSmall, slashAmountMedium: DefaultL1ContractsConfig.slashAmountMedium, diff --git a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts index 1901a1a9c981..f8a2b03d9866 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts @@ -162,6 +162,7 @@ describe('e2e_p2p_add_rollup', () => { slashingLifetimeInRounds: t.ctx.aztecNodeConfig.slashingLifetimeInRounds, slashingExecutionDelayInRounds: t.ctx.aztecNodeConfig.slashingExecutionDelayInRounds, slashingVetoer: t.ctx.aztecNodeConfig.slashingVetoer, + slashingDisableDuration: t.ctx.aztecNodeConfig.slashingDisableDuration, manaTarget: t.ctx.aztecNodeConfig.manaTarget, provingCostPerMana: t.ctx.aztecNodeConfig.provingCostPerMana, feeJuicePortalInitialBalance: fundingNeeded, diff --git a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts index a6f15fe013c0..9ac39e72224d 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts @@ -158,7 +158,7 @@ describe('veto slash', () => { const vetoer = deployerClient.account.address; const governance = EthAddress.random().toString(); // We don't need a real governance address for this test debugLogger.info(`\n\ndeploying slasher with vetoer: ${vetoer}\n\n`); - const slasher = await deployer.deploy(SlasherArtifact, [vetoer, governance]); + const slasher = await deployer.deploy(SlasherArtifact, [vetoer, governance, 3600n]); await deployer.waitForDeployments(); let proposer: EthAddress; diff --git a/yarn-project/ethereum/src/config.ts b/yarn-project/ethereum/src/config.ts index e22478a47eb8..8282dda2aadb 100644 --- a/yarn-project/ethereum/src/config.ts +++ b/yarn-project/ethereum/src/config.ts @@ -50,6 +50,8 @@ export type L1ContractsConfig = { slashingVetoer: EthAddress; /** How many slashing rounds back we slash (ie when slashing in round N, we slash for offenses committed during epochs of round N-offset) */ slashingOffsetInRounds: number; + /** How long slashing can be disabled for in seconds when vetoer disables it */ + slashingDisableDuration: number; /** Type of slasher proposer */ slasherFlavor: 'empire' | 'tally' | 'none'; /** Minimum amount that can be slashed in tally slashing */ @@ -93,6 +95,7 @@ export const DefaultL1ContractsConfig = { exitDelaySeconds: 2 * 24 * 60 * 60, slasherFlavor: 'tally' as const, slashingOffsetInRounds: 2, + slashingDisableDuration: 5 * 24 * 60 * 60, // 5 days in seconds } satisfies L1ContractsConfig; const LocalGovernanceConfiguration = { @@ -384,6 +387,11 @@ export const l1ContractsConfigMappings: ConfigMappingsType = parseEnv: (val: string) => EthAddress.fromString(val), defaultValue: DefaultL1ContractsConfig.slashingVetoer, }, + slashingDisableDuration: { + env: 'AZTEC_SLASHING_DISABLE_DURATION', + description: 'How long slashing can be disabled for in seconds when vetoer disables it', + ...numberConfigHelper(DefaultL1ContractsConfig.slashingDisableDuration), + }, governanceProposerQuorum: { env: 'AZTEC_GOVERNANCE_PROPOSER_QUORUM', description: 'The governance proposing quorum', diff --git a/yarn-project/ethereum/src/contracts/slasher_contract.ts b/yarn-project/ethereum/src/contracts/slasher_contract.ts index ff93b8f1da61..e7b41c7e0bf9 100644 --- a/yarn-project/ethereum/src/contracts/slasher_contract.ts +++ b/yarn-project/ethereum/src/contracts/slasher_contract.ts @@ -38,6 +38,19 @@ export class SlasherContract { } } + /** + * Checks if slashing is currently enabled. Slashing can be disabled by the vetoer. + * @returns True if slashing is enabled, false otherwise + */ + public async isSlashingEnabled(): Promise { + try { + return await this.contract.read.isSlashingEnabled(); + } catch (error) { + this.log.error(`Error checking if slashing is enabled`, error); + throw error; + } + } + /** * Gets the current vetoer address. * @returns The vetoer address @@ -47,6 +60,15 @@ export class SlasherContract { return EthAddress.fromString(vetoer); } + /** + * Gets the disable duration by the vetoer. + * @returns The disable duration in seconds + */ + public async getSlashingDisableDuration(): Promise { + const duration = await this.contract.read.SLASHING_DISABLE_DURATION(); + return Number(duration); + } + /** * Gets the current governance address. * @returns The governance address diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index e48065b81285..692021c22bf3 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -548,6 +548,7 @@ export const deployRollup = async ( slashingOffsetInRounds: BigInt(args.slashingOffsetInRounds), slashAmounts: [args.slashAmountSmall, args.slashAmountMedium, args.slashAmountLarge], localEjectionThreshold: args.localEjectionThreshold, + slashingDisableDuration: BigInt(args.slashingDisableDuration ?? 0n), }; const genesisStateArgs = { diff --git a/yarn-project/ethereum/src/queries.ts b/yarn-project/ethereum/src/queries.ts index d043141dcf05..3e827090ddeb 100644 --- a/yarn-project/ethereum/src/queries.ts +++ b/yarn-project/ethereum/src/queries.ts @@ -46,6 +46,7 @@ export async function getL1ContractsConfig( slashingOffsetInRounds, slashingAmounts, slashingVetoer, + slashingDisableDuration, manaTarget, provingCostPerMana, rollupVersion, @@ -71,6 +72,7 @@ export async function getL1ContractsConfig( slasherProposer?.type === 'tally' ? slasherProposer.getSlashOffsetInRounds() : 0n, slasherProposer?.type === 'tally' ? slasherProposer.getSlashingAmounts() : [0n, 0n, 0n], slasher?.getVetoer() ?? EthAddress.ZERO, + slasher?.getSlashingDisableDuration() ?? 0, rollup.getManaTarget(), rollup.getProvingCostPerMana(), rollup.getVersion(), @@ -96,7 +98,8 @@ export async function getL1ContractsConfig( slashingLifetimeInRounds: Number(slashingLifetimeInRounds), slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds), slashingVetoer, - manaTarget: manaTarget, + slashingDisableDuration, + manaTarget, provingCostPerMana: provingCostPerMana, rollupVersion: Number(rollupVersion), genesisArchiveTreeRoot, diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 9686769bed08..0aa94bcd62bb 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -253,6 +253,7 @@ export type EnvVar = | 'AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS' | 'AZTEC_SLASHING_VETOER' | 'AZTEC_SLASHING_OFFSET_IN_ROUNDS' + | 'AZTEC_SLASHING_DISABLE_DURATION' | 'AZTEC_SLASH_AMOUNT_SMALL' | 'AZTEC_SLASH_AMOUNT_MEDIUM' | 'AZTEC_SLASH_AMOUNT_LARGE' diff --git a/yarn-project/slasher/src/empire_slasher_client.test.ts b/yarn-project/slasher/src/empire_slasher_client.test.ts index 77ecaf1ef179..30fd6ba33dac 100644 --- a/yarn-project/slasher/src/empire_slasher_client.test.ts +++ b/yarn-project/slasher/src/empire_slasher_client.test.ts @@ -163,6 +163,7 @@ describe('EmpireSlasherClient', () => { // Setup rollup and slasher contract mocks rollupContract.getSlasherContract.mockResolvedValue(slasherContract); slasherContract.isPayloadVetoed.mockResolvedValue(false); + slasherContract.isSlashingEnabled.mockResolvedValue(true); // Create watcher dummyWatcher = new DummyWatcher(); diff --git a/yarn-project/slasher/src/empire_slasher_client.ts b/yarn-project/slasher/src/empire_slasher_client.ts index 0b03eb7c9339..45fb86f4e52b 100644 --- a/yarn-project/slasher/src/empire_slasher_client.ts +++ b/yarn-project/slasher/src/empire_slasher_client.ts @@ -404,11 +404,17 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher continue; } + // Check if slashing is enabled at all + if (!(await this.slasher.isSlashingEnabled())) { + this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`); + return undefined; + } + // Check if the slash payload is vetoed const isVetoed = await this.slasher.isPayloadVetoed(payload.payload); if (isVetoed) { - this.log.info(`Payload ${payload.payload} from round ${payload.round} is vetoed, skipping execution`); + this.log.info(`Payload ${payload.payload} from round ${payload.round} is vetoed (skipping execution)`); toRemove.push(payload); continue; } diff --git a/yarn-project/slasher/src/tally_slasher_client.test.ts b/yarn-project/slasher/src/tally_slasher_client.test.ts index 4c1444fe7316..1613766a1cbd 100644 --- a/yarn-project/slasher/src/tally_slasher_client.test.ts +++ b/yarn-project/slasher/src/tally_slasher_client.test.ts @@ -140,6 +140,7 @@ describe('TallySlasherClient', () => { // Setup rollup and slasher contract mocks rollup.getSlasherContract.mockResolvedValue(slasherContract); slasherContract.isPayloadVetoed.mockResolvedValue(false); + slasherContract.isSlashingEnabled.mockResolvedValue(true); // Mock event listeners to return unwatch functions tallySlashingProposer.listenToVoteCast.mockReturnValue(() => {}); @@ -360,6 +361,41 @@ describe('TallySlasherClient', () => { expect(actions).toEqual([]); }); + + it('should not execute vetoed rounds', async () => { + const currentRound = 5n; + const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const executableRound = 2n; // After execution delay of 2: currentRound - delay - 1 = 5 - 2 - 1 = 2 + + tallySlashingProposer.getRound.mockResolvedValueOnce({ + isExecuted: false, + readyToExecute: true, + voteCount: 120n, + }); + + const payloadAddress = EthAddress.random(); + tallySlashingProposer.getPayload.mockResolvedValue({ + address: payloadAddress, + actions: [{ validator: committee[0], slashAmount: slashingUnit }], + }); + + slasherContract.isPayloadVetoed.mockResolvedValueOnce(true); + const actions = await tallySlasherClient.getProposerActions(currentSlot); + + expect(actions).toHaveLength(0); + expect(tallySlashingProposer.getRound).toHaveBeenCalledWith(executableRound); + expect(slasherContract.isPayloadVetoed).toHaveBeenCalledWith(payloadAddress); + }); + + it('should not execute when slashing is disabled', async () => { + const currentRound = 5n; + const currentSlot = currentRound * BigInt(roundSize); // Round 5 + + slasherContract.isSlashingEnabled.mockResolvedValue(false); + const actions = await tallySlasherClient.getProposerActions(currentSlot); + + expect(actions).toHaveLength(0); + }); }); describe('multiple', () => { diff --git a/yarn-project/slasher/src/tally_slasher_client.ts b/yarn-project/slasher/src/tally_slasher_client.ts index 32fba5d70648..c035c0e45c82 100644 --- a/yarn-project/slasher/src/tally_slasher_client.ts +++ b/yarn-project/slasher/src/tally_slasher_client.ts @@ -189,6 +189,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC let logData: Record = { currentRound, executableRound, slotNumber }; try { + // Check if slashing is enabled at all + if (!(await this.slasher.isSlashingEnabled())) { + this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`, logData); + return undefined; + } + const roundInfo = await this.tallySlashingProposer.getRound(executableRound); logData = { ...logData, roundInfo }; if (roundInfo.isExecuted) {