diff --git a/l1-contracts/src/core/Leonidas.sol b/l1-contracts/src/core/Leonidas.sol
index 77244bec6284..213b6da39258 100644
--- a/l1-contracts/src/core/Leonidas.sol
+++ b/l1-contracts/src/core/Leonidas.sol
@@ -42,13 +42,17 @@ contract Leonidas is Staking, TimeFns, ILeonidas {
   LeonidasStorage private leonidasStore;
 
   constructor(
-    address _ares,
     IERC20 _stakingAsset,
     uint256 _minimumStake,
+    uint256 _slashingQuorum,
+    uint256 _roundSize,
     uint256 _slotDuration,
     uint256 _epochDuration,
     uint256 _targetCommitteeSize
-  ) Staking(_ares, _stakingAsset, _minimumStake) TimeFns(_slotDuration, _epochDuration) {
+  )
+    Staking(_stakingAsset, _minimumStake, _slashingQuorum, _roundSize)
+    TimeFns(_slotDuration, _epochDuration)
+  {
     GENESIS_TIME = Timestamp.wrap(block.timestamp);
     SLOT_DURATION = _slotDuration;
     EPOCH_DURATION = _epochDuration;
diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol
index 11ee424ba4dd..e9bee0d0b51b 100644
--- a/l1-contracts/src/core/Rollup.sol
+++ b/l1-contracts/src/core/Rollup.sol
@@ -52,6 +52,8 @@ struct Config {
   uint256 targetCommitteeSize;
   uint256 aztecEpochProofClaimWindowInL2Slots;
   uint256 minimumStake;
+  uint256 slashingQuorum;
+  uint256 slashingRoundSize;
 }
 
 /**
@@ -108,15 +110,15 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, Leonidas, IRollup, ITes
   )
     Ownable(_ares)
     Leonidas(
-      _ares,
       _stakingAsset,
       _config.minimumStake,
+      _config.slashingQuorum,
+      _config.slashingRoundSize,
       _config.aztecSlotDuration,
       _config.aztecEpochDuration,
       _config.targetCommitteeSize
     )
   {
-    rollupStore.epochProofVerifier = new MockVerifier();
     FEE_JUICE_PORTAL = _fpcJuicePortal;
     REWARD_DISTRIBUTOR = _rewardDistributor;
     ASSET = _fpcJuicePortal.UNDERLYING();
@@ -125,14 +127,16 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, Leonidas, IRollup, ITes
     );
     INBOX = IInbox(address(new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT)));
     OUTBOX = IOutbox(address(new Outbox(address(this))));
-    rollupStore.vkTreeRoot = _vkTreeRoot;
-    rollupStore.protocolContractTreeRoot = _protocolContractTreeRoot;
     VERSION = 1;
     L1_BLOCK_AT_GENESIS = block.number;
     CLAIM_DURATION_IN_L2_SLOTS = _config.aztecEpochProofClaimWindowInL2Slots;
 
     IS_FOUNDRY_TEST = VM_ADDRESS.code.length > 0;
 
+    rollupStore.epochProofVerifier = new MockVerifier();
+    rollupStore.vkTreeRoot = _vkTreeRoot;
+    rollupStore.protocolContractTreeRoot = _protocolContractTreeRoot;
+
     // Genesis block
     rollupStore.blocks[0] = BlockLog({
       feeHeader: FeeHeader({
diff --git a/l1-contracts/src/core/interfaces/ISlasher.sol b/l1-contracts/src/core/interfaces/ISlasher.sol
new file mode 100644
index 000000000000..6ad8c6957191
--- /dev/null
+++ b/l1-contracts/src/core/interfaces/ISlasher.sol
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 Aztec Labs.
+pragma solidity >=0.8.27;
+
+import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
+
+interface ISlasher {
+  function slash(IPayload _payload) external returns (bool);
+}
diff --git a/l1-contracts/src/core/staking/Slasher.sol b/l1-contracts/src/core/staking/Slasher.sol
new file mode 100644
index 000000000000..39e44791ff2e
--- /dev/null
+++ b/l1-contracts/src/core/staking/Slasher.sol
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 Aztec Labs.
+pragma solidity >=0.8.27;
+
+import {ISlasher} from "@aztec/core/interfaces/ISlasher.sol";
+import {SlashingProposer} from "@aztec/core/staking/SlashingProposer.sol";
+import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
+
+contract Slasher is ISlasher {
+  SlashingProposer public immutable PROPOSER;
+
+  event SlashFailed(address target, bytes data, bytes returnData);
+
+  error Slasher__CallerNotProposer(address caller, address proposer); // 0x44c1f74f
+
+  constructor(uint256 _n, uint256 _m) {
+    PROPOSER = new SlashingProposer(msg.sender, this, _n, _m);
+  }
+
+  function slash(IPayload _payload) external override(ISlasher) returns (bool) {
+    require(
+      msg.sender == address(PROPOSER), Slasher__CallerNotProposer(msg.sender, address(PROPOSER))
+    );
+
+    IPayload.Action[] memory actions = _payload.getActions();
+
+    for (uint256 i = 0; i < actions.length; i++) {
+      // Allow failure of individual calls but emit the failure!
+      (bool success, bytes memory returnData) = actions[i].target.call(actions[i].data);
+      if (!success) {
+        emit SlashFailed(actions[i].target, actions[i].data, returnData);
+      }
+    }
+
+    return true;
+  }
+}
diff --git a/l1-contracts/src/core/staking/SlashingProposer.sol b/l1-contracts/src/core/staking/SlashingProposer.sol
new file mode 100644
index 000000000000..dfd445af9379
--- /dev/null
+++ b/l1-contracts/src/core/staking/SlashingProposer.sol
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 Aztec Labs.
+pragma solidity >=0.8.27;
+
+import {ISlasher} from "@aztec/core/interfaces/ISlasher.sol";
+import {IGovernanceProposer} from "@aztec/governance/interfaces/IGovernanceProposer.sol";
+import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
+import {EmpireBase} from "@aztec/governance/proposer/EmpireBase.sol";
+
+/**
+ * @notice  A SlashingProposer implementation following the empire model
+ */
+contract SlashingProposer is IGovernanceProposer, EmpireBase {
+  address public immutable INSTANCE;
+  ISlasher public immutable SLASHER;
+
+  constructor(address _instance, ISlasher _slasher, uint256 _slashingQuorum, uint256 _roundSize)
+    EmpireBase(_slashingQuorum, _roundSize)
+  {
+    INSTANCE = _instance;
+    SLASHER = _slasher;
+  }
+
+  function getExecutor() public view override(EmpireBase, IGovernanceProposer) returns (address) {
+    return address(SLASHER);
+  }
+
+  function getInstance() public view override(EmpireBase, IGovernanceProposer) returns (address) {
+    return INSTANCE;
+  }
+
+  function _execute(IPayload _proposal) internal override(EmpireBase) returns (bool) {
+    return SLASHER.slash(_proposal);
+  }
+}
diff --git a/l1-contracts/src/core/staking/Staking.sol b/l1-contracts/src/core/staking/Staking.sol
index 0d75e74e1c1a..5e928f64c568 100644
--- a/l1-contracts/src/core/staking/Staking.sol
+++ b/l1-contracts/src/core/staking/Staking.sol
@@ -12,6 +12,7 @@ import {
 } from "@aztec/core/interfaces/IStaking.sol";
 import {Errors} from "@aztec/core/libraries/Errors.sol";
 import {Timestamp} from "@aztec/core/libraries/TimeMath.sol";
+import {Slasher} from "@aztec/core/staking/Slasher.sol";
 import {IERC20} from "@oz/token/ERC20/IERC20.sol";
 import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";
 import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol";
@@ -23,14 +24,19 @@ contract Staking is IStaking {
   // Constant pulled out of the ass
   Timestamp public constant EXIT_DELAY = Timestamp.wrap(60 * 60 * 24);
 
-  address public immutable SLASHER;
+  Slasher public immutable SLASHER;
   IERC20 public immutable STAKING_ASSET;
   uint256 public immutable MINIMUM_STAKE;
 
   StakingStorage internal stakingStore;
 
-  constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake) {
-    SLASHER = _slasher;
+  constructor(
+    IERC20 _stakingAsset,
+    uint256 _minimumStake,
+    uint256 _slashingQuorum,
+    uint256 _roundSize
+  ) {
+    SLASHER = new Slasher(_slashingQuorum, _roundSize);
     STAKING_ASSET = _stakingAsset;
     MINIMUM_STAKE = _minimumStake;
   }
@@ -57,7 +63,9 @@ contract Staking is IStaking {
   }
 
   function slash(address _attester, uint256 _amount) external override(IStaking) {
-    require(msg.sender == SLASHER, Errors.Staking__NotSlasher(SLASHER, msg.sender));
+    require(
+      msg.sender == address(SLASHER), Errors.Staking__NotSlasher(address(SLASHER), msg.sender)
+    );
 
     ValidatorInfo storage validator = stakingStore.info[_attester];
     require(validator.status != Status.NONE, Errors.Staking__NoOneToSlash(_attester));
diff --git a/l1-contracts/src/governance/CoinIssuer.sol b/l1-contracts/src/governance/CoinIssuer.sol
index 37ac8f18b4df..33a0c06df0ed 100644
--- a/l1-contracts/src/governance/CoinIssuer.sol
+++ b/l1-contracts/src/governance/CoinIssuer.sol
@@ -33,7 +33,7 @@ contract CoinIssuer is ICoinIssuer, Ownable {
    */
   function mint(address _to, uint256 _amount) external override(ICoinIssuer) onlyOwner {
     uint256 maxMint = mintAvailable();
-    require(_amount <= maxMint, Errors.CoinIssuer__InssuficientMintAvailable(maxMint, _amount));
+    require(_amount <= maxMint, Errors.CoinIssuer__InsufficientMintAvailable(maxMint, _amount));
     timeOfLastMint = block.timestamp;
     ASSET.mint(_to, _amount);
   }
diff --git a/l1-contracts/src/governance/interfaces/IGovernanceProposer.sol b/l1-contracts/src/governance/interfaces/IGovernanceProposer.sol
index 7539446a1de9..dc63068a4819 100644
--- a/l1-contracts/src/governance/interfaces/IGovernanceProposer.sol
+++ b/l1-contracts/src/governance/interfaces/IGovernanceProposer.sol
@@ -3,19 +3,19 @@
 pragma solidity >=0.8.27;
 
 import {Slot} from "@aztec/core/libraries/TimeMath.sol";
-import {IGovernance} from "@aztec/governance/interfaces/IGovernance.sol";
 import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
 
 interface IGovernanceProposer {
   event VoteCast(IPayload indexed proposal, uint256 indexed round, address indexed voter);
   event ProposalPushed(IPayload indexed proposal, uint256 indexed round);
 
-  function vote(IPayload _proposa) external returns (bool);
+  function vote(IPayload _proposal) external returns (bool);
   function pushProposal(uint256 _roundNumber) external returns (bool);
   function yeaCount(address _instance, uint256 _round, IPayload _proposal)
     external
     view
     returns (uint256);
   function computeRound(Slot _slot) external view returns (uint256);
-  function getGovernance() external view returns (IGovernance);
+  function getInstance() external view returns (address);
+  function getExecutor() external view returns (address);
 }
diff --git a/l1-contracts/src/governance/libraries/Errors.sol b/l1-contracts/src/governance/libraries/Errors.sol
index fb835660287d..263749a198cd 100644
--- a/l1-contracts/src/governance/libraries/Errors.sol
+++ b/l1-contracts/src/governance/libraries/Errors.sol
@@ -45,20 +45,20 @@ library Errors {
   error Governance__ProposalLib__ZeroYeaVotesNeeded();
   error Governance__ProposalLib__MoreYeaVoteThanExistNeeded();
 
-  error GovernanceProposer__CanOnlyPushProposalInPast(); // 0x49fdf611"
-  error GovernanceProposer__FailedToPropose(IPayload proposal); // 0x6ca2a2ed
-  error GovernanceProposer__InstanceHaveNoCode(address instance); // 0x20a3b441
-  error GovernanceProposer__InsufficientVotes(); // 0xba1e05ef
+  error GovernanceProposer__CanOnlyPushProposalInPast(); // 0x84a5b5ae
+  error GovernanceProposer__FailedToPropose(IPayload proposal); // 0x8d94fbfc
+  error GovernanceProposer__InstanceHaveNoCode(address instance); // 0x5fa92625
+  error GovernanceProposer__InsufficientVotes(uint256 votesCast, uint256 votesNeeded); // 0xd4ad89c2
   error GovernanceProposer__InvalidNAndMValues(uint256 n, uint256 m); // 0x520d9704
   error GovernanceProposer__NCannotBeLargerTHanM(uint256 n, uint256 m); // 0x2fdfc063
   error GovernanceProposer__OnlyProposerCanVote(address caller, address proposer); // 0xba27df38
   error GovernanceProposer__ProposalAlreadyExecuted(uint256 roundNumber); // 0x7aeacb17
-  error GovernanceProposer__ProposalCannotBeAddressZero(); // 0xdb3e4b6e
-  error GovernanceProposer__ProposalHaveNoCode(IPayload proposal); // 0xdce0615b
-  error GovernanceProposer__ProposalTooOld(uint256 roundNumber, uint256 currentRoundNumber); //0x02283b1a
-  error GovernanceProposer__VoteAlreadyCastForSlot(Slot slot); //0xc2201452
+  error GovernanceProposer__ProposalCannotBeAddressZero(); // 0x16ac1942
+  error GovernanceProposer__ProposalHaveNoCode(IPayload proposal); // 0xb69440a1
+  error GovernanceProposer__ProposalTooOld(uint256 roundNumber, uint256 currentRoundNumber); // 0xc3d7aa4f
+  error GovernanceProposer__VoteAlreadyCastForSlot(Slot slot); // 0x3a6150ca
 
-  error CoinIssuer__InssuficientMintAvailable(uint256 available, uint256 needed); // 0xf268b931
+  error CoinIssuer__InsufficientMintAvailable(uint256 available, uint256 needed); // 0xa1cc8799
 
   error Registry__RollupAlreadyRegistered(address rollup); // 0x3c34eabf
   error Registry__RollupNotRegistered(address rollup); // 0xa1fee4cf
diff --git a/l1-contracts/src/governance/GovernanceProposer.sol b/l1-contracts/src/governance/proposer/EmpireBase.sol
similarity index 86%
rename from l1-contracts/src/governance/GovernanceProposer.sol
rename to l1-contracts/src/governance/proposer/EmpireBase.sol
index 7e665ee3aa82..70a43c18b0c9 100644
--- a/l1-contracts/src/governance/GovernanceProposer.sol
+++ b/l1-contracts/src/governance/proposer/EmpireBase.sol
@@ -4,10 +4,8 @@ pragma solidity >=0.8.27;
 
 import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol";
 import {Slot, SlotLib} from "@aztec/core/libraries/TimeMath.sol";
-import {IGovernance} from "@aztec/governance/interfaces/IGovernance.sol";
 import {IGovernanceProposer} from "@aztec/governance/interfaces/IGovernanceProposer.sol";
 import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
-import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";
 import {Errors} from "@aztec/governance/libraries/Errors.sol";
 
 /**
@@ -17,7 +15,7 @@ import {Errors} from "@aztec/governance/libraries/Errors.sol";
  *          This also means that the implementation here will need to be "updated" if
  *          the interfaces of the sequencer selection changes, for example going optimistic.
  */
-contract GovernanceProposer is IGovernanceProposer {
+abstract contract EmpireBase is IGovernanceProposer {
   using SlotLib for Slot;
 
   struct RoundAccounting {
@@ -29,14 +27,12 @@ contract GovernanceProposer is IGovernanceProposer {
 
   uint256 public constant LIFETIME_IN_ROUNDS = 5;
 
-  IRegistry public immutable REGISTRY;
   uint256 public immutable N;
   uint256 public immutable M;
 
   mapping(address instance => mapping(uint256 roundNumber => RoundAccounting)) public rounds;
 
-  constructor(IRegistry _registry, uint256 _n, uint256 _m) {
-    REGISTRY = _registry;
+  constructor(uint256 _n, uint256 _m) {
     N = _n;
     M = _m;
 
@@ -57,11 +53,12 @@ contract GovernanceProposer is IGovernanceProposer {
    * @return True if executed successfully, false otherwise
    */
   function vote(IPayload _proposal) external override(IGovernanceProposer) returns (bool) {
-    require(
+    // For now, skipping this as the check is not really needed but there were not full agreement
+    /*require(
       address(_proposal).code.length > 0, Errors.GovernanceProposer__ProposalHaveNoCode(_proposal)
-    );
+    );*/
 
-    address instance = REGISTRY.getRollup();
+    address instance = getInstance();
     require(instance.code.length > 0, Errors.GovernanceProposer__InstanceHaveNoCode(instance));
 
     ILeonidas selection = ILeonidas(instance);
@@ -102,7 +99,7 @@ contract GovernanceProposer is IGovernanceProposer {
    */
   function pushProposal(uint256 _roundNumber) external override(IGovernanceProposer) returns (bool) {
     // Need to ensure that the round is not active.
-    address instance = REGISTRY.getRollup();
+    address instance = getInstance();
     require(instance.code.length > 0, Errors.GovernanceProposer__InstanceHaveNoCode(instance));
 
     ILeonidas selection = ILeonidas(instance);
@@ -120,16 +117,14 @@ contract GovernanceProposer is IGovernanceProposer {
     require(
       round.leader != IPayload(address(0)), Errors.GovernanceProposer__ProposalCannotBeAddressZero()
     );
-    require(round.yeaCount[round.leader] >= N, Errors.GovernanceProposer__InsufficientVotes());
+    uint256 votesCast = round.yeaCount[round.leader];
+    require(votesCast >= N, Errors.GovernanceProposer__InsufficientVotes(votesCast, N));
 
     round.executed = true;
 
     emit ProposalPushed(round.leader, _roundNumber);
 
-    require(
-      getGovernance().propose(round.leader),
-      Errors.GovernanceProposer__FailedToPropose(round.leader)
-    );
+    require(_execute(round.leader), Errors.GovernanceProposer__FailedToPropose(round.leader));
     return true;
   }
 
@@ -162,7 +157,8 @@ contract GovernanceProposer is IGovernanceProposer {
     return _slot.unwrap() / M;
   }
 
-  function getGovernance() public view override(IGovernanceProposer) returns (IGovernance) {
-    return IGovernance(REGISTRY.getGovernance());
-  }
+  // Virtual functions
+  function getInstance() public view virtual override(IGovernanceProposer) returns (address);
+  function getExecutor() public view virtual override(IGovernanceProposer) returns (address);
+  function _execute(IPayload _proposal) internal virtual returns (bool);
 }
diff --git a/l1-contracts/src/governance/proposer/GovernanceProposer.sol b/l1-contracts/src/governance/proposer/GovernanceProposer.sol
new file mode 100644
index 000000000000..734a42172e53
--- /dev/null
+++ b/l1-contracts/src/governance/proposer/GovernanceProposer.sol
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 Aztec Labs.
+pragma solidity >=0.8.27;
+
+import {IGovernance} from "@aztec/governance/interfaces/IGovernance.sol";
+import {IGovernanceProposer} from "@aztec/governance/interfaces/IGovernanceProposer.sol";
+import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
+import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";
+import {EmpireBase} from "./EmpireBase.sol";
+
+/**
+ * @notice  A GovernanceProposer implementation following the empire model
+ *          Beware that while governance generally do not care about the implementation
+ *          this implementation will since it is dependent on the sequencer selection.
+ *          This also means that the implementation here will need to be "updated" if
+ *          the interfaces of the sequencer selection changes, for example going optimistic.
+ */
+contract GovernanceProposer is IGovernanceProposer, EmpireBase {
+  IRegistry public immutable REGISTRY;
+
+  constructor(IRegistry _registry, uint256 _n, uint256 _m) EmpireBase(_n, _m) {
+    REGISTRY = _registry;
+  }
+
+  function getExecutor() public view override(EmpireBase, IGovernanceProposer) returns (address) {
+    return REGISTRY.getGovernance();
+  }
+
+  function getInstance() public view override(EmpireBase, IGovernanceProposer) returns (address) {
+    return REGISTRY.getRollup();
+  }
+
+  function _execute(IPayload _proposal) internal override(EmpireBase) returns (bool) {
+    return IGovernance(getExecutor()).propose(_proposal);
+  }
+}
diff --git a/l1-contracts/src/periphery/SlashFactory.sol b/l1-contracts/src/periphery/SlashFactory.sol
new file mode 100644
index 000000000000..14904c1f62e0
--- /dev/null
+++ b/l1-contracts/src/periphery/SlashFactory.sol
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 Aztec Labs.
+pragma solidity >=0.8.27;
+
+import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol";
+import {Epoch} from "@aztec/core/libraries/TimeMath.sol";
+import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
+import {ISlashFactory} from "./interfaces/ISlashFactory.sol";
+import {SlashPayload} from "./SlashPayload.sol";
+
+contract SlashFactory is ISlashFactory {
+  ILeonidas public immutable LEONIDAS;
+
+  constructor(ILeonidas _leonidas) {
+    LEONIDAS = _leonidas;
+  }
+
+  function createSlashPayload(Epoch _epoch, uint256 _amount)
+    external
+    override(ISlashFactory)
+    returns (IPayload)
+  {
+    (address predictedAddress, bool isDeployed) = getAddressAndIsDeployed(_epoch, _amount);
+
+    if (isDeployed) {
+      return IPayload(predictedAddress);
+    }
+
+    SlashPayload payload =
+      new SlashPayload{salt: bytes32(Epoch.unwrap(_epoch))}(_epoch, LEONIDAS, _amount);
+
+    emit SlashPayloadCreated(address(payload), _epoch, _amount);
+    return IPayload(address(payload));
+  }
+
+  function getAddressAndIsDeployed(Epoch _epoch, uint256 _amount)
+    public
+    view
+    override(ISlashFactory)
+    returns (address, bool)
+  {
+    address predictedAddress = _computeSlashPayloadAddress(_epoch, _amount);
+    bool isDeployed = predictedAddress.code.length > 0;
+    return (predictedAddress, isDeployed);
+  }
+
+  function _computeSlashPayloadAddress(Epoch _epoch, uint256 _amount)
+    internal
+    view
+    returns (address)
+  {
+    bytes32 salt = bytes32(Epoch.unwrap(_epoch));
+    return address(
+      uint160(
+        uint256(
+          keccak256(
+            abi.encodePacked(
+              bytes1(0xff),
+              address(this),
+              salt,
+              keccak256(
+                abi.encodePacked(
+                  type(SlashPayload).creationCode, abi.encode(_epoch, LEONIDAS, _amount)
+                )
+              )
+            )
+          )
+        )
+      )
+    );
+  }
+}
diff --git a/l1-contracts/src/periphery/SlashPayload.sol b/l1-contracts/src/periphery/SlashPayload.sol
new file mode 100644
index 000000000000..4410cfe0ae9d
--- /dev/null
+++ b/l1-contracts/src/periphery/SlashPayload.sol
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 Aztec Labs.
+pragma solidity >=0.8.27;
+
+import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol";
+import {IStaking} from "@aztec/core/interfaces/IStaking.sol";
+import {Epoch} from "@aztec/core/libraries/TimeMath.sol";
+import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
+
+/**
+ * @notice The simplest payload that you can find, slash all attesters for an epoch.
+ */
+contract SlashPayload is IPayload {
+  Epoch public immutable EPOCH;
+  ILeonidas public immutable LEONIDAS;
+  uint256 public immutable AMOUNT;
+
+  constructor(Epoch _epoch, ILeonidas _leonidas, uint256 _amount) {
+    EPOCH = _epoch;
+    LEONIDAS = _leonidas;
+    AMOUNT = _amount;
+  }
+
+  function getActions() external view override(IPayload) returns (IPayload.Action[] memory) {
+    address[] memory attesters = ILeonidas(LEONIDAS).getEpochCommittee(EPOCH);
+    IPayload.Action[] memory actions = new IPayload.Action[](attesters.length);
+
+    for (uint256 i = 0; i < attesters.length; i++) {
+      actions[i] = IPayload.Action({
+        target: address(LEONIDAS),
+        data: abi.encodeWithSelector(IStaking.slash.selector, attesters[i], AMOUNT)
+      });
+    }
+
+    return actions;
+  }
+}
diff --git a/l1-contracts/src/periphery/interfaces/ISlashFactory.sol b/l1-contracts/src/periphery/interfaces/ISlashFactory.sol
new file mode 100644
index 000000000000..7300cfbba846
--- /dev/null
+++ b/l1-contracts/src/periphery/interfaces/ISlashFactory.sol
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 Aztec Labs.
+pragma solidity >=0.8.27;
+
+import {Epoch} from "@aztec/core/libraries/TimeMath.sol";
+import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
+
+interface ISlashFactory {
+  event SlashPayloadCreated(
+    address indexed payloadAddress, Epoch indexed epoch, uint256 indexed amount
+  );
+
+  function createSlashPayload(Epoch _epoch, uint256 _amount) external returns (IPayload);
+  function getAddressAndIsDeployed(Epoch _epoch, uint256 _amount)
+    external
+    view
+    returns (address, bool);
+}
diff --git a/l1-contracts/terraform/main.tf b/l1-contracts/terraform/main.tf
index d619a827877f..a9b9b4a3faa0 100644
--- a/l1-contracts/terraform/main.tf
+++ b/l1-contracts/terraform/main.tf
@@ -109,4 +109,13 @@ variable "GOVERNANCE_CONTRACT_ADDRESS" {
 
 output "GOVERNANCE_CONTRACT_ADDRESS" {
   value = var.GOVERNANCE_CONTRACT_ADDRESS
-}
\ No newline at end of file
+}
+
+variable "SLASH_FACTORY_CONTRACT_ADDRESS" {
+  type    = string
+  default = ""
+}
+
+output "SLASH_FACTORY_CONTRACT_ADDRESS" {
+  value = var.SLASH_FACTORY_CONTRACT_ADDRESS
+}
diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol
index 504db52ae570..b5552b0036df 100644
--- a/l1-contracts/test/Rollup.t.sol
+++ b/l1-contracts/test/Rollup.t.sol
@@ -77,9 +77,10 @@ contract RollupTest is DecoderBase, TimeFns {
       testERC20 = new TestERC20("test", "TEST", address(this));
 
       leo = new Leonidas(
-        address(1),
         testERC20,
         TestConstants.AZTEC_MINIMUM_STAKE,
+        TestConstants.AZTEC_SLASHING_QUORUM,
+        TestConstants.AZTEC_SLASHING_ROUND_SIZE,
         TestConstants.AZTEC_SLOT_DURATION,
         TestConstants.AZTEC_EPOCH_DURATION,
         TestConstants.AZTEC_TARGET_COMMITTEE_SIZE
diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol
index 8331d66d7eed..f6282f33b211 100644
--- a/l1-contracts/test/fees/FeeRollup.t.sol
+++ b/l1-contracts/test/fees/FeeRollup.t.sol
@@ -131,7 +131,9 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase {
         aztecEpochDuration: EPOCH_DURATION,
         targetCommitteeSize: 48,
         aztecEpochProofClaimWindowInL2Slots: 16,
-        minimumStake: 100 ether
+        minimumStake: TestConstants.AZTEC_MINIMUM_STAKE,
+        slashingQuorum: TestConstants.AZTEC_SLASHING_QUORUM,
+        slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE
       })
     );
     fakeCanonical.setCanonicalRollup(address(rollup));
diff --git a/l1-contracts/test/governance/coin-issuer/mint.t.sol b/l1-contracts/test/governance/coin-issuer/mint.t.sol
index 29304fbb5893..229d2f52d91d 100644
--- a/l1-contracts/test/governance/coin-issuer/mint.t.sol
+++ b/l1-contracts/test/governance/coin-issuer/mint.t.sol
@@ -35,7 +35,7 @@ contract MintTest is CoinIssuerBase {
     // it reverts
     uint256 amount = bound(_amount, maxMint + 1, type(uint256).max);
     vm.expectRevert(
-      abi.encodeWithSelector(Errors.CoinIssuer__InssuficientMintAvailable.selector, maxMint, amount)
+      abi.encodeWithSelector(Errors.CoinIssuer__InsufficientMintAvailable.selector, maxMint, amount)
     );
     nom.mint(address(0xdead), amount);
   }
diff --git a/l1-contracts/test/governance/governance-proposer/Base.t.sol b/l1-contracts/test/governance/governance-proposer/Base.t.sol
index 1f7209114380..bbfd7b548287 100644
--- a/l1-contracts/test/governance/governance-proposer/Base.t.sol
+++ b/l1-contracts/test/governance/governance-proposer/Base.t.sol
@@ -4,7 +4,7 @@ pragma solidity >=0.8.27;
 import {Test} from "forge-std/Test.sol";
 
 import {Registry} from "@aztec/governance/Registry.sol";
-import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol";
+import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol";
 
 import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
 
diff --git a/l1-contracts/test/governance/governance-proposer/constructor.t.sol b/l1-contracts/test/governance/governance-proposer/constructor.t.sol
index f32b8aefa59e..327ef727701d 100644
--- a/l1-contracts/test/governance/governance-proposer/constructor.t.sol
+++ b/l1-contracts/test/governance/governance-proposer/constructor.t.sol
@@ -2,12 +2,22 @@
 pragma solidity >=0.8.27;
 
 import {Test} from "forge-std/Test.sol";
-import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol";
+import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol";
 import {Errors} from "@aztec/governance/libraries/Errors.sol";
 import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";
 
+contract FakeRegistry {
+  function getGovernance() external pure returns (address) {
+    return address(0x01);
+  }
+
+  function getRollup() external pure returns (address) {
+    return address(0x02);
+  }
+}
+
 contract ConstructorTest is Test {
-  IRegistry internal constant REGISTRY = IRegistry(address(0x02));
+  IRegistry internal REGISTRY = IRegistry(address(new FakeRegistry()));
 
   function test_WhenNIsLessThanOrEqualHalfOfM(uint256 _n, uint256 _m) external {
     // it revert
@@ -42,5 +52,7 @@ contract ConstructorTest is Test {
     assertEq(address(g.REGISTRY()), address(REGISTRY));
     assertEq(g.N(), n);
     assertEq(g.M(), m);
+    assertEq(g.getExecutor(), address(REGISTRY.getGovernance()), "executor");
+    assertEq(g.getInstance(), address(REGISTRY.getRollup()), "instance");
   }
 }
diff --git a/l1-contracts/test/governance/governance-proposer/pushProposal.t.sol b/l1-contracts/test/governance/governance-proposer/pushProposal.t.sol
index 26de9a2f3432..f64ac8bc75dc 100644
--- a/l1-contracts/test/governance/governance-proposer/pushProposal.t.sol
+++ b/l1-contracts/test/governance/governance-proposer/pushProposal.t.sol
@@ -30,7 +30,7 @@ contract PushProposalTest is GovernanceProposerBase {
   }
 
   modifier givenCanonicalInstanceHoldCode() {
-    leonidas = new Leonidas(address(this));
+    leonidas = new Leonidas();
     vm.prank(registry.getGovernance());
     registry.upgrade(address(leonidas));
 
@@ -164,12 +164,16 @@ contract PushProposalTest is GovernanceProposerBase {
     vm.prank(proposer);
     governanceProposer.vote(proposal);
 
+    uint256 votesNeeded = governanceProposer.N();
+
     vm.warp(
       Timestamp.unwrap(
         leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(governanceProposer.M()))
       )
     );
-    vm.expectRevert(abi.encodeWithSelector(Errors.GovernanceProposer__InsufficientVotes.selector));
+    vm.expectRevert(
+      abi.encodeWithSelector(Errors.GovernanceProposer__InsufficientVotes.selector, 1, votesNeeded)
+    );
     governanceProposer.pushProposal(1);
   }
 
@@ -204,7 +208,7 @@ contract PushProposalTest is GovernanceProposerBase {
     // it revert
 
     // When using a new registry we change the governanceProposer's interpetation of time :O
-    Leonidas freshInstance = new Leonidas(address(this));
+    Leonidas freshInstance = new Leonidas();
     vm.prank(registry.getGovernance());
     registry.upgrade(address(freshInstance));
 
diff --git a/l1-contracts/test/governance/governance-proposer/vote.t.sol b/l1-contracts/test/governance/governance-proposer/vote.t.sol
index f78f9f009e04..91c283639122 100644
--- a/l1-contracts/test/governance/governance-proposer/vote.t.sol
+++ b/l1-contracts/test/governance/governance-proposer/vote.t.sol
@@ -15,7 +15,8 @@ contract VoteTest is GovernanceProposerBase {
   address internal proposer = address(0);
   Leonidas internal leonidas;
 
-  function test_WhenProposalHoldNoCode() external {
+  // Skipping this test since the it matches the for now skipped check in `EmpireBase::vote`
+  function skip__test_WhenProposalHoldNoCode() external {
     // it revert
     vm.expectRevert(
       abi.encodeWithSelector(Errors.GovernanceProposer__ProposalHaveNoCode.selector, proposal)
@@ -39,7 +40,7 @@ contract VoteTest is GovernanceProposerBase {
   }
 
   modifier givenCanonicalRollupHoldCode() {
-    leonidas = new Leonidas(address(this));
+    leonidas = new Leonidas();
     vm.prank(registry.getGovernance());
     registry.upgrade(address(leonidas));
 
@@ -138,7 +139,7 @@ contract VoteTest is GovernanceProposerBase {
     uint256 leonidasRound = governanceProposer.computeRound(leonidasSlot);
     uint256 yeaBefore = governanceProposer.yeaCount(address(leonidas), leonidasRound, proposal);
 
-    Leonidas freshInstance = new Leonidas(address(this));
+    Leonidas freshInstance = new Leonidas();
     vm.prank(registry.getGovernance());
     registry.upgrade(address(freshInstance));
 
diff --git a/l1-contracts/test/governance/governance/base.t.sol b/l1-contracts/test/governance/governance/base.t.sol
index cc5a9878a068..05a125f10ff2 100644
--- a/l1-contracts/test/governance/governance/base.t.sol
+++ b/l1-contracts/test/governance/governance/base.t.sol
@@ -3,7 +3,7 @@ pragma solidity >=0.8.27;
 
 import {TestBase} from "@test/base/Base.sol";
 import {Governance} from "@aztec/governance/Governance.sol";
-import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol";
+import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol";
 import {Registry} from "@aztec/governance/Registry.sol";
 import {DataStructures} from "@aztec/governance/libraries/DataStructures.sol";
 import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol";
diff --git a/l1-contracts/test/governance/scenario/NewGovernanceProposerPayload.sol b/l1-contracts/test/governance/scenario/NewGovernanceProposerPayload.sol
index 613dc7006b47..a4cb726dc2e9 100644
--- a/l1-contracts/test/governance/scenario/NewGovernanceProposerPayload.sol
+++ b/l1-contracts/test/governance/scenario/NewGovernanceProposerPayload.sol
@@ -4,7 +4,7 @@ pragma solidity >=0.8.27;
 import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
 import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";
 import {Governance} from "@aztec/governance/Governance.sol";
-import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol";
+import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol";
 
 /**
  * @title NewGovernanceProposerPayload
diff --git a/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol b/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol
index 8504653da175..0a403de63f17 100644
--- a/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol
+++ b/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol
@@ -6,7 +6,7 @@ import {TestBase} from "@test/base/Base.sol";
 import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol";
 import {Rollup} from "../../harnesses/Rollup.sol";
 import {Governance} from "@aztec/governance/Governance.sol";
-import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol";
+import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol";
 import {Registry} from "@aztec/governance/Registry.sol";
 import {DataStructures} from "@aztec/governance/libraries/DataStructures.sol";
 import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol";
diff --git a/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol b/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol
new file mode 100644
index 000000000000..95c3010ab2a2
--- /dev/null
+++ b/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {TestBase} from "@test/base/Base.sol";
+
+import {Errors} from "@aztec/core/libraries/Errors.sol";
+import {Registry} from "@aztec/governance/Registry.sol";
+import {Rollup, Config} from "@aztec/core/Rollup.sol";
+import {TestERC20} from "@aztec/mock/TestERC20.sol";
+import {MockFeeJuicePortal} from "@aztec/mock/MockFeeJuicePortal.sol";
+import {TestConstants} from "../../../harnesses/TestConstants.sol";
+import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol";
+
+import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol";
+
+import {SlashFactory} from "@aztec/periphery/SlashFactory.sol";
+import {Slasher, IPayload} from "@aztec/core/staking/Slasher.sol";
+import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol";
+import {Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol";
+
+import {Errors} from "@aztec/core/libraries/Errors.sol";
+import {Timestamp} from "@aztec/core/libraries/TimeMath.sol";
+
+import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol";
+import {SlashingProposer} from "@aztec/core/staking/SlashingProposer.sol";
+
+import {Slot, SlotLib, Epoch} from "@aztec/core/libraries/TimeMath.sol";
+
+contract SlashingScenario is TestBase {
+  using SlotLib for Slot;
+
+  TestERC20 internal testERC20;
+  RewardDistributor internal rewardDistributor;
+  Rollup internal rollup;
+  Slasher internal slasher;
+  SlashFactory internal slashFactory;
+  SlashingProposer internal slashingProposer;
+
+  function test_Slashing() public {
+    uint256 validatorCount = 4;
+
+    CheatDepositArgs[] memory initialValidators = new CheatDepositArgs[](validatorCount);
+
+    for (uint256 i = 1; i < validatorCount + 1; i++) {
+      uint256 attesterPrivateKey = uint256(keccak256(abi.encode("attester", i)));
+      address attester = vm.addr(attesterPrivateKey);
+      uint256 proposerPrivateKey = uint256(keccak256(abi.encode("proposer", i)));
+      address proposer = vm.addr(proposerPrivateKey);
+
+      initialValidators[i - 1] = CheatDepositArgs({
+        attester: attester,
+        proposer: proposer,
+        withdrawer: address(this),
+        amount: TestConstants.AZTEC_MINIMUM_STAKE
+      });
+    }
+
+    testERC20 = new TestERC20("test", "TEST", address(this));
+    Registry registry = new Registry(address(this));
+    rewardDistributor = new RewardDistributor(testERC20, registry, address(this));
+    rollup = new Rollup({
+      _fpcJuicePortal: new MockFeeJuicePortal(),
+      _rewardDistributor: rewardDistributor,
+      _stakingAsset: testERC20,
+      _vkTreeRoot: bytes32(0),
+      _protocolContractTreeRoot: bytes32(0),
+      _ares: address(this),
+      _config: Config({
+        aztecSlotDuration: TestConstants.AZTEC_SLOT_DURATION,
+        aztecEpochDuration: TestConstants.AZTEC_EPOCH_DURATION,
+        targetCommitteeSize: TestConstants.AZTEC_TARGET_COMMITTEE_SIZE,
+        aztecEpochProofClaimWindowInL2Slots: TestConstants.AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS,
+        minimumStake: TestConstants.AZTEC_MINIMUM_STAKE,
+        slashingQuorum: TestConstants.AZTEC_SLASHING_QUORUM,
+        slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE
+      })
+    });
+    slasher = rollup.SLASHER();
+    slashingProposer = slasher.PROPOSER();
+    slashFactory = new SlashFactory(ILeonidas(address(rollup)));
+
+    testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * validatorCount);
+    testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * validatorCount);
+    rollup.cheat__InitialiseValidatorSet(initialValidators);
+
+    // Lets make a proposal to slash!
+
+    uint256 slashAmount = 10e18;
+    IPayload payload = slashFactory.createSlashPayload(Epoch.wrap(0), slashAmount);
+
+    // Cast a bunch of votes
+    vm.warp(Timestamp.unwrap(rollup.getTimestampForSlot(Slot.wrap(1))));
+
+    for (uint256 i = 0; i < 10; i++) {
+      address proposer = rollup.getCurrentProposer();
+      vm.prank(proposer);
+      slashingProposer.vote(payload);
+      vm.warp(Timestamp.unwrap(rollup.getTimestampForSlot(rollup.getCurrentSlot() + Slot.wrap(1))));
+    }
+
+    address[] memory attesters = rollup.getAttesters();
+    uint256[] memory stakes = new uint256[](attesters.length);
+
+    for (uint256 i = 0; i < attesters.length; i++) {
+      ValidatorInfo memory info = rollup.getInfo(attesters[i]);
+      stakes[i] = info.stake;
+      assertTrue(info.status == Status.VALIDATING, "Invalid status");
+    }
+
+    slashingProposer.pushProposal(0);
+
+    // Make sure that the slash was successful,
+    // Meaning that validators are now LIVING and have lost the slash amount
+    for (uint256 i = 0; i < attesters.length; i++) {
+      ValidatorInfo memory info = rollup.getInfo(attesters[i]);
+      uint256 stake = info.stake;
+      assertEq(stake, stakes[i] - slashAmount, "Invalid stake");
+      assertTrue(info.status == Status.LIVING, "Invalid status");
+    }
+  }
+}
diff --git a/l1-contracts/test/harnesses/Leonidas.sol b/l1-contracts/test/harnesses/Leonidas.sol
index a7c78f304b10..c52eb3015890 100644
--- a/l1-contracts/test/harnesses/Leonidas.sol
+++ b/l1-contracts/test/harnesses/Leonidas.sol
@@ -7,11 +7,12 @@ import {TestConstants} from "./TestConstants.sol";
 import {TestERC20} from "@aztec/mock/TestERC20.sol";
 
 contract Leonidas is RealLeonidas {
-  constructor(address _ares)
+  constructor()
     RealLeonidas(
-      _ares,
       new TestERC20("test", "TEST", address(this)),
       100e18,
+      TestConstants.AZTEC_SLASHING_QUORUM,
+      TestConstants.AZTEC_SLASHING_ROUND_SIZE,
       TestConstants.AZTEC_SLOT_DURATION,
       TestConstants.AZTEC_EPOCH_DURATION,
       TestConstants.AZTEC_TARGET_COMMITTEE_SIZE
diff --git a/l1-contracts/test/harnesses/Rollup.sol b/l1-contracts/test/harnesses/Rollup.sol
index 41d72b20de9f..27d55a9913e3 100644
--- a/l1-contracts/test/harnesses/Rollup.sol
+++ b/l1-contracts/test/harnesses/Rollup.sol
@@ -29,7 +29,9 @@ contract Rollup is RealRollup {
         aztecEpochDuration: TestConstants.AZTEC_EPOCH_DURATION,
         targetCommitteeSize: TestConstants.AZTEC_TARGET_COMMITTEE_SIZE,
         aztecEpochProofClaimWindowInL2Slots: TestConstants.AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS,
-        minimumStake: TestConstants.AZTEC_MINIMUM_STAKE
+        minimumStake: TestConstants.AZTEC_MINIMUM_STAKE,
+        slashingQuorum: TestConstants.AZTEC_SLASHING_QUORUM,
+        slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE
       })
     )
   {}
diff --git a/l1-contracts/test/harnesses/TestConstants.sol b/l1-contracts/test/harnesses/TestConstants.sol
index 371a2d8f594e..aad8edd6db0d 100644
--- a/l1-contracts/test/harnesses/TestConstants.sol
+++ b/l1-contracts/test/harnesses/TestConstants.sol
@@ -10,4 +10,6 @@ library TestConstants {
   uint256 internal constant AZTEC_TARGET_COMMITTEE_SIZE = 48;
   uint256 internal constant AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS = 13;
   uint256 internal constant AZTEC_MINIMUM_STAKE = 100e18;
+  uint256 internal constant AZTEC_SLASHING_QUORUM = 6;
+  uint256 internal constant AZTEC_SLASHING_ROUND_SIZE = 10;
 }
diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol
index dc5340a39aea..9f806139582e 100644
--- a/l1-contracts/test/sparta/Sparta.t.sol
+++ b/l1-contracts/test/sparta/Sparta.t.sol
@@ -12,7 +12,7 @@ import {Inbox} from "@aztec/core/messagebridge/Inbox.sol";
 import {Outbox} from "@aztec/core/messagebridge/Outbox.sol";
 import {Errors} from "@aztec/core/libraries/Errors.sol";
 import {Registry} from "@aztec/governance/Registry.sol";
-import {Rollup} from "../harnesses/Rollup.sol";
+import {Rollup, Config} from "@aztec/core/Rollup.sol";
 import {Leonidas} from "@aztec/core/Leonidas.sol";
 import {NaiveMerkle} from "../merkle/Naive.sol";
 import {MerkleTestUtil} from "../merkle/TestUtil.sol";
@@ -28,6 +28,11 @@ import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol";
 
 import {Slot, Epoch, SlotLib, EpochLib} from "@aztec/core/libraries/TimeMath.sol";
 import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol";
+
+import {SlashFactory} from "@aztec/periphery/SlashFactory.sol";
+import {Slasher, IPayload} from "@aztec/core/staking/Slasher.sol";
+import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol";
+import {Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol";
 // solhint-disable comprehensive-interface
 
 /**
@@ -45,6 +50,8 @@ contract SpartaTest is DecoderBase {
     bool shouldRevert;
   }
 
+  SlashFactory internal slashFactory;
+  Slasher internal slasher;
   Inbox internal inbox;
   Outbox internal outbox;
   Rollup internal rollup;
@@ -66,9 +73,10 @@ contract SpartaTest is DecoderBase {
     string memory _name = "mixed_block_1";
     {
       Leonidas leonidas = new Leonidas(
-        address(1),
         testERC20,
         TestConstants.AZTEC_MINIMUM_STAKE,
+        TestConstants.AZTEC_SLASHING_QUORUM,
+        TestConstants.AZTEC_SLASHING_ROUND_SIZE,
         TestConstants.AZTEC_SLOT_DURATION,
         TestConstants.AZTEC_EPOCH_DURATION,
         TestConstants.AZTEC_TARGET_COMMITTEE_SIZE
@@ -104,9 +112,25 @@ contract SpartaTest is DecoderBase {
     testERC20 = new TestERC20("test", "TEST", address(this));
     Registry registry = new Registry(address(this));
     rewardDistributor = new RewardDistributor(testERC20, registry, address(this));
-    rollup = new Rollup(
-      new MockFeeJuicePortal(), rewardDistributor, testERC20, bytes32(0), bytes32(0), address(this)
-    );
+    rollup = new Rollup({
+      _fpcJuicePortal: new MockFeeJuicePortal(),
+      _rewardDistributor: rewardDistributor,
+      _stakingAsset: testERC20,
+      _vkTreeRoot: bytes32(0),
+      _protocolContractTreeRoot: bytes32(0),
+      _ares: address(this),
+      _config: Config({
+        aztecSlotDuration: TestConstants.AZTEC_SLOT_DURATION,
+        aztecEpochDuration: TestConstants.AZTEC_EPOCH_DURATION,
+        targetCommitteeSize: TestConstants.AZTEC_TARGET_COMMITTEE_SIZE,
+        aztecEpochProofClaimWindowInL2Slots: TestConstants.AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS,
+        minimumStake: TestConstants.AZTEC_MINIMUM_STAKE,
+        slashingQuorum: TestConstants.AZTEC_SLASHING_QUORUM,
+        slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE
+      })
+    });
+    slasher = rollup.SLASHER();
+    slashFactory = new SlashFactory(ILeonidas(address(rollup)));
 
     testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount);
     testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount);
@@ -183,6 +207,40 @@ contract SpartaTest is DecoderBase {
     _testBlock("mixed_block_2", false, 3, false);
   }
 
+  function testNukeFromOrbit() public setup(4) {
+    // We propose some blocks, and have a bunch of validators attest to them.
+    // Then we slash EVERYONE that was in the committees because the epoch never
+    // got finalised.
+    // This is LIKELY, not the action you really want to take, you want to slash
+    // the people actually attesting, etc, but for simplicity we can do this as showcase.
+    _testBlock("mixed_block_1", false, 3, false);
+    _testBlock("mixed_block_2", false, 3, false);
+
+    address[] memory attesters = rollup.getAttesters();
+    uint256[] memory stakes = new uint256[](attesters.length);
+
+    for (uint256 i = 0; i < attesters.length; i++) {
+      ValidatorInfo memory info = rollup.getInfo(attesters[i]);
+      stakes[i] = info.stake;
+      assertTrue(info.status == Status.VALIDATING, "Invalid status");
+    }
+
+    // We say, these things are bad, call the baba yaga to take care of them!
+    uint256 slashAmount = 10e18;
+    IPayload slashPayload = slashFactory.createSlashPayload(rollup.getCurrentEpoch(), slashAmount);
+    vm.prank(address(slasher.PROPOSER()));
+    slasher.slash(slashPayload);
+
+    // Make sure that the slash was successful,
+    // Meaning that validators are now LIVING and have lost the slash amount
+    for (uint256 i = 0; i < attesters.length; i++) {
+      ValidatorInfo memory info = rollup.getInfo(attesters[i]);
+      uint256 stake = info.stake;
+      assertEq(stake, stakes[i] - slashAmount, "Invalid stake");
+      assertTrue(info.status == Status.LIVING, "Invalid status");
+    }
+  }
+
   function testInvalidProposer() public setup(4) {
     _testBlock("mixed_block_1", true, 3, true);
   }
diff --git a/l1-contracts/test/staking/StakingCheater.sol b/l1-contracts/test/staking/StakingCheater.sol
index ba89e1e07ab5..a886a3d2f72c 100644
--- a/l1-contracts/test/staking/StakingCheater.sol
+++ b/l1-contracts/test/staking/StakingCheater.sol
@@ -9,9 +9,12 @@ import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol";
 contract StakingCheater is Staking {
   using EnumerableSet for EnumerableSet.AddressSet;
 
-  constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake)
-    Staking(_slasher, _stakingAsset, _minimumStake)
-  {}
+  constructor(
+    IERC20 _stakingAsset,
+    uint256 _minimumStake,
+    uint256 _slashingQuorum,
+    uint256 _roundSize
+  ) Staking(_stakingAsset, _minimumStake, _slashingQuorum, _roundSize) {}
 
   function cheat__SetStatus(address _attester, Status _status) external {
     stakingStore.info[_attester].status = _status;
diff --git a/l1-contracts/test/staking/base.t.sol b/l1-contracts/test/staking/base.t.sol
index 6aa8eaa8ca4b..441d418d244f 100644
--- a/l1-contracts/test/staking/base.t.sol
+++ b/l1-contracts/test/staking/base.t.sol
@@ -16,10 +16,13 @@ contract StakingBase is TestBase {
   address internal constant ATTESTER = address(bytes20("ATTESTER"));
   address internal constant WITHDRAWER = address(bytes20("WITHDRAWER"));
   address internal constant RECIPIENT = address(bytes20("RECIPIENT"));
-  address internal constant SLASHER = address(bytes20("SLASHER"));
+
+  address internal SLASHER;
 
   function setUp() public virtual {
     stakingAsset = new TestERC20("test", "TEST", address(this));
-    staking = new StakingCheater(SLASHER, stakingAsset, MINIMUM_STAKE);
+    staking = new StakingCheater(stakingAsset, MINIMUM_STAKE, 1, 1);
+
+    SLASHER = address(staking.SLASHER());
   }
 }
diff --git a/spartan/aztec-network/files/config/config-prover-env.sh b/spartan/aztec-network/files/config/config-prover-env.sh
index 073547821d48..2d56ed1c897e 100644
--- a/spartan/aztec-network/files/config/config-prover-env.sh
+++ b/spartan/aztec-network/files/config/config-prover-env.sh
@@ -19,6 +19,7 @@ coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F
 reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}')
 governance_proposer_address=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}')
 governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}')
+slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}')
 
 # Write the addresses to a file in the shared volume
 cat <<EOF >/shared/contracts/contracts.env
@@ -34,6 +35,7 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address
 export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address
 export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$governance_proposer_address
 export GOVERNANCE_CONTRACT_ADDRESS=$governance_address
+export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address
 EOF
 
 cat /shared/contracts/contracts.env
diff --git a/spartan/aztec-network/files/config/config-validator-env.sh b/spartan/aztec-network/files/config/config-validator-env.sh
index b2848f8e069c..05d55e437f39 100644
--- a/spartan/aztec-network/files/config/config-validator-env.sh
+++ b/spartan/aztec-network/files/config/config-validator-env.sh
@@ -19,6 +19,7 @@ coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F
 reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}')
 governance_proposer_address=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}')
 governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}')
+slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}')
 # We assume that there is an env var set for validator keys from the config map
 # We get the index in the config map from the pod name, which will have the validator index within it
 
@@ -39,6 +40,7 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address
 export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address
 export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$governance_proposer_address
 export GOVERNANCE_CONTRACT_ADDRESS=$governance_address
+export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address
 export VALIDATOR_PRIVATE_KEY=$private_key
 export L1_PRIVATE_KEY=$private_key
 export SEQ_PUBLISHER_PRIVATE_KEY=$private_key
diff --git a/spartan/aztec-network/files/config/deploy-l1-contracts.sh b/spartan/aztec-network/files/config/deploy-l1-contracts.sh
index 1f4c56599f74..366a00bd41fe 100644
--- a/spartan/aztec-network/files/config/deploy-l1-contracts.sh
+++ b/spartan/aztec-network/files/config/deploy-l1-contracts.sh
@@ -36,6 +36,7 @@ coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F
 reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}')
 governance_proposer_address=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}')
 governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}')
+slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}')
 
 # Write the addresses to a file in the shared volume
 cat <<EOF > /shared/contracts/contracts.env
@@ -50,6 +51,7 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address
 export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address
 export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$governance_proposer_address
 export GOVERNANCE_CONTRACT_ADDRESS=$governance_address
+export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address
 EOF
 
 cat /shared/contracts/contracts.env
diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts
index fa7904296cc7..e00f1cb41017 100644
--- a/yarn-project/aztec-node/src/aztec-node/server.ts
+++ b/yarn-project/aztec-node/src/aztec-node/server.ts
@@ -72,7 +72,7 @@ import {
   createP2PClient,
 } from '@aztec/p2p';
 import { ProtocolContractAddress } from '@aztec/protocol-contracts';
-import { GlobalVariableBuilder, type L1Publisher, SequencerClient } from '@aztec/sequencer-client';
+import { GlobalVariableBuilder, type L1Publisher, SequencerClient, createSlasherClient } from '@aztec/sequencer-client';
 import { PublicProcessorFactory } from '@aztec/simulator';
 import { type TelemetryClient } from '@aztec/telemetry-client';
 import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
@@ -164,8 +164,10 @@ export class AztecNodeService implements AztecNode {
     // create the tx pool and the p2p client, which will need the l2 block source
     const p2pClient = await createP2PClient(config, archiver, proofVerifier, worldStateSynchronizer, telemetry);
 
+    const slasherClient = await createSlasherClient(config, archiver, telemetry);
+
     // start both and wait for them to sync from the block source
-    await Promise.all([p2pClient.start(), worldStateSynchronizer.start()]);
+    await Promise.all([p2pClient.start(), worldStateSynchronizer.start(), slasherClient.start()]);
 
     const validatorClient = await createValidatorClient(config, config.l1Contracts.rollupAddress, p2pClient, telemetry);
 
@@ -176,6 +178,7 @@ export class AztecNodeService implements AztecNode {
           validatorClient,
           p2pClient,
           worldStateSynchronizer,
+          slasherClient,
           contractDataSource: archiver,
           l2BlockSource: archiver,
           l1ToL2MessageSource: archiver,
diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts
index 66a54e8cfb5b..f45eb0203d18 100644
--- a/yarn-project/aztec.js/src/contract/contract.test.ts
+++ b/yarn-project/aztec.js/src/contract/contract.test.ts
@@ -47,6 +47,7 @@ describe('Contract Class', () => {
     coinIssuerAddress: EthAddress.random(),
     rewardDistributorAddress: EthAddress.random(),
     governanceProposerAddress: EthAddress.random(),
+    slashFactoryAddress: EthAddress.random(),
   };
   const mockNodeInfo: NodeInfo = {
     nodeVersion: 'vx.x.x',
diff --git a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts
index a3e6c77d39d5..cf5dbe7bdc10 100644
--- a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts
+++ b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts
@@ -1,5 +1,5 @@
 import { createCompatibleClient } from '@aztec/aztec.js';
-import { MINIMUM_STAKE, createEthereumChain } from '@aztec/ethereum';
+import { createEthereumChain, getL1ContractsConfigEnvVars } from '@aztec/ethereum';
 import { type LogFn, type Logger } from '@aztec/foundation/log';
 import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts';
 
@@ -71,14 +71,16 @@ export async function sequencers(opts: {
       client: walletClient,
     });
 
+    const config = getL1ContractsConfigEnvVars();
+
     await Promise.all(
       [
-        await stakingAsset.write.mint([walletClient.account.address, MINIMUM_STAKE], {} as any),
-        await stakingAsset.write.approve([rollup.address, MINIMUM_STAKE], {} as any),
+        await stakingAsset.write.mint([walletClient.account.address, config.minimumStake], {} as any),
+        await stakingAsset.write.approve([rollup.address, config.minimumStake], {} as any),
       ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })),
     );
 
-    const hash = await writeableRollup.write.deposit([who, who, who, MINIMUM_STAKE]);
+    const hash = await writeableRollup.write.deposit([who, who, who, config.minimumStake]);
     await publicClient.waitForTransactionReceipt({ hash });
     log(`Added in tx ${hash}`);
   } else if (command === 'remove') {
diff --git a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts
index 21ac9d71ec68..39b4bfd46351 100644
--- a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts
+++ b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts
@@ -48,5 +48,6 @@ export async function deployL1Contracts(
     log(`RewardDistributor Address: ${l1ContractAddresses.rewardDistributorAddress.toString()}`);
     log(`GovernanceProposer Address: ${l1ContractAddresses.governanceProposerAddress.toString()}`);
     log(`Governance Address: ${l1ContractAddresses.governanceAddress.toString()}`);
+    log(`SlashFactory Address: ${l1ContractAddresses.slashFactoryAddress.toString()}`);
   }
 }
diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts
index 7d5edca07bac..40d06e2fd6d0 100644
--- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts
+++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts
@@ -1,6 +1,6 @@
 import { EthCheatCodes } from '@aztec/aztec.js';
 import { type EthAddress } from '@aztec/circuits.js';
-import { MINIMUM_STAKE, createEthereumChain, getL1ContractsConfigEnvVars, isAnvilTestChain } from '@aztec/ethereum';
+import { createEthereumChain, getL1ContractsConfigEnvVars, isAnvilTestChain } from '@aztec/ethereum';
 import { type LogFn, type Logger } from '@aztec/foundation/log';
 import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts';
 
@@ -40,6 +40,7 @@ export async function addL1Validator({
   log,
   debugLogger,
 }: RollupCommandArgs & LoggerArgs & { validatorAddress: EthAddress }) {
+  const config = getL1ContractsConfigEnvVars();
   const dualLog = makeDualLog(log, debugLogger);
   const publicClient = getPublicClient(rpcUrl, chainId);
   const walletClient = getWalletClient(rpcUrl, chainId, privateKey, mnemonic);
@@ -57,8 +58,8 @@ export async function addL1Validator({
 
   await Promise.all(
     [
-      await stakingAsset.write.mint([walletClient.account.address, MINIMUM_STAKE], {} as any),
-      await stakingAsset.write.approve([rollupAddress.toString(), MINIMUM_STAKE], {} as any),
+      await stakingAsset.write.mint([walletClient.account.address, config.minimumStake], {} as any),
+      await stakingAsset.write.approve([rollupAddress.toString(), config.minimumStake], {} as any),
     ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })),
   );
 
@@ -67,7 +68,7 @@ export async function addL1Validator({
     validatorAddress.toString(),
     validatorAddress.toString(),
     validatorAddress.toString(),
-    MINIMUM_STAKE,
+    config.minimumStake,
   ]);
   dualLog(`Transaction hash: ${txHash}`);
   await publicClient.waitForTransactionReceipt({ hash: txHash });
diff --git a/yarn-project/end-to-end/scripts/e2e_test_config.yml b/yarn-project/end-to-end/scripts/e2e_test_config.yml
index 2fb7902c93f8..2ea76eb0e911 100644
--- a/yarn-project/end-to-end/scripts/e2e_test_config.yml
+++ b/yarn-project/end-to-end/scripts/e2e_test_config.yml
@@ -90,6 +90,8 @@ tests:
   e2e_p2p_gossip:
     test_path: 'e2e_p2p/gossip_network.test.ts'
     with_alerts: true
+  e2e_p2p_slashing:
+    test_path: 'e2e_p2p/slashing.test.ts'
   e2e_p2p_upgrade_governance_proposer:
     test_path: 'e2e_p2p/upgrade_governance_proposer.test.ts'
   e2e_p2p_rediscovery:
diff --git a/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh b/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh
index 2f1d670620ce..9c87ef3332c3 100755
--- a/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh
+++ b/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh
@@ -63,6 +63,7 @@ COIN_ISSUER_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'CoinIssuer Address: \K
 REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}')
 GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}')
 GOVERNANCE_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}')
+SLASH_FACTORY_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}')
 
 # Save contract addresses to state/l1-contracts.env
 cat <<EOCONFIG >$(git rev-parse --show-toplevel)/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env
@@ -77,6 +78,7 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$COIN_ISSUER_CONTRACT_ADDRESS
 export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$REWARD_DISTRIBUTOR_CONTRACT_ADDRESS
 export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$GOVERNANCE_PROPOSER_CONTRACT_ADDRESS
 export GOVERNANCE_CONTRACT_ADDRESS=$GOVERNANCE_CONTRACT_ADDRESS
+export SLASH_FACTORY_CONTRACT_ADDRESS=$SLASH_FACTORY_CONTRACT_ADDRESS
 EOCONFIG
 
 echo "Contract addresses saved to state/l1-contracts.env"
diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts
index 09d282c4f437..3211d00bf291 100644
--- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts
+++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts
@@ -1,7 +1,7 @@
 import { getSchnorrAccount } from '@aztec/accounts/schnorr';
 import { type AztecNodeConfig, type AztecNodeService } from '@aztec/aztec-node';
 import { type AccountWalletWithSecretKey } from '@aztec/aztec.js';
-import { EthCheatCodes, MINIMUM_STAKE, getL1ContractsConfigEnvVars } from '@aztec/ethereum';
+import { EthCheatCodes, getL1ContractsConfigEnvVars } from '@aztec/ethereum';
 import { type Logger, createLogger } from '@aztec/foundation/log';
 import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts';
 import { SpamContract } from '@aztec/noir-contracts.js';
@@ -60,6 +60,7 @@ export class P2PNetworkTest {
     initialValidatorConfig: AztecNodeConfig,
     // If set enable metrics collection
     metricsPort?: number,
+    assumeProvenThrough?: number,
   ) {
     this.logger = createLogger(`e2e:e2e_p2p:${testName}`);
 
@@ -71,12 +72,24 @@ export class P2PNetworkTest {
 
     this.bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt();
 
-    this.snapshotManager = createSnapshotManager(`e2e_p2p_network/${testName}`, process.env.E2E_DATA_PATH, {
-      ...initialValidatorConfig,
-      ethereumSlotDuration: l1ContractsConfig.ethereumSlotDuration,
-      salt: 420,
-      metricsPort: metricsPort,
-    });
+    this.snapshotManager = createSnapshotManager(
+      `e2e_p2p_network/${testName}`,
+      process.env.E2E_DATA_PATH,
+      {
+        ...initialValidatorConfig,
+        ethereumSlotDuration: l1ContractsConfig.ethereumSlotDuration,
+        salt: 420,
+        metricsPort: metricsPort,
+      },
+      {
+        aztecEpochDuration: initialValidatorConfig.aztecEpochDuration ?? l1ContractsConfig.aztecEpochDuration,
+        aztecEpochProofClaimWindowInL2Slots:
+          initialValidatorConfig.aztecEpochProofClaimWindowInL2Slots ??
+          l1ContractsConfig.aztecEpochProofClaimWindowInL2Slots,
+        assumeProvenThrough: assumeProvenThrough ?? Number.MAX_SAFE_INTEGER,
+        initialValidators: [],
+      },
+    );
   }
 
   /**
@@ -113,11 +126,15 @@ export class P2PNetworkTest {
     numberOfNodes,
     basePort,
     metricsPort,
+    initialConfig,
+    assumeProvenThrough,
   }: {
     testName: string;
     numberOfNodes: number;
     basePort?: number;
     metricsPort?: number;
+    initialConfig?: Partial<AztecNodeConfig>;
+    assumeProvenThrough?: number;
   }) {
     const port = basePort || (await getPort());
 
@@ -125,9 +142,20 @@ export class P2PNetworkTest {
     const bootstrapNode = await createBootstrapNodeFromPrivateKey(BOOTSTRAP_NODE_PRIVATE_KEY, port, telemetry);
     const bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt();
 
-    const initialValidatorConfig = await createValidatorConfig({} as AztecNodeConfig, bootstrapNodeEnr);
+    const initialValidatorConfig = await createValidatorConfig(
+      (initialConfig ?? {}) as AztecNodeConfig,
+      bootstrapNodeEnr,
+    );
 
-    return new P2PNetworkTest(testName, bootstrapNode, port, numberOfNodes, initialValidatorConfig);
+    return new P2PNetworkTest(
+      testName,
+      bootstrapNode,
+      port,
+      numberOfNodes,
+      initialValidatorConfig,
+      metricsPort,
+      assumeProvenThrough,
+    );
   }
 
   async applyBaseSnapshots() {
@@ -148,7 +176,7 @@ export class P2PNetworkTest {
           client: deployL1ContractsValues.walletClient,
         });
 
-        const stakeNeeded = MINIMUM_STAKE * BigInt(this.numberOfNodes);
+        const stakeNeeded = l1ContractsConfig.minimumStake * BigInt(this.numberOfNodes);
         await Promise.all(
           [
             await stakingAsset.write.mint(
@@ -171,7 +199,7 @@ export class P2PNetworkTest {
             attester: attester.address,
             proposer: proposer.address,
             withdrawer: attester.address,
-            amount: MINIMUM_STAKE,
+            amount: l1ContractsConfig.minimumStake,
           } as const);
 
           this.logger.verbose(
diff --git a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts
new file mode 100644
index 000000000000..7aa8d891158d
--- /dev/null
+++ b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts
@@ -0,0 +1,277 @@
+import { type AztecNodeService } from '@aztec/aztec-node';
+import { sleep } from '@aztec/aztec.js';
+import { RollupAbi, SlashFactoryAbi, SlasherAbi, SlashingProposerAbi } from '@aztec/l1-artifacts';
+
+import fs from 'fs';
+import { getAddress, getContract, parseEventLogs } from 'viem';
+
+import { shouldCollectMetrics } from '../fixtures/fixtures.js';
+import { createNodes } from '../fixtures/setup_p2p_test.js';
+import { AlertChecker, type AlertConfig } from '../quality_of_service/alert_checker.js';
+import { P2PNetworkTest } from './p2p_network.js';
+import { createPXEServiceAndSubmitTransactions } from './shared.js';
+
+const CHECK_ALERTS = process.env.CHECK_ALERTS === 'true';
+
+// Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds
+const NUM_NODES = 4;
+const BOOT_NODE_UDP_PORT = 40600;
+
+const DATA_DIR = './data/gossip';
+
+const qosAlerts: AlertConfig[] = [
+  {
+    alert: 'SequencerTimeToCollectAttestations',
+    expr: 'aztec_sequencer_time_to_collect_attestations > 3500',
+    labels: { severity: 'error' },
+    for: '10m',
+    annotations: {},
+  },
+];
+
+// This test is showcasing that slashing can happen, abusing that our nodes are honest but stupid
+// making them slash themselves.
+describe('e2e_p2p_network', () => {
+  let t: P2PNetworkTest;
+  let nodes: AztecNodeService[];
+
+  beforeEach(async () => {
+    t = await P2PNetworkTest.create({
+      testName: 'e2e_p2p_network',
+      numberOfNodes: NUM_NODES,
+      basePort: BOOT_NODE_UDP_PORT,
+      metricsPort: shouldCollectMetrics(),
+      initialConfig: {
+        aztecEpochDuration: 1,
+        aztecEpochProofClaimWindowInL2Slots: 1,
+      },
+      assumeProvenThrough: 1,
+    });
+
+    await t.applyBaseSnapshots();
+    await t.setup();
+    await t.removeInitialNode();
+  });
+
+  afterEach(async () => {
+    await t.stopNodes(nodes);
+    await t.teardown();
+    for (let i = 0; i < NUM_NODES; i++) {
+      fs.rmSync(`${DATA_DIR}-${i}`, { recursive: true, force: true });
+    }
+  });
+
+  afterAll(async () => {
+    if (CHECK_ALERTS) {
+      const checker = new AlertChecker(t.logger);
+      await checker.runAlertCheck(qosAlerts);
+    }
+  });
+
+  it('should slash the attesters', async () => {
+    // create the bootstrap node for the network
+    if (!t.bootstrapNodeEnr) {
+      throw new Error('Bootstrap node ENR is not available');
+    }
+
+    const rollup = getContract({
+      address: t.ctx.deployL1ContractsValues!.l1ContractAddresses.rollupAddress.toString(),
+      abi: RollupAbi,
+      client: t.ctx.deployL1ContractsValues!.walletClient,
+    });
+
+    const slasherContract = getContract({
+      address: getAddress(await rollup.read.SLASHER()),
+      abi: SlasherAbi,
+      client: t.ctx.deployL1ContractsValues.publicClient,
+    });
+
+    const slashingProposer = getContract({
+      address: getAddress(await slasherContract.read.PROPOSER()),
+      abi: SlashingProposerAbi,
+      client: t.ctx.deployL1ContractsValues.publicClient,
+    });
+
+    const slashFactory = getContract({
+      address: getAddress(t.ctx.deployL1ContractsValues.l1ContractAddresses.slashFactoryAddress.toString()),
+      abi: SlashFactoryAbi,
+      client: t.ctx.deployL1ContractsValues.publicClient,
+    });
+
+    const slashingInfo = async () => {
+      const bn = await t.ctx.cheatCodes.eth.blockNumber();
+      const slotNumber = await rollup.read.getCurrentSlot();
+      const roundNumber = await slashingProposer.read.computeRound([slotNumber]);
+      const instanceAddress = t.ctx.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString();
+      const info = await slashingProposer.read.rounds([instanceAddress, roundNumber]);
+      const leaderVotes = await slashingProposer.read.yeaCount([instanceAddress, roundNumber, info[1]]);
+      return { bn, slotNumber, roundNumber, info, leaderVotes };
+    };
+
+    const waitForL1Block = async () => {
+      // Send and wait an l1 block
+      await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({
+        hash: await t.ctx.deployL1ContractsValues.walletClient.sendTransaction({
+          to: t.ctx.deployL1ContractsValues.walletClient.account.address,
+          value: 1n,
+          account: t.ctx.deployL1ContractsValues.walletClient.account,
+        }),
+      });
+    };
+
+    t.ctx.aztecNodeConfig.validatorReexecute = false;
+
+    // create our network of nodes and submit txs into each of them
+    // the number of txs per node and the number of txs per rollup
+    // should be set so that the only way for rollups to be built
+    // is if the txs are successfully gossiped around the nodes.
+    t.logger.info('Creating nodes');
+    nodes = await createNodes(
+      t.ctx.aztecNodeConfig,
+      t.bootstrapNodeEnr,
+      NUM_NODES,
+      BOOT_NODE_UDP_PORT,
+      DATA_DIR,
+      // To collect metrics - run in aztec-packages `docker compose --profile metrics up` and set COLLECT_METRICS=true
+      shouldCollectMetrics(),
+    );
+
+    // We are overriding the slashing amount to 1, such that the slashing will "really" happen.
+    for (const node of nodes) {
+      const seqClient = node.getSequencer();
+      if (!seqClient) {
+        throw new Error('Sequencer not found');
+      }
+      const sequencer = (seqClient as any).sequencer;
+      const slasher = (sequencer as any).slasherClient;
+      slasher.slashingAmount = 1n;
+    }
+
+    // wait a bit for peers to discover each other
+    await sleep(4000);
+
+    let sInfo = await slashingInfo();
+
+    const votesNeeded = await slashingProposer.read.N();
+    const roundSize = await slashingProposer.read.M();
+
+    // We should push us to land exactly at the next round
+    const nextRoundTimestamp1 = await rollup.read.getTimestampForSlot([
+      ((await rollup.read.getCurrentSlot()) / roundSize) * roundSize + roundSize,
+    ]);
+    await t.ctx.cheatCodes.eth.warp(Number(nextRoundTimestamp1));
+
+    // Produce blocks until we hit an issue with pruning.
+    // Then we should jump in time to the next round so we are sure that we have the votes
+    // Then we just sit on our hands and wait.
+
+    const seqClient = nodes[0].getSequencer();
+    if (!seqClient) {
+      throw new Error('Sequencer not found');
+    }
+    const sequencer = (seqClient as any).sequencer;
+    const slasher = (sequencer as any).slasherClient;
+
+    t.logger.info(`Producing blocks until we hit a pruning event`);
+
+    for (let i = 0; i < 15; i++) {
+      t.logger.info('Submitting transactions');
+      const bn = await nodes[0].getBlockNumber();
+      await createPXEServiceAndSubmitTransactions(t.logger, nodes[0], 1);
+
+      t.logger.info(`Waiting for block number to change`);
+      while (bn === (await nodes[0].getBlockNumber())) {
+        await sleep(1000);
+      }
+
+      if (slasher.slashEvents.length > 0) {
+        t.logger.info(`We have a slash event`);
+        break;
+      }
+    }
+
+    for (let i = 0; i < 15; i++) {
+      t.logger.info('Waiting for slot number to change and votes to be cast');
+      const slotNumber = await rollup.read.getCurrentSlot();
+      t.logger.info(`Waiting for block number to change`);
+      while (slotNumber === (await rollup.read.getCurrentSlot())) {
+        await sleep(1000);
+      }
+      sInfo = await slashingInfo();
+      t.logger.info(`We have ${sInfo.leaderVotes} votes in round ${sInfo.roundNumber} on ${sInfo.info[1]}`);
+      if (sInfo.leaderVotes > votesNeeded) {
+        t.logger.info(`We have sufficient votes`);
+        break;
+      }
+    }
+
+    t.logger.info('Deploy the actual payload for slashing!');
+    const slashEvent = slasher.slashEvents[0];
+    await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({
+      hash: await slashFactory.write.createSlashPayload([slashEvent.epoch, slashEvent.amount], {
+        account: t.ctx.deployL1ContractsValues.walletClient.account,
+      }),
+    });
+
+    t.logger.info(`We jump in time to the next round to execute`);
+    const nextRoundTimestamp2 = await rollup.read.getTimestampForSlot([
+      ((await rollup.read.getCurrentSlot()) / roundSize) * roundSize + roundSize,
+    ]);
+    await t.ctx.cheatCodes.eth.warp(Number(nextRoundTimestamp2));
+    await waitForL1Block();
+
+    const attestersPre = await rollup.read.getAttesters();
+
+    for (const attester of attestersPre) {
+      const attesterInfo = await rollup.read.getInfo([attester]);
+      // Check that status isValidating
+      expect(attesterInfo.status).toEqual(1);
+    }
+
+    t.logger.info(`Push the proposal, SLASHING!`);
+    const tx = await slashingProposer.write.pushProposal([sInfo.roundNumber], {
+      account: t.ctx.deployL1ContractsValues.walletClient.account,
+    });
+    await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({
+      hash: tx,
+    });
+
+    const receipt = await t.ctx.deployL1ContractsValues.publicClient.getTransactionReceipt({
+      hash: tx,
+    });
+
+    const slashingEvents = parseEventLogs({
+      abi: RollupAbi,
+      logs: receipt.logs,
+    }).filter(log => log.eventName === 'Slashed');
+
+    const attestersSlashed = slashingEvents.map(event => {
+      // Because TS is a little nagging bitch
+      return (event.args as any).attester;
+    });
+
+    // Convert attestersPre elements to lowercase for consistent comparison
+    const normalizedAttestersPre = attestersPre.map(addr => addr.toLowerCase());
+    const normalizedAttestersSlashed = attestersSlashed.map(addr => addr.toLowerCase());
+    expect(new Set(normalizedAttestersPre)).toEqual(new Set(normalizedAttestersSlashed));
+
+    const instanceAddress = t.ctx.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString();
+    const infoPost = await slashingProposer.read.rounds([instanceAddress, sInfo.roundNumber]);
+
+    expect(sInfo.info[0]).toEqual(infoPost[0]);
+    expect(sInfo.info[1]).toEqual(infoPost[1]);
+    expect(sInfo.info[2]).toEqual(false);
+    expect(infoPost[2]).toEqual(true);
+
+    const attestersPost = await rollup.read.getAttesters();
+
+    for (const attester of attestersPre) {
+      const attesterInfo = await rollup.read.getInfo([attester]);
+      // Check that status is Living
+      expect(attesterInfo.status).toEqual(2);
+    }
+    const committee = await rollup.read.getEpochCommittee([slashEvent.epoch]);
+    expect(attestersPre.length).toBe(committee.length);
+    expect(attestersPost.length).toBe(0);
+  }, 1_000_000);
+});
diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts
index 6743a7ef2c4e..d65f18090e02 100644
--- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts
+++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts
@@ -305,9 +305,9 @@ async function setupFromFresh(
   }
 
   const deployL1ContractsValues = await setupL1Contracts(aztecNodeConfig.l1RpcUrl, hdAccount, logger, {
+    ...getL1ContractsConfigEnvVars(),
     salt: opts.salt,
     ...deployL1ContractsArgs,
-    ...getL1ContractsConfigEnvVars(),
     initialValidators: opts.initialValidators,
   });
   aztecNodeConfig.l1Contracts = deployL1ContractsValues.l1ContractAddresses;
diff --git a/yarn-project/ethereum/src/config.ts b/yarn-project/ethereum/src/config.ts
index eda0024870b3..4bb362d886e4 100644
--- a/yarn-project/ethereum/src/config.ts
+++ b/yarn-project/ethereum/src/config.ts
@@ -1,4 +1,9 @@
-import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config';
+import {
+  type ConfigMappingsType,
+  bigintConfigHelper,
+  getConfigFromMappings,
+  numberConfigHelper,
+} from '@aztec/foundation/config';
 
 export type L1ContractsConfig = {
   /** How many seconds an L1 slot lasts. */
@@ -11,6 +16,16 @@ export type L1ContractsConfig = {
   aztecTargetCommitteeSize: number;
   /** The number of L2 slots that we can wait for a proof of an epoch to be produced. */
   aztecEpochProofClaimWindowInL2Slots: number;
+  /** The minimum stake for a validator. */
+  minimumStake: bigint;
+  /** The slashing quorum */
+  slashingQuorum: number;
+  /** The slashing round size */
+  slashingRoundSize: number;
+  /** Governance proposing quorum */
+  governanceProposerQuorum: number;
+  /** Governance proposing round size */
+  governanceProposerRoundSize: number;
 };
 
 export const DefaultL1ContractsConfig: L1ContractsConfig = {
@@ -19,6 +34,11 @@ export const DefaultL1ContractsConfig: L1ContractsConfig = {
   aztecEpochDuration: 16,
   aztecTargetCommitteeSize: 48,
   aztecEpochProofClaimWindowInL2Slots: 13,
+  minimumStake: BigInt(100e18),
+  slashingQuorum: 6,
+  slashingRoundSize: 10,
+  governanceProposerQuorum: 6,
+  governanceProposerRoundSize: 10,
 };
 
 export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> = {
@@ -47,6 +67,31 @@ export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> =
     description: 'The number of L2 slots that we can wait for a proof of an epoch to be produced.',
     ...numberConfigHelper(DefaultL1ContractsConfig.aztecEpochProofClaimWindowInL2Slots),
   },
+  minimumStake: {
+    env: 'AZTEC_MINIMUM_STAKE',
+    description: 'The minimum stake for a validator.',
+    ...bigintConfigHelper(DefaultL1ContractsConfig.minimumStake),
+  },
+  slashingQuorum: {
+    env: 'AZTEC_SLASHING_QUORUM',
+    description: 'The slashing quorum',
+    ...numberConfigHelper(DefaultL1ContractsConfig.slashingQuorum),
+  },
+  slashingRoundSize: {
+    env: 'AZTEC_SLASHING_ROUND_SIZE',
+    description: 'The slashing round size',
+    ...numberConfigHelper(DefaultL1ContractsConfig.slashingRoundSize),
+  },
+  governanceProposerQuorum: {
+    env: 'AZTEC_GOVERNANCE_PROPOSER_QUORUM',
+    description: 'The governance proposing quorum',
+    ...numberConfigHelper(DefaultL1ContractsConfig.governanceProposerQuorum),
+  },
+  governanceProposerRoundSize: {
+    env: 'AZTEC_GOVERNANCE_PROPOSER_ROUND_SIZE',
+    description: 'The governance proposing round size',
+    ...numberConfigHelper(DefaultL1ContractsConfig.governanceProposerRoundSize),
+  },
 };
 
 export function getL1ContractsConfigEnvVars(): L1ContractsConfig {
diff --git a/yarn-project/ethereum/src/constants.ts b/yarn-project/ethereum/src/constants.ts
index 2fea0175acac..c1f4b34d7321 100644
--- a/yarn-project/ethereum/src/constants.ts
+++ b/yarn-project/ethereum/src/constants.ts
@@ -2,4 +2,3 @@ import { type Hex } from 'viem';
 
 export const NULL_KEY: Hex = `0x${'0000000000000000000000000000000000000000000000000000000000000000'}`;
 export const AZTEC_TEST_CHAIN_ID = 677692;
-export const MINIMUM_STAKE = BigInt(100e18);
diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts
index 31b2c1eeb50e..657c9ff71457 100644
--- a/yarn-project/ethereum/src/deploy_l1_contracts.ts
+++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts
@@ -26,6 +26,8 @@ import {
   RollupAbi,
   RollupBytecode,
   RollupLinkReferences,
+  SlashFactoryAbi,
+  SlashFactoryBytecode,
   TestERC20Abi,
   TestERC20Bytecode,
 } from '@aztec/l1-artifacts';
@@ -53,7 +55,6 @@ import { type HDAccount, type PrivateKeyAccount, mnemonicToAccount, privateKeyTo
 import { foundry } from 'viem/chains';
 
 import { type L1ContractsConfig } from './config.js';
-import { MINIMUM_STAKE } from './constants.js';
 import { isAnvilTestChain } from './ethereum_chain.js';
 import { type L1ContractAddresses } from './l1_contract_addresses.js';
 import { L1TxUtils } from './l1_tx_utils.js';
@@ -156,6 +157,10 @@ export interface L1ContractArtifactsForDeployment {
    * Governance contract artifacts.
    */
   governance: ContractArtifacts;
+  /**
+   * SlashFactory contract artifacts.
+   */
+  slashFactory: ContractArtifacts;
 }
 
 export const l1Artifacts: L1ContractArtifactsForDeployment = {
@@ -216,6 +221,10 @@ export const l1Artifacts: L1ContractArtifactsForDeployment = {
     contractAbi: GovernanceAbi,
     contractBytecode: GovernanceBytecode,
   },
+  slashFactory: {
+    contractAbi: SlashFactoryAbi,
+    contractBytecode: SlashFactoryBytecode,
+  },
 };
 
 export interface DeployL1ContractsArgs extends L1ContractsConfig {
@@ -331,14 +340,10 @@ export const deployL1Contracts = async (
   ]);
   logger.verbose(`Deployed Staking Asset at ${stakingAssetAddress}`);
 
-  // @todo  #8084
-  // @note These numbers are just chosen to make testing simple.
-  const quorumSize = 6n;
-  const roundSize = 10n;
   const governanceProposerAddress = await govDeployer.deploy(l1Artifacts.governanceProposer, [
     registryAddress.toString(),
-    quorumSize,
-    roundSize,
+    args.governanceProposerQuorum,
+    args.governanceProposerRoundSize,
   ]);
   logger.verbose(`Deployed GovernanceProposer at ${governanceProposerAddress}`);
 
@@ -382,7 +387,9 @@ export const deployL1Contracts = async (
     aztecEpochDuration: args.aztecEpochDuration,
     targetCommitteeSize: args.aztecTargetCommitteeSize,
     aztecEpochProofClaimWindowInL2Slots: args.aztecEpochProofClaimWindowInL2Slots,
-    minimumStake: MINIMUM_STAKE,
+    minimumStake: args.minimumStake,
+    slashingQuorum: args.slashingQuorum,
+    slashingRoundSize: args.slashingRoundSize,
   };
   const rollupArgs = [
     feeJuicePortalAddress.toString(),
@@ -396,6 +403,9 @@ export const deployL1Contracts = async (
   const rollupAddress = await deployer.deploy(l1Artifacts.rollup, rollupArgs);
   logger.verbose(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs);
 
+  const slashFactoryAddress = await deployer.deploy(l1Artifacts.slashFactory, [rollupAddress.toString()]);
+  logger.info(`Deployed SlashFactory at ${slashFactoryAddress}`);
+
   await deployer.waitForDeployments();
   logger.verbose(`All core contracts have been deployed`);
 
@@ -434,7 +444,7 @@ export const deployL1Contracts = async (
 
   if (args.initialValidators && args.initialValidators.length > 0) {
     // Mint tokens, approve them, use cheat code to initialise validator set without setting up the epoch.
-    const stakeNeeded = MINIMUM_STAKE * BigInt(args.initialValidators.length);
+    const stakeNeeded = args.minimumStake * BigInt(args.initialValidators.length);
     await Promise.all(
       [
         await stakingAsset.write.mint([walletClient.account.address, stakeNeeded], {} as any),
@@ -447,7 +457,7 @@ export const deployL1Contracts = async (
         attester: v.toString(),
         proposer: v.toString(),
         withdrawer: v.toString(),
-        amount: MINIMUM_STAKE,
+        amount: args.minimumStake,
       })),
     ]);
     txHashes.push(initiateValidatorSetTxHash);
@@ -560,6 +570,7 @@ export const deployL1Contracts = async (
     rewardDistributorAddress,
     governanceProposerAddress,
     governanceAddress,
+    slashFactoryAddress,
   };
 
   logger.info(`Aztec L1 contracts initialized`, l1Contracts);
diff --git a/yarn-project/ethereum/src/l1_contract_addresses.ts b/yarn-project/ethereum/src/l1_contract_addresses.ts
index eca35f4edead..aca32ba2dd21 100644
--- a/yarn-project/ethereum/src/l1_contract_addresses.ts
+++ b/yarn-project/ethereum/src/l1_contract_addresses.ts
@@ -21,6 +21,7 @@ export const L1ContractsNames = [
   'governanceProposerAddress',
   'governanceAddress',
   'stakingAssetAddress',
+  'slashFactoryAddress',
 ] as const;
 
 /** Provides the directory of current L1 contract addresses */
@@ -40,6 +41,7 @@ export const L1ContractAddressesSchema = z.object({
   rewardDistributorAddress: schemas.EthAddress,
   governanceProposerAddress: schemas.EthAddress,
   governanceAddress: schemas.EthAddress,
+  slashFactoryAddress: schemas.EthAddress,
 }) satisfies ZodFor<L1ContractAddresses>;
 
 const parseEnv = (val: string) => EthAddress.fromString(val);
@@ -100,4 +102,9 @@ export const l1ContractAddressesMapping: ConfigMappingsType<L1ContractAddresses>
     description: 'The deployed L1 governance contract address',
     parseEnv,
   },
+  slashFactoryAddress: {
+    env: 'SLASH_FACTORY_CONTRACT_ADDRESS',
+    description: 'The deployed L1 slashFactory contract address',
+    parseEnv,
+  },
 };
diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts
index ea13f2f97118..894dac8d9047 100644
--- a/yarn-project/foundation/src/config/env_var.ts
+++ b/yarn-project/foundation/src/config/env_var.ts
@@ -147,6 +147,7 @@ export type EnvVar =
   | 'SEQ_REQUIRED_CONFIRMATIONS'
   | 'SEQ_TX_POLLING_INTERVAL_MS'
   | 'SEQ_ENFORCE_TIME_TABLE'
+  | 'SLASH_FACTORY_CONTRACT_ADDRESS'
   | 'STAKING_ASSET_CONTRACT_ADDRESS'
   | 'REWARD_DISTRIBUTOR_CONTRACT_ADDRESS'
   | 'TELEMETRY'
@@ -174,6 +175,11 @@ export type EnvVar =
   | 'AZTEC_EPOCH_DURATION'
   | 'AZTEC_TARGET_COMMITTEE_SIZE'
   | 'AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS'
+  | 'AZTEC_MINIMUM_STAKE'
+  | 'AZTEC_SLASHING_QUORUM'
+  | 'AZTEC_SLASHING_ROUND_SIZE'
+  | 'AZTEC_GOVERNANCE_PROPOSER_QUORUM'
+  | 'AZTEC_GOVERNANCE_PROPOSER_ROUND_SIZE'
   | 'L1_GAS_LIMIT_BUFFER_PERCENTAGE'
   | 'L1_GAS_LIMIT_BUFFER_FIXED'
   | 'L1_GAS_PRICE_MIN'
diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh
index 3b78a796f506..def74c08542a 100755
--- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh
+++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh
@@ -30,6 +30,10 @@ CONTRACTS=(
   "l1-contracts:NewGovernanceProposerPayload"
   "l1-contracts:LeonidasLib"
   "l1-contracts:ExtRollupLib"
+  "l1-contracts:SlashingProposer"
+  "l1-contracts:Slasher"
+  "l1-contracts:EmpireBase"
+  "l1-contracts:SlashFactory"
 )
 
 
diff --git a/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts b/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts
index d0dc0103bbfe..51bd6ce16cc4 100644
--- a/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts
+++ b/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts
@@ -44,6 +44,7 @@ async function createPXEService(): Promise<PXE> {
     coinIssuerAddress: EthAddress.random(),
     rewardDistributorAddress: EthAddress.random(),
     governanceProposerAddress: EthAddress.random(),
+    slashFactoryAddress: EthAddress.random(),
   };
   node.getL1ContractAddresses.mockResolvedValue(mockedContracts);
 
diff --git a/yarn-project/sequencer-client/package.json b/yarn-project/sequencer-client/package.json
index 334377750069..826c5529a4aa 100644
--- a/yarn-project/sequencer-client/package.json
+++ b/yarn-project/sequencer-client/package.json
@@ -50,6 +50,7 @@
     "viem": "^2.7.15"
   },
   "devDependencies": {
+    "@aztec/archiver": "workspace:^",
     "@aztec/kv-store": "workspace:^",
     "@jest/globals": "^29.5.0",
     "@types/jest": "^29.5.0",
@@ -104,9 +105,9 @@
     "rootDir": "./src",
     "reporters": [
       [
-        "default",
+        "summary",
         {
-          "summaryThreshold": 9999
+          "summaryThreshold": 0
         }
       ]
     ],
diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts
index ba9987262c27..533e6068f8d5 100644
--- a/yarn-project/sequencer-client/src/client/sequencer-client.ts
+++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts
@@ -11,6 +11,7 @@ import { type SequencerClientConfig } from '../config.js';
 import { GlobalVariableBuilder } from '../global_variable_builder/index.js';
 import { L1Publisher } from '../publisher/index.js';
 import { Sequencer, type SequencerConfig } from '../sequencer/index.js';
+import { type SlasherClient } from '../slasher/index.js';
 import { TxValidatorFactory } from '../tx_validator/tx_validator_factory.js';
 
 /**
@@ -38,6 +39,7 @@ export class SequencerClient {
       validatorClient: ValidatorClient | undefined; // allowed to be undefined while we migrate
       p2pClient: P2P;
       worldStateSynchronizer: WorldStateSynchronizer;
+      slasherClient: SlasherClient;
       contractDataSource: ContractDataSource;
       l2BlockSource: L2BlockSource;
       l1ToL2MessageSource: L1ToL2MessageSource;
@@ -49,6 +51,7 @@ export class SequencerClient {
       validatorClient,
       p2pClient,
       worldStateSynchronizer,
+      slasherClient,
       contractDataSource,
       l2BlockSource,
       l1ToL2MessageSource,
@@ -71,6 +74,7 @@ export class SequencerClient {
       globalsBuilder,
       p2pClient,
       worldStateSynchronizer,
+      slasherClient,
       new LightweightBlockBuilderFactory(telemetryClient),
       l2BlockSource,
       l1ToL2MessageSource,
diff --git a/yarn-project/sequencer-client/src/index.ts b/yarn-project/sequencer-client/src/index.ts
index 1718ed0a3a63..e02cef93366a 100644
--- a/yarn-project/sequencer-client/src/index.ts
+++ b/yarn-project/sequencer-client/src/index.ts
@@ -2,6 +2,7 @@ export * from './client/index.js';
 export * from './config.js';
 export * from './publisher/index.js';
 export * from './sequencer/index.js';
+export * from './slasher/index.js';
 
 // Used by the node to simulate public parts of transactions. Should these be moved to a shared library?
 // ISSUE(#9832)
diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts
index a189fb28c9f0..15e7caf11894 100644
--- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts
+++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts
@@ -17,22 +17,16 @@ import {
   type Proof,
   type RootRollupPublicInputs,
 } from '@aztec/circuits.js';
-import {
-  type EthereumChain,
-  type L1ContractsConfig,
-  L1TxUtils,
-  type L1TxUtilsConfig,
-  createEthereumChain,
-} from '@aztec/ethereum';
+import { type EthereumChain, type L1ContractsConfig, L1TxUtils, createEthereumChain } from '@aztec/ethereum';
 import { makeTuple } from '@aztec/foundation/array';
 import { areArraysEqual, compactArray, times } from '@aztec/foundation/collection';
 import { type Signature } from '@aztec/foundation/eth-signature';
 import { Fr } from '@aztec/foundation/fields';
-import { createLogger } from '@aztec/foundation/log';
+import { type Logger, createLogger } from '@aztec/foundation/log';
 import { type Tuple, serializeToBuffer } from '@aztec/foundation/serialize';
 import { InterruptibleSleep } from '@aztec/foundation/sleep';
 import { Timer } from '@aztec/foundation/timer';
-import { GovernanceProposerAbi, RollupAbi } from '@aztec/l1-artifacts';
+import { EmpireBaseAbi, RollupAbi, SlasherAbi } from '@aztec/l1-artifacts';
 import { type TelemetryClient } from '@aztec/telemetry-client';
 
 import pick from 'lodash.pick';
@@ -132,6 +126,13 @@ export type L1SubmitEpochProofArgs = {
   proof: Proof;
 };
 
+export enum VoteType {
+  GOVERNANCE,
+  SLASHING,
+}
+
+type GetSlashPayloadCallBack = (slotNumber: bigint) => Promise<EthAddress | undefined>;
+
 /**
  * Publishes L2 blocks to L1. This implementation does *not* retry a transaction in
  * the event of network congestion, but should work for local development.
@@ -146,20 +147,25 @@ export class L1Publisher {
   private interrupted = false;
   private metrics: L1PublisherMetrics;
 
-  private payload: EthAddress = EthAddress.ZERO;
-  private myLastVote: bigint = 0n;
+  protected governanceLog = createLogger('sequencer:publisher:governance');
+  protected governanceProposerAddress?: EthAddress;
+  private governancePayload: EthAddress = EthAddress.ZERO;
+
+  protected slashingLog = createLogger('sequencer:publisher:slashing');
+  protected slashingProposerAddress?: EthAddress;
+  private getSlashPayload?: GetSlashPayloadCallBack = undefined;
+
+  private myLastVotes: Record<VoteType, bigint> = {
+    [VoteType.GOVERNANCE]: 0n,
+    [VoteType.SLASHING]: 0n,
+  };
 
   protected log = createLogger('sequencer:publisher');
-  protected governanceLog = createLogger('sequencer:publisher:governance');
 
   protected rollupContract: GetContractReturnType<
     typeof RollupAbi,
     WalletClient<HttpTransport, Chain, PrivateKeyAccount>
   >;
-  protected governanceProposerContract?: GetContractReturnType<
-    typeof GovernanceProposerAbi,
-    WalletClient<HttpTransport, Chain, PrivateKeyAccount>
-  > = undefined;
 
   protected publicClient: PublicClient<HttpTransport, Chain>;
   protected walletClient: WalletClient<HttpTransport, Chain, PrivateKeyAccount>;
@@ -172,7 +178,7 @@ export class L1Publisher {
   private readonly l1TxUtils: L1TxUtils;
 
   constructor(
-    config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'> & L1TxUtilsConfig,
+    config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
     client: TelemetryClient,
   ) {
     this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000;
@@ -199,16 +205,31 @@ export class L1Publisher {
     });
 
     if (l1Contracts.governanceProposerAddress) {
-      this.governanceProposerContract = getContract({
-        address: getAddress(l1Contracts.governanceProposerAddress.toString()),
-        abi: GovernanceProposerAbi,
-        client: this.walletClient,
-      });
+      this.governanceProposerAddress = EthAddress.fromString(l1Contracts.governanceProposerAddress.toString());
     }
 
     this.l1TxUtils = new L1TxUtils(this.publicClient, this.walletClient, this.log, config);
   }
 
+  public registerSlashPayloadGetter(callback: GetSlashPayloadCallBack) {
+    this.getSlashPayload = callback;
+  }
+
+  private async getSlashingProposerAddress() {
+    if (this.slashingProposerAddress) {
+      return this.slashingProposerAddress;
+    }
+
+    const slasherAddress = await this.rollupContract.read.SLASHER();
+    const slasher = getContract({
+      address: getAddress(slasherAddress.toString()),
+      abi: SlasherAbi,
+      client: this.walletClient,
+    });
+    this.slashingProposerAddress = EthAddress.fromString(await slasher.read.PROPOSER());
+    return this.slashingProposerAddress;
+  }
+
   get publisherAddress() {
     return this.account.address;
   }
@@ -224,12 +245,12 @@ export class L1Publisher {
     });
   }
 
-  public getPayLoad() {
-    return this.payload;
+  public getGovernancePayload() {
+    return this.governancePayload;
   }
 
-  public setPayload(payload: EthAddress) {
-    this.payload = payload;
+  public setGovernancePayload(payload: EthAddress) {
+    this.governancePayload = payload;
   }
 
   public getSenderAddress(): EthAddress {
@@ -411,68 +432,106 @@ export class L1Publisher {
       calldataGas: getCalldataGasUsage(calldata),
     };
   }
-
-  public async castVote(slotNumber: bigint, timestamp: bigint): Promise<boolean> {
-    if (this.payload.equals(EthAddress.ZERO)) {
+  public async castVote(slotNumber: bigint, timestamp: bigint, voteType: VoteType) {
+    // @todo This function can be optimized by doing some of the computations locally instead of calling the L1 contracts
+    if (this.myLastVotes[voteType] >= slotNumber) {
       return false;
     }
 
-    if (!this.governanceProposerContract) {
-      return false;
-    }
+    const voteConfig = async (): Promise<
+      { payload: EthAddress; voteContractAddress: EthAddress; logger: Logger } | undefined
+    > => {
+      if (voteType === VoteType.GOVERNANCE) {
+        if (this.governancePayload.equals(EthAddress.ZERO)) {
+          return undefined;
+        }
+        if (!this.governanceProposerAddress) {
+          return undefined;
+        }
+        return {
+          payload: this.governancePayload,
+          voteContractAddress: this.governanceProposerAddress,
+          logger: this.governanceLog,
+        };
+      } else if (voteType === VoteType.SLASHING) {
+        if (!this.getSlashPayload) {
+          return undefined;
+        }
+        const slashingProposerAddress = await this.getSlashingProposerAddress();
+        if (!slashingProposerAddress) {
+          return undefined;
+        }
+
+        const slashPayload = await this.getSlashPayload(slotNumber);
+
+        if (!slashPayload) {
+          return undefined;
+        }
+
+        return {
+          payload: slashPayload,
+          voteContractAddress: slashingProposerAddress,
+          logger: this.slashingLog,
+        };
+      } else {
+        throw new Error('Invalid vote type');
+      }
+    };
 
-    if (this.myLastVote >= slotNumber) {
+    const vConfig = await voteConfig();
+
+    if (!vConfig) {
       return false;
     }
 
-    // @todo This can be optimized A LOT by doing the computation instead of making calls to L1, but it is  very convenient
-    // for when we keep changing the values and don't want to have multiple versions of the same logic implemented.
+    const { payload, voteContractAddress, logger } = vConfig;
+
+    const voteContract = getContract({
+      address: getAddress(voteContractAddress.toString()),
+      abi: EmpireBaseAbi,
+      client: this.walletClient,
+    });
 
     const [proposer, roundNumber] = await Promise.all([
       this.rollupContract.read.getProposerAt([timestamp]),
-      this.governanceProposerContract.read.computeRound([slotNumber]),
+      voteContract.read.computeRound([slotNumber]),
     ]);
 
     if (proposer.toLowerCase() !== this.account.address.toLowerCase()) {
       return false;
     }
 
-    const [slotForLastVote] = await this.governanceProposerContract.read.rounds([
-      this.rollupContract.address,
-      roundNumber,
-    ]);
+    const [slotForLastVote] = await voteContract.read.rounds([this.rollupContract.address, roundNumber]);
 
     if (slotForLastVote >= slotNumber) {
       return false;
     }
 
-    // Storing these early such that a quick entry again would not send another tx,
-    // revert the state if there is a failure.
-    const cachedMyLastVote = this.myLastVote;
-    this.myLastVote = slotNumber;
-
-    this.governanceLog.verbose(`Casting vote for ${this.payload}`);
+    const cachedMyLastVote = this.myLastVotes[voteType];
+    this.myLastVotes[voteType] = slotNumber;
 
     let txHash;
     try {
-      txHash = await this.governanceProposerContract.write.vote([this.payload.toString()], { account: this.account });
+      txHash = await voteContract.write.vote([payload.toString()], {
+        account: this.account,
+      });
     } catch (err) {
       const msg = prettyLogViemErrorMsg(err);
-      this.governanceLog.error(`Failed to vote`, msg);
-      this.myLastVote = cachedMyLastVote;
+      logger.error(`Failed to vote`, msg);
+      this.myLastVotes[voteType] = cachedMyLastVote;
       return false;
     }
 
     if (txHash) {
       const receipt = await this.getTransactionReceipt(txHash);
       if (!receipt) {
-        this.governanceLog.warn(`Failed to get receipt for tx ${txHash}`);
-        this.myLastVote = cachedMyLastVote;
+        logger.warn(`Failed to get receipt for tx ${txHash}`);
+        this.myLastVotes[voteType] = cachedMyLastVote;
         return false;
       }
     }
 
-    this.governanceLog.info(`Cast vote for ${this.payload}`);
+    logger.info(`Cast vote for ${payload}`);
     return true;
   }
 
diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
index 1807a42a5e1a..4b1ddf4d720d 100644
--- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
+++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
@@ -46,6 +46,7 @@ import { type MockProxy, mock, mockFn } from 'jest-mock-extended';
 
 import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
 import { type L1Publisher } from '../publisher/l1-publisher.js';
+import { type SlasherClient } from '../slasher/index.js';
 import { TxValidatorFactory } from '../tx_validator/tx_validator_factory.js';
 import { Sequencer } from './sequencer.js';
 import { SequencerState } from './utils.js';
@@ -187,6 +188,8 @@ describe('sequencer', () => {
       createBlockProposal: mockFn().mockResolvedValue(createBlockProposal()),
     });
 
+    const slasherClient = mock<SlasherClient>();
+
     const l1GenesisTime = Math.floor(Date.now() / 1000);
     sequencer = new TestSubject(
       publisher,
@@ -195,6 +198,7 @@ describe('sequencer', () => {
       globalVariableBuilder,
       p2p,
       worldState,
+      slasherClient,
       blockBuilderFactory,
       l2BlockSource,
       l1ToL2MessageSource,
diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts
index d13515124d3a..d69b6603fcf2 100644
--- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts
+++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts
@@ -35,8 +35,9 @@ import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec
 import { type ValidatorClient } from '@aztec/validator-client';
 
 import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
-import { type L1Publisher } from '../publisher/l1-publisher.js';
+import { type L1Publisher, VoteType } from '../publisher/l1-publisher.js';
 import { prettyLogViemErrorMsg } from '../publisher/utils.js';
+import { type SlasherClient } from '../slasher/slasher_client.js';
 import { type TxValidatorFactory } from '../tx_validator/tx_validator_factory.js';
 import { type SequencerConfig } from './config.js';
 import { SequencerMetrics } from './metrics.js';
@@ -100,6 +101,7 @@ export class Sequencer {
     private globalsBuilder: GlobalVariableBuilder,
     private p2pClient: P2P,
     private worldState: WorldStateSynchronizer,
+    private slasherClient: SlasherClient,
     private blockBuilderFactory: BlockBuilderFactory,
     private l2BlockSource: L2BlockSource,
     private l1ToL2MessageSource: L1ToL2MessageSource,
@@ -116,6 +118,9 @@ export class Sequencer {
 
     // Register the block builder with the validator client for re-execution
     this.validatorClient?.registerBlockBuilder(this.buildBlock.bind(this));
+
+    // Register the slasher on the publisher to fetch slashing payloads
+    this.publisher.registerSlashPayloadGetter(this.slasherClient.getSlashPayload.bind(this.slasherClient));
   }
 
   get tracer(): Tracer {
@@ -160,7 +165,7 @@ export class Sequencer {
       this.maxBlockSizeInBytes = config.maxBlockSizeInBytes;
     }
     if (config.governanceProposerPayload) {
-      this.publisher.setPayload(config.governanceProposerPayload);
+      this.publisher.setGovernancePayload(config.governanceProposerPayload);
     }
     this.enforceTimeTable = config.enforceTimeTable === true;
 
@@ -206,6 +211,7 @@ export class Sequencer {
     this.log.debug(`Stopping sequencer`);
     await this.validatorClient?.stop();
     await this.runningPromise?.stop();
+    await this.slasherClient?.stop();
     this.publisher.interrupt();
     this.setState(SequencerState.STOPPED, 0n, true /** force */);
     this.log.info('Stopped sequencer');
@@ -275,7 +281,8 @@ export class Sequencer {
       slot,
     );
 
-    void this.publisher.castVote(slot, newGlobalVariables.timestamp.toBigInt());
+    void this.publisher.castVote(slot, newGlobalVariables.timestamp.toBigInt(), VoteType.GOVERNANCE);
+    void this.publisher.castVote(slot, newGlobalVariables.timestamp.toBigInt(), VoteType.SLASHING);
 
     if (!this.shouldProposeBlock(historicalHeader, {})) {
       return;
diff --git a/yarn-project/sequencer-client/src/slasher/index.ts b/yarn-project/sequencer-client/src/slasher/index.ts
new file mode 100644
index 000000000000..7a1631a7727d
--- /dev/null
+++ b/yarn-project/sequencer-client/src/slasher/index.ts
@@ -0,0 +1,24 @@
+import type { L2BlockSource } from '@aztec/circuit-types';
+import { type L1ContractsConfig, type L1ReaderConfig } from '@aztec/ethereum';
+import { createLogger } from '@aztec/foundation/log';
+import { type AztecKVStore } from '@aztec/kv-store';
+import { type DataStoreConfig } from '@aztec/kv-store/config';
+import { createStore } from '@aztec/kv-store/lmdb';
+import { type TelemetryClient } from '@aztec/telemetry-client';
+import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
+
+import { SlasherClient } from './slasher_client.js';
+import { type SlasherConfig } from './slasher_client.js';
+
+export * from './slasher_client.js';
+
+export const createSlasherClient = async (
+  _config: SlasherConfig & DataStoreConfig & L1ContractsConfig & L1ReaderConfig,
+  l2BlockSource: L2BlockSource,
+  telemetry: TelemetryClient = new NoopTelemetryClient(),
+  deps: { store?: AztecKVStore } = {},
+) => {
+  const config = { ..._config };
+  const store = deps.store ?? (await createStore('slasher', config, createLogger('slasher:lmdb')));
+  return new SlasherClient(config, store, l2BlockSource, telemetry);
+};
diff --git a/yarn-project/sequencer-client/src/slasher/slasher_client.test.ts b/yarn-project/sequencer-client/src/slasher/slasher_client.test.ts
new file mode 100644
index 000000000000..bb097b9da729
--- /dev/null
+++ b/yarn-project/sequencer-client/src/slasher/slasher_client.test.ts
@@ -0,0 +1,120 @@
+import { MockL2BlockSource } from '@aztec/archiver/test';
+import { L2Block } from '@aztec/circuit-types';
+import {
+  type L1ContractAddresses,
+  type L1ContractsConfig,
+  type L1ReaderConfig,
+  getL1ContractsConfigEnvVars,
+} from '@aztec/ethereum';
+import { EthAddress } from '@aztec/foundation/eth-address';
+import { retryUntil } from '@aztec/foundation/retry';
+import { sleep } from '@aztec/foundation/sleep';
+import { type AztecKVStore } from '@aztec/kv-store';
+import { openTmpStore } from '@aztec/kv-store/lmdb';
+
+import { expect } from '@jest/globals';
+
+import { SlasherClient, type SlasherConfig } from './slasher_client.js';
+
+// Most of this test are directly copied from the P2P client test.
+describe('In-Memory Slasher Client', () => {
+  let blockSource: MockL2BlockSource;
+  let kvStore: AztecKVStore;
+  let client: SlasherClient;
+  let config: SlasherConfig & L1ContractsConfig & L1ReaderConfig;
+
+  beforeEach(() => {
+    blockSource = new MockL2BlockSource();
+    blockSource.createBlocks(100);
+
+    const l1Config = getL1ContractsConfigEnvVars();
+
+    // Need some configuration here. Can be a basic bitch config really.
+    config = {
+      ...l1Config,
+      blockCheckIntervalMS: 100,
+      blockRequestBatchSize: 20,
+      l1Contracts: {
+        slashFactoryAddress: EthAddress.ZERO,
+      } as unknown as L1ContractAddresses,
+      l1RpcUrl: 'http://127.0.0.1:8545',
+      l1ChainId: 1,
+      viemPollingIntervalMS: 1000,
+    };
+
+    kvStore = openTmpStore();
+    client = new SlasherClient(config, kvStore, blockSource);
+  });
+
+  const advanceToProvenBlock = async (getProvenBlockNumber: number, provenEpochNumber = getProvenBlockNumber) => {
+    blockSource.setProvenBlockNumber(getProvenBlockNumber);
+    blockSource.setProvenEpochNumber(provenEpochNumber);
+    await retryUntil(
+      () => Promise.resolve(client.getSyncedProvenBlockNum() >= getProvenBlockNumber),
+      'synced',
+      10,
+      0.1,
+    );
+  };
+
+  afterEach(async () => {
+    if (client.isReady()) {
+      await client.stop();
+    }
+  });
+
+  it('can start & stop', async () => {
+    expect(client.isReady()).toEqual(false);
+
+    await client.start();
+    expect(client.isReady()).toEqual(true);
+
+    await client.stop();
+    expect(client.isReady()).toEqual(false);
+  });
+
+  it('restores the previous block number it was at', async () => {
+    await client.start();
+    await client.stop();
+
+    const client2 = new SlasherClient(config, kvStore, blockSource);
+    expect(client2.getSyncedLatestBlockNum()).toEqual(client.getSyncedLatestBlockNum());
+  });
+
+  describe('Chain prunes', () => {
+    it('moves the tips on a chain reorg', async () => {
+      blockSource.setProvenBlockNumber(0);
+      await client.start();
+
+      await advanceToProvenBlock(90);
+
+      await expect(client.getL2Tips()).resolves.toEqual({
+        latest: { number: 100, hash: expect.any(String) },
+        proven: { number: 90, hash: expect.any(String) },
+        finalized: { number: 90, hash: expect.any(String) },
+      });
+
+      blockSource.removeBlocks(10);
+
+      // give the client a chance to react to the reorg
+      await sleep(100);
+
+      await expect(client.getL2Tips()).resolves.toEqual({
+        latest: { number: 90, hash: expect.any(String) },
+        proven: { number: 90, hash: expect.any(String) },
+        finalized: { number: 90, hash: expect.any(String) },
+      });
+
+      blockSource.addBlocks([L2Block.random(91), L2Block.random(92)]);
+
+      // give the client a chance to react to the new blocks
+      await sleep(100);
+
+      await expect(client.getL2Tips()).resolves.toEqual({
+        latest: { number: 92, hash: expect.any(String) },
+        proven: { number: 90, hash: expect.any(String) },
+        finalized: { number: 90, hash: expect.any(String) },
+      });
+    });
+  });
+});
diff --git a/yarn-project/sequencer-client/src/slasher/slasher_client.ts b/yarn-project/sequencer-client/src/slasher/slasher_client.ts
new file mode 100644
index 000000000000..9e2f30d012e3
--- /dev/null
+++ b/yarn-project/sequencer-client/src/slasher/slasher_client.ts
@@ -0,0 +1,408 @@
+import {
+  type L2Block,
+  type L2BlockId,
+  type L2BlockSource,
+  L2BlockStream,
+  type L2BlockStreamEvent,
+  type L2Tips,
+} from '@aztec/circuit-types';
+import { INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js/constants';
+import { type L1ContractsConfig, type L1ReaderConfig, createEthereumChain } from '@aztec/ethereum';
+import { EthAddress } from '@aztec/foundation/eth-address';
+import { createLogger } from '@aztec/foundation/log';
+import { type AztecKVStore, type AztecMap, type AztecSingleton } from '@aztec/kv-store';
+import { SlashFactoryAbi } from '@aztec/l1-artifacts';
+import { type TelemetryClient, WithTracer } from '@aztec/telemetry-client';
+import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
+
+import {
+  type Chain,
+  type GetContractReturnType,
+  type HttpTransport,
+  type PublicClient,
+  createPublicClient,
+  getAddress,
+  getContract,
+  http,
+} from 'viem';
+
+/**
+ * Enum defining the possible states of the Slasher client.
+ */
+export enum SlasherClientState {
+  IDLE,
+  SYNCHING,
+  RUNNING,
+  STOPPED,
+}
+
+/**
+ * The synchronization status of the Slasher client.
+ */
+export interface SlasherSyncState {
+  /**
+   * The current state of the slasher client.
+   */
+  state: SlasherClientState;
+  /**
+   * The block number that the slasher client is synced to.
+   */
+  syncedToL2Block: L2BlockId;
+}
+
+export interface SlasherConfig {
+  blockCheckIntervalMS: number;
+  blockRequestBatchSize: number;
+}
+
+type SlashEvent = {
+  epoch: bigint;
+  amount: bigint;
+  lifetime: bigint;
+};
+
+/**
+ * @notice A Hypomeiones slasher client implementation
+ *
+ * Hypomeiones: a class of individuals in ancient Sparta who were considered inferior or lesser citizens compared
+ * to the full Spartan citizens.
+ *
+ * The implementation here is less than ideal. It exists, not to be the end all be all, but to show that
+ * slashing can be done with this mechanism.
+ *
+ * The implementation is VERY brute in the sense that it only looks for pruned blocks and then tries to slash
+ * the full committee of that.
+ * If it sees a prune, it will mark the full epoch as "to be slashed" and the
+ *
+ * Also, it is not particularly smart around what it should if there were to be multiple slashing events.
+ *
+ * A few improvements:
+ * - Only vote on the proposal if it is possible to reach, e.g., if 6 votes are needed and only 4 slots are left don't vote.
+ * - Stop voting on a payload once it is processed.
+ * - Only vote on the proposal if it have not already been executed
+ *  - Caveat, we need to fully decide if it is acceptable to have the same payload address multiple times. In the current
+ *    slash factory that could mean slashing the same committee for the same error multiple times.
+ * - Decide how to deal with multiple slashing events in the same round.
+ *  - This could be that multiple epochs are pruned in the same round, but with the current naive implementation we could end up
+ *    slashing only the first, because the "lifetime" of the second would have passed after that vote
+ */
+export class SlasherClient extends WithTracer {
+  private currentState = SlasherClientState.IDLE;
+  private syncPromise = Promise.resolve();
+  private syncResolve?: () => void = undefined;
+  private latestBlockNumberAtStart = -1;
+  private provenBlockNumberAtStart = -1;
+
+  private synchedBlockSlots: AztecMap<number, bigint>;
+  private synchedBlockHashes: AztecMap<number, string>;
+  private synchedLatestBlockNumber: AztecSingleton<number>;
+  private synchedProvenBlockNumber: AztecSingleton<number>;
+
+  private blockStream;
+
+  private slashEvents: SlashEvent[] = [];
+
+  protected slashFactoryContract?: GetContractReturnType<typeof SlashFactoryAbi, PublicClient<HttpTransport, Chain>> =
+    undefined;
+
+  // The amount to slash for a prune.
+  // Note that we set it to 0, such that no actual slashing will happen, but the event will be fired,
+  // showing that the slashing mechanism is working.
+  private slashingAmount: bigint = 0n;
+
+  constructor(
+    private config: SlasherConfig & L1ContractsConfig & L1ReaderConfig,
+    private store: AztecKVStore,
+    private l2BlockSource: L2BlockSource,
+    telemetry: TelemetryClient = new NoopTelemetryClient(),
+    private log = createLogger('slasher'),
+  ) {
+    super(telemetry, 'slasher');
+
+    this.blockStream = new L2BlockStream(l2BlockSource, this, this, createLogger('slasher:block_stream'), {
+      batchSize: config.blockRequestBatchSize,
+      pollIntervalMS: config.blockCheckIntervalMS,
+    });
+
+    this.synchedBlockHashes = store.openMap('slasher_block_hashes');
+    this.synchedBlockSlots = store.openMap('slasher_block_slots');
+    this.synchedLatestBlockNumber = store.openSingleton('slasher_last_l2_block');
+    this.synchedProvenBlockNumber = store.openSingleton('slasher_last_proven_l2_block');
+
+    if (config.l1Contracts.slashFactoryAddress && config.l1Contracts.slashFactoryAddress !== EthAddress.ZERO) {
+      const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId);
+      const publicClient = createPublicClient({
+        chain: chain.chainInfo,
+        transport: http(chain.rpcUrl),
+        pollingInterval: config.viemPollingIntervalMS,
+      });
+
+      this.slashFactoryContract = getContract({
+        address: getAddress(config.l1Contracts.slashFactoryAddress.toString()),
+        abi: SlashFactoryAbi,
+        client: publicClient,
+      });
+    }
+
+    this.log.info(`Slasher client initialized`);
+  }
+
+  // This is where we should put a bunch of the improvements mentioned earlier.
+  public async getSlashPayload(slotNumber: bigint): Promise<EthAddress | undefined> {
+    if (!this.slashFactoryContract) {
+      return undefined;
+    }
+
+    // As long as the slot is greater than the lifetime, we want to keep deleting the first element
+    // since it will not make sense to include anymore.
+    while (this.slashEvents.length > 0 && this.slashEvents[0].lifetime < slotNumber) {
+      this.slashEvents.shift();
+    }
+
+    if (this.slashEvents.length == 0) {
+      return undefined;
+    }
+
+    const slashEvent = this.slashEvents[0];
+
+    const [payloadAddress, isDeployed] = await this.slashFactoryContract.read.getAddressAndIsDeployed([
+      slashEvent.epoch,
+      slashEvent.amount,
+    ]);
+
+    if (!isDeployed) {
+      // The proposal cannot be executed until it is deployed
+      this.log.verbose(`Voting on not yet deployed payload: ${payloadAddress}`);
+    }
+
+    return EthAddress.fromString(payloadAddress);
+  }
+
+  public getL2BlockHash(number: number): Promise<string | undefined> {
+    return Promise.resolve(this.synchedBlockHashes.get(number));
+  }
+
+  public getL2Tips(): Promise<L2Tips> {
+    const latestBlockNumber = this.getSyncedLatestBlockNum();
+    let latestBlockHash: string | undefined;
+    const provenBlockNumber = this.getSyncedProvenBlockNum();
+    let provenBlockHash: string | undefined;
+
+    if (latestBlockNumber > 0) {
+      latestBlockHash = this.synchedBlockHashes.get(latestBlockNumber);
+      if (typeof latestBlockHash === 'undefined') {
+        this.log.warn(`Block hash for latest block ${latestBlockNumber} not found`);
+        throw new Error();
+      }
+    }
+
+    if (provenBlockNumber > 0) {
+      provenBlockHash = this.synchedBlockHashes.get(provenBlockNumber);
+      if (typeof provenBlockHash === 'undefined') {
+        this.log.warn(`Block hash for proven block ${provenBlockNumber} not found`);
+        throw new Error();
+      }
+    }
+
+    return Promise.resolve({
+      latest: { hash: latestBlockHash!, number: latestBlockNumber },
+      proven: { hash: provenBlockHash!, number: provenBlockNumber },
+      finalized: { hash: provenBlockHash!, number: provenBlockNumber },
+    });
+  }
+
+  public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
+    this.log.debug(`Handling block stream event ${event.type}`);
+    switch (event.type) {
+      case 'blocks-added':
+        await this.handleLatestL2Blocks(event.blocks);
+        break;
+      case 'chain-finalized':
+        // TODO (alexg): I think we can prune the block hashes map here
+        break;
+      case 'chain-proven': {
+        const from = this.getSyncedProvenBlockNum() + 1;
+        const limit = event.blockNumber - from + 1;
+        await this.handleProvenL2Blocks(await this.l2BlockSource.getBlocks(from, limit));
+        break;
+      }
+      case 'chain-pruned':
+        await this.handlePruneL2Blocks(event.blockNumber);
+        break;
+      default: {
+        const _: never = event;
+        break;
+      }
+    }
+  }
+
+  public async start() {
+    if (this.currentState === SlasherClientState.STOPPED) {
+      throw new Error('Slasher already stopped');
+    }
+    if (this.currentState !== SlasherClientState.IDLE) {
+      return this.syncPromise;
+    }
+
+    // get the current latest block numbers
+    this.latestBlockNumberAtStart = await this.l2BlockSource.getBlockNumber();
+    this.provenBlockNumberAtStart = await this.l2BlockSource.getProvenBlockNumber();
+
+    const syncedLatestBlock = this.getSyncedLatestBlockNum() + 1;
+    const syncedProvenBlock = this.getSyncedProvenBlockNum() + 1;
+
+    // if there are blocks to be retrieved, go to a synching state
+    if (syncedLatestBlock <= this.latestBlockNumberAtStart || syncedProvenBlock <= this.provenBlockNumberAtStart) {
+      this.setCurrentState(SlasherClientState.SYNCHING);
+      this.syncPromise = new Promise(resolve => {
+        this.syncResolve = resolve;
+      });
+      this.log.verbose(`Starting sync from ${syncedLatestBlock} (last proven ${syncedProvenBlock})`);
+    } else {
+      // if no blocks to be retrieved, go straight to running
+      this.setCurrentState(SlasherClientState.RUNNING);
+      this.syncPromise = Promise.resolve();
+      this.log.verbose(`Block ${syncedLatestBlock} (proven ${syncedProvenBlock}) already beyond current block`);
+    }
+
+    this.blockStream.start();
+    this.log.verbose(`Started block downloader from block ${syncedLatestBlock}`);
+
+    return this.syncPromise;
+  }
+
+  /**
+   * Allows consumers to stop the instance of the slasher client.
+   * 'ready' will now return 'false' and the running promise that keeps the client synced is interrupted.
+   */
+  public async stop() {
+    this.log.debug('Stopping Slasher client...');
+    await this.blockStream.stop();
+    this.log.debug('Stopped block downloader');
+    this.setCurrentState(SlasherClientState.STOPPED);
+    this.log.info('Slasher client stopped.');
+  }
+
+  /**
+   * Public function to check if the slasher client is fully synced and ready to receive txs.
+   * @returns True if the slasher client is ready to receive txs.
+   */
+  public isReady() {
+    return this.currentState === SlasherClientState.RUNNING;
+  }
+
+  /**
+   * Public function to check the latest block number that the slasher client is synced to.
+   * @returns Block number of latest L2 Block we've synced with.
+   */
+  public getSyncedLatestBlockNum() {
+    return this.synchedLatestBlockNumber.get() ?? INITIAL_L2_BLOCK_NUM - 1;
+  }
+
+  /**
+   * Public function to check the latest proven block number that the slasher client is synced to.
+   * @returns Block number of latest proven L2 Block we've synced with.
+   */
+  public getSyncedProvenBlockNum() {
+    return this.synchedProvenBlockNumber.get() ?? INITIAL_L2_BLOCK_NUM - 1;
+  }
+
+  /**
+   * Method to check the status of the slasher client.
+   * @returns Information about slasher client status: state & syncedToBlockNum.
+   */
+  public async getStatus(): Promise<SlasherSyncState> {
+    const blockNumber = this.getSyncedLatestBlockNum();
+    const blockHash =
+      blockNumber == 0
+        ? ''
+        : await this.l2BlockSource.getBlockHeader(blockNumber).then(header => header?.hash().toString());
+    return Promise.resolve({
+      state: this.currentState,
+      syncedToL2Block: { number: blockNumber, hash: blockHash },
+    } as SlasherSyncState);
+  }
+
+  /**
+   * Handles new mined blocks by marking the txs in them as mined.
+   * @param blocks - A list of existing blocks with txs that the slasher client needs to ensure the tx pool is reconciled with.
+   * @returns Empty promise.
+   */
+  private async handleLatestL2Blocks(blocks: L2Block[]): Promise<void> {
+    if (!blocks.length) {
+      return Promise.resolve();
+    }
+
+    const lastBlockNum = blocks[blocks.length - 1].number;
+    await Promise.all(blocks.map(block => this.synchedBlockHashes.set(block.number, block.hash().toString())));
+    await Promise.all(
+      blocks.map(block => this.synchedBlockSlots.set(block.number, block.header.globalVariables.slotNumber.toBigInt())),
+    );
+    await this.synchedLatestBlockNumber.set(lastBlockNum);
+    this.log.debug(`Synched to latest block ${lastBlockNum}`);
+    this.startServiceIfSynched();
+  }
+
+  /**
+   * Handles new proven blocks by deleting the txs in them, or by deleting the txs in blocks `keepProvenTxsFor` ago.
+   * @param blocks - A list of proven L2 blocks.
+   * @returns Empty promise.
+   */
+  private async handleProvenL2Blocks(blocks: L2Block[]): Promise<void> {
+    if (!blocks.length) {
+      return Promise.resolve();
+    }
+    const lastBlockNum = blocks[blocks.length - 1].number;
+    await this.synchedProvenBlockNumber.set(lastBlockNum);
+    this.log.debug(`Synched to proven block ${lastBlockNum}`);
+
+    this.startServiceIfSynched();
+  }
+
+  /**
+   * Updates the tx pool after a chain prune.
+   * @param latestBlock - The block number the chain was pruned to.
+   */
+  private async handlePruneL2Blocks(latestBlock: number): Promise<void> {
+    const slotNumber = this.synchedBlockSlots.get(latestBlock) ?? BigInt(0);
+    const epochNumber = slotNumber / BigInt(this.config.aztecEpochDuration);
+    this.log.info(`Detected chain prune. Punishing the validators at epoch ${epochNumber}`);
+
+    // Set the lifetime such that we have a full round that we could vote throughout.
+    const slotsIntoRound = slotNumber % BigInt(this.config.slashingRoundSize);
+    const toNext = slotsIntoRound == 0n ? 0n : BigInt(this.config.slashingRoundSize) - slotsIntoRound;
+
+    const lifetime = slotNumber + toNext + BigInt(this.config.slashingRoundSize);
+
+    this.slashEvents.push({
+      epoch: epochNumber,
+      amount: this.slashingAmount,
+      lifetime,
+    });
+
+    await this.synchedLatestBlockNumber.set(latestBlock);
+  }
+
+  private startServiceIfSynched() {
+    if (
+      this.currentState === SlasherClientState.SYNCHING &&
+      this.getSyncedLatestBlockNum() >= this.latestBlockNumberAtStart &&
+      this.getSyncedProvenBlockNum() >= this.provenBlockNumberAtStart
+    ) {
+      this.log.debug(`Synched to blocks at start`);
+      this.setCurrentState(SlasherClientState.RUNNING);
+      if (this.syncResolve !== undefined) {
+        this.syncResolve();
+      }
+    }
+  }
+
+  /**
+   * Method to set the value of the current state.
+   * @param newState - New state value.
+   */
+  private setCurrentState(newState: SlasherClientState) {
+    this.currentState = newState;
+    this.log.debug(`Moved to state ${SlasherClientState[this.currentState]}`);
+  }
+}
diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock
index 9a83f7b8135f..c11117fa6d64 100644
--- a/yarn-project/yarn.lock
+++ b/yarn-project/yarn.lock
@@ -1151,6 +1151,7 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@aztec/sequencer-client@workspace:sequencer-client"
   dependencies:
+    "@aztec/archiver": "workspace:^"
     "@aztec/aztec.js": "workspace:^"
     "@aztec/bb-prover": "workspace:^"
     "@aztec/circuit-types": "workspace:^"