From 111f3247c33524308796ea5f6763c5f6611875ee Mon Sep 17 00:00:00 2001
From: LHerskind <lasse.herskind@gmail.com>
Date: Wed, 2 Oct 2024 10:46:57 +0000
Subject: [PATCH] feat: gerousia

---
 l1-contracts/src/governance/Gerousia.sol      | 154 +++++++++
 .../src/governance/interfaces/IApella.sol     |   6 +
 .../src/governance/interfaces/IGerousia.sol   |  17 +
 .../src/governance/libraries/Errors.sol       |  16 +-
 .../test/governance/gerousia/Base.t.sol       |  39 +++
 .../governance/gerousia/constructor.t.sol     |  45 +++
 .../test/governance/gerousia/constructor.tree |   7 +
 .../governance/gerousia/mocks/FalsyApella.sol |  10 +
 .../gerousia/mocks/FaultyApella.sol           |  13 +
 .../governance/gerousia/pushProposal.t.sol    | 243 ++++++++++++++
 .../governance/gerousia/pushProposal.tree     |  29 ++
 .../test/governance/gerousia/vote.t.sol       | 301 ++++++++++++++++++
 .../test/governance/gerousia/vote.tree        |  39 +++
 13 files changed, 918 insertions(+), 1 deletion(-)
 create mode 100644 l1-contracts/src/governance/Gerousia.sol
 create mode 100644 l1-contracts/src/governance/interfaces/IApella.sol
 create mode 100644 l1-contracts/src/governance/interfaces/IGerousia.sol
 create mode 100644 l1-contracts/test/governance/gerousia/Base.t.sol
 create mode 100644 l1-contracts/test/governance/gerousia/constructor.t.sol
 create mode 100644 l1-contracts/test/governance/gerousia/constructor.tree
 create mode 100644 l1-contracts/test/governance/gerousia/mocks/FalsyApella.sol
 create mode 100644 l1-contracts/test/governance/gerousia/mocks/FaultyApella.sol
 create mode 100644 l1-contracts/test/governance/gerousia/pushProposal.t.sol
 create mode 100644 l1-contracts/test/governance/gerousia/pushProposal.tree
 create mode 100644 l1-contracts/test/governance/gerousia/vote.t.sol
 create mode 100644 l1-contracts/test/governance/gerousia/vote.tree

diff --git a/l1-contracts/src/governance/Gerousia.sol b/l1-contracts/src/governance/Gerousia.sol
new file mode 100644
index 00000000000..1307be7929e
--- /dev/null
+++ b/l1-contracts/src/governance/Gerousia.sol
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";
+import {IApella} from "@aztec/governance/interfaces/IApella.sol";
+import {IGerousia} from "@aztec/governance/interfaces/IGerousia.sol";
+import {Errors} from "@aztec/governance/libraries/Errors.sol";
+
+import {Slot, SlotLib} from "@aztec/core/libraries/TimeMath.sol";
+import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol";
+
+/**
+ * @notice  A Gerousia 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 exampel going optimistic.
+ */
+contract Gerousia is IGerousia {
+  using SlotLib for Slot;
+
+  struct RoundAccounting {
+    Slot lastVote;
+    address leader;
+    bool executed;
+    mapping(address proposal => uint256 count) yeaCount;
+  }
+
+  uint256 public constant LIFETIME_IN_ROUNDS = 5;
+
+  IApella public immutable APELLA;
+  IRegistry public immutable REGISTRY;
+  uint256 public immutable N;
+  uint256 public immutable M;
+
+  mapping(address instance => mapping(uint256 roundNumber => RoundAccounting)) public rounds;
+
+  constructor(IApella _apella, IRegistry _registry, uint256 _n, uint256 _m) {
+    APELLA = _apella;
+    REGISTRY = _registry;
+    N = _n;
+    M = _m;
+
+    require(N > M / 2, Errors.Gerousia__InvalidNAndMValues(N, M));
+    require(N <= M, Errors.Gerousia__NCannotBeLargerTHanM(N, M));
+  }
+
+  // Note that this one is heavily realying on the fact that this contract
+  // could be updated at the same time as another upgrade is made.
+
+  /**
+   * @notice	Cast a vote on a proposal
+   *					Note that this is assuming that the canonical rollup will cast it as
+   * 					part of block production, we will perform it here
+   *
+   * @param _proposal - The proposal to cast a vote on
+   *
+   * @return True if executed successfully, false otherwise
+   */
+  function vote(address _proposal) external override(IGerousia) returns (bool) {
+    require(_proposal.code.length > 0, Errors.Gerousia__ProposalHaveNoCode(_proposal));
+
+    address instance = REGISTRY.getRollup();
+    require(instance.code.length > 0, Errors.Gerousia__InstanceHaveNoCode(instance));
+
+    ILeonidas selection = ILeonidas(instance);
+    Slot currentSlot = selection.getCurrentSlot();
+
+    uint256 roundNumber = computeRound(currentSlot);
+
+    RoundAccounting storage round = rounds[instance][roundNumber];
+
+    require(currentSlot > round.lastVote, Errors.Gerousia__VoteAlreadyCastForSlot(currentSlot));
+
+    address proposer = selection.getCurrentProposer();
+    require(msg.sender == proposer, Errors.Gerousia__OnlyProposerCanVote(msg.sender, proposer));
+
+    round.yeaCount[_proposal] += 1;
+    round.lastVote = currentSlot;
+
+    // @todo We can optimise here for gas by storing some of it packed with the leader.
+    if (round.leader != _proposal && round.yeaCount[_proposal] > round.yeaCount[round.leader]) {
+      round.leader = _proposal;
+    }
+
+    emit VoteCast(_proposal, roundNumber, msg.sender);
+
+    return true;
+  }
+
+  /**
+   * @notice  Push the proposal to the appela
+   *
+   * @param _roundNumber - The round number to execute
+   *
+   * @return True if executed successfully, false otherwise
+   */
+  function pushProposal(uint256 _roundNumber) external override(IGerousia) returns (bool) {
+    // Need to ensure that the round is not active.
+    address instance = REGISTRY.getRollup();
+    require(instance.code.length > 0, Errors.Gerousia__InstanceHaveNoCode(instance));
+
+    ILeonidas selection = ILeonidas(instance);
+    Slot currentSlot = selection.getCurrentSlot();
+
+    uint256 currentRound = computeRound(currentSlot);
+    require(_roundNumber < currentRound, Errors.Gerousia__CanOnlyPushProposalInPast());
+    require(
+      _roundNumber + LIFETIME_IN_ROUNDS >= currentRound,
+      Errors.Gerousia__ProposalTooOld(_roundNumber)
+    );
+
+    RoundAccounting storage round = rounds[instance][_roundNumber];
+    require(!round.executed, Errors.Gerousia__ProposalAlreadyExecuted(_roundNumber));
+    require(round.leader != address(0), Errors.Gerousia__ProposalCannotBeAddressZero());
+    require(round.yeaCount[round.leader] >= N, Errors.Gerousia__InsufficientVotes());
+
+    round.executed = true;
+
+    emit ProposalPushed(round.leader, _roundNumber);
+
+    require(APELLA.propose(round.leader), Errors.Gerousia__FailedToPropose(round.leader));
+    return true;
+  }
+
+  /**
+   * @notice  Fetch the yea count for a specific proposal in a specific round on a specific instance
+   *
+   * @param _instance - The address of the instance
+   * @param _round - The round to lookup
+   * @param _proposal - The address of the proposal
+   *
+   * @return The number of yea votes
+   */
+  function yeaCount(address _instance, uint256 _round, address _proposal)
+    external
+    view
+    override(IGerousia)
+    returns (uint256)
+  {
+    return rounds[_instance][_round].yeaCount[_proposal];
+  }
+
+  /**
+   * @notice  Computes the round at the given slot
+   *
+   * @param _slot - The slot to compute round for
+   *
+   * @return The round number
+   */
+  function computeRound(Slot _slot) public view override(IGerousia) returns (uint256) {
+    return _slot.unwrap() / M;
+  }
+}
diff --git a/l1-contracts/src/governance/interfaces/IApella.sol b/l1-contracts/src/governance/interfaces/IApella.sol
new file mode 100644
index 00000000000..15728b7143f
--- /dev/null
+++ b/l1-contracts/src/governance/interfaces/IApella.sol
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+interface IApella {
+  function propose(address _proposal) external returns (bool);
+}
diff --git a/l1-contracts/src/governance/interfaces/IGerousia.sol b/l1-contracts/src/governance/interfaces/IGerousia.sol
new file mode 100644
index 00000000000..bbd8a6c2066
--- /dev/null
+++ b/l1-contracts/src/governance/interfaces/IGerousia.sol
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {Slot} from "@aztec/core/libraries/TimeMath.sol";
+
+interface IGerousia {
+  event VoteCast(address indexed proposal, uint256 indexed round, address indexed voter);
+  event ProposalPushed(address indexed proposal, uint256 indexed round);
+
+  function vote(address _proposa) external returns (bool);
+  function pushProposal(uint256 _roundNumber) external returns (bool);
+  function yeaCount(address _instance, uint256 _round, address _proposal)
+    external
+    view
+    returns (uint256);
+  function computeRound(Slot _slot) external view returns (uint256);
+}
diff --git a/l1-contracts/src/governance/libraries/Errors.sol b/l1-contracts/src/governance/libraries/Errors.sol
index 80a2c29e8f2..41611a86eec 100644
--- a/l1-contracts/src/governance/libraries/Errors.sol
+++ b/l1-contracts/src/governance/libraries/Errors.sol
@@ -2,6 +2,8 @@
 // Copyright 2023 Aztec Labs.
 pragma solidity >=0.8.27;
 
+import {Slot} from "@aztec/core/libraries/TimeMath.sol";
+
 /**
  * @title Errors Library
  * @author Aztec Labs
@@ -10,7 +12,19 @@ pragma solidity >=0.8.27;
  * when there are multiple contracts that could have thrown the error.
  */
 library Errors {
-  // Registry
+  error Gerousia__CanOnlyPushProposalInPast(); // 0x49fdf611"
+  error Gerousia__FailedToPropose(address proposal); // 0x6ca2a2ed
+  error Gerousia__InstanceHaveNoCode(address instance); // 0x20a3b441
+  error Gerousia__InsufficientVotes(); // 0xba1e05ef
+  error Gerousia__InvalidNAndMValues(uint256 N, uint256 M); // 0x520d9704
+  error Gerousia__NCannotBeLargerTHanM(uint256 N, uint256 M); // 0x2fdfc063
+  error Gerousia__OnlyProposerCanVote(address caller, address proposer); // 0xba27df38
+  error Gerousia__ProposalAlreadyExecuted(uint256 roundNumber); // 0x7aeacb17
+  error Gerousia__ProposalCannotBeAddressZero(); // 0xdb3e4b6e
+  error Gerousia__ProposalHaveNoCode(address proposal); // 0xdce0615b
+  error Gerousia__ProposalTooOld(uint256 roundNumber); //0x02283b1a
+  error Gerousia__VoteAlreadyCastForSlot(Slot slot); //0xc2201452
+
   error Nomismatokopio__InssuficientMintAvailable(uint256 available, uint256 needed); // 0xf268b931
 
   error Registry__RollupAlreadyRegistered(address rollup); // 0x3c34eabf
diff --git a/l1-contracts/test/governance/gerousia/Base.t.sol b/l1-contracts/test/governance/gerousia/Base.t.sol
new file mode 100644
index 00000000000..a13484f1f8e
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/Base.t.sol
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {Test} from "forge-std/Test.sol";
+
+import {Registry} from "@aztec/governance/Registry.sol";
+import {Gerousia} from "@aztec/governance/Gerousia.sol";
+
+import {IApella} from "@aztec/governance/interfaces/IApella.sol";
+
+contract FakeApella is IApella {
+  address public gerousia;
+
+  mapping(address => bool) public proposals;
+
+  function setGerousia(address _gerousia) external {
+    gerousia = _gerousia;
+  }
+
+  function propose(address _proposal) external override(IApella) returns (bool) {
+    proposals[_proposal] = true;
+    return true;
+  }
+}
+
+contract GerousiaBase is Test {
+  Registry internal registry;
+  FakeApella internal apella;
+  Gerousia internal gerousia;
+
+  function setUp() public virtual {
+    registry = new Registry(address(this));
+    apella = new FakeApella();
+
+    gerousia = new Gerousia(apella, registry, 667, 1000);
+
+    apella.setGerousia(address(gerousia));
+  }
+}
diff --git a/l1-contracts/test/governance/gerousia/constructor.t.sol b/l1-contracts/test/governance/gerousia/constructor.t.sol
new file mode 100644
index 00000000000..e3fb6c87b01
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/constructor.t.sol
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {Test} from "forge-std/Test.sol";
+import {Gerousia} from "@aztec/governance/Gerousia.sol";
+import {Errors} from "@aztec/governance/libraries/Errors.sol";
+import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";
+import {IApella} from "@aztec/governance/interfaces/IApella.sol";
+
+contract ConstructorTest is Test {
+  IApella internal constant APELLA = IApella(address(0x01));
+  IRegistry internal constant REGISTRY = IRegistry(address(0x02));
+
+  function test_WhenNIsLessThanOrEqualHalfOfM(uint256 _n, uint256 _m) external {
+    // it revert
+
+    uint256 n = bound(_n, 0, _m / 2);
+
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__InvalidNAndMValues.selector, n, _m));
+    new Gerousia(APELLA, REGISTRY, n, _m);
+  }
+
+  function test_WhenNLargerThanM(uint256 _n, uint256 _m) external {
+    // it revert
+    uint256 m = bound(_m, 0, type(uint256).max - 1);
+    uint256 n = bound(_n, m + 1, type(uint256).max);
+
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__NCannotBeLargerTHanM.selector, n, m));
+    new Gerousia(APELLA, REGISTRY, n, m);
+  }
+
+  function test_WhenNIsGreatherThanHalfOfM(uint256 _n, uint256 _m) external {
+    // it deploys
+
+    uint256 m = bound(_m, 1, type(uint256).max);
+    uint256 n = bound(_n, m / 2 + 1, m);
+
+    Gerousia g = new Gerousia(APELLA, REGISTRY, n, m);
+
+    assertEq(address(g.APELLA()), address(APELLA));
+    assertEq(address(g.REGISTRY()), address(REGISTRY));
+    assertEq(g.N(), n);
+    assertEq(g.M(), m);
+  }
+}
diff --git a/l1-contracts/test/governance/gerousia/constructor.tree b/l1-contracts/test/governance/gerousia/constructor.tree
new file mode 100644
index 00000000000..dbbe248ce5d
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/constructor.tree
@@ -0,0 +1,7 @@
+ConstructorTest
+├── when N is less than or equal half of M
+│   └── it revert
+├── when N larger than M
+│   └── it revert
+└── when N is greather than half of M
+    └── it deploys
\ No newline at end of file
diff --git a/l1-contracts/test/governance/gerousia/mocks/FalsyApella.sol b/l1-contracts/test/governance/gerousia/mocks/FalsyApella.sol
new file mode 100644
index 00000000000..65fb4c201e7
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/mocks/FalsyApella.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {IApella} from "@aztec/governance/interfaces/IApella.sol";
+
+contract FalsyApella is IApella {
+  function propose(address) external pure override(IApella) returns (bool) {
+    return false;
+  }
+}
diff --git a/l1-contracts/test/governance/gerousia/mocks/FaultyApella.sol b/l1-contracts/test/governance/gerousia/mocks/FaultyApella.sol
new file mode 100644
index 00000000000..69e65231917
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/mocks/FaultyApella.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {IApella} from "@aztec/governance/interfaces/IApella.sol";
+
+contract FaultyApella is IApella {
+  error Faulty();
+
+  function propose(address) external pure override(IApella) returns (bool) {
+    require(false, Faulty());
+    return true;
+  }
+}
diff --git a/l1-contracts/test/governance/gerousia/pushProposal.t.sol b/l1-contracts/test/governance/gerousia/pushProposal.t.sol
new file mode 100644
index 00000000000..ee4d86d75ad
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/pushProposal.t.sol
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {IGerousia} from "@aztec/governance/interfaces/IGerousia.sol";
+import {GerousiaBase} from "./Base.t.sol";
+import {Leonidas} from "@aztec/core/Leonidas.sol";
+import {Errors} from "@aztec/governance/libraries/Errors.sol";
+import {Slot, SlotLib, Timestamp} from "@aztec/core/libraries/TimeMath.sol";
+
+import {FaultyApella} from "./mocks/FaultyApella.sol";
+import {FalsyApella} from "./mocks/FalsyApella.sol";
+
+contract PushProposalTest is GerousiaBase {
+  using SlotLib for Slot;
+
+  Leonidas internal leonidas;
+
+  address internal proposal = address(this);
+  address internal proposer = address(0);
+
+  function test_GivenCanonicalInstanceHoldNoCode(uint256 _roundNumber) external {
+    // it revert
+    vm.expectRevert(
+      abi.encodeWithSelector(Errors.Gerousia__InstanceHaveNoCode.selector, address(0xdead))
+    );
+    gerousia.pushProposal(_roundNumber);
+  }
+
+  modifier givenCanonicalInstanceHoldCode() {
+    leonidas = new Leonidas(address(this));
+    registry.upgrade(address(leonidas));
+
+    // We jump into the future since slot 0, will behave as if already voted in
+    vm.warp(Timestamp.unwrap(leonidas.getTimestampForSlot(Slot.wrap(1))));
+    _;
+  }
+
+  function test_WhenRoundNotInPast() external givenCanonicalInstanceHoldCode {
+    // it revert
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__CanOnlyPushProposalInPast.selector));
+    gerousia.pushProposal(0);
+  }
+
+  modifier whenRoundInPast() {
+    vm.warp(Timestamp.unwrap(leonidas.getTimestampForSlot(Slot.wrap(gerousia.M()))));
+    _;
+  }
+
+  function test_WhenRoundTooFarInPast() external givenCanonicalInstanceHoldCode whenRoundInPast {
+    // it revert
+
+    vm.warp(
+      Timestamp.unwrap(
+        leonidas.getTimestampForSlot(Slot.wrap((gerousia.LIFETIME_IN_ROUNDS() + 1) * gerousia.M()))
+      )
+    );
+
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__ProposalTooOld.selector, 0));
+    gerousia.pushProposal(0);
+  }
+
+  modifier whenRoundInRecentPast() {
+    _;
+  }
+
+  function test_GivenRoundAlreadyExecuted()
+    external
+    givenCanonicalInstanceHoldCode
+    whenRoundInPast
+    whenRoundInRecentPast
+  {
+    // it revert
+
+    {
+      // Need to execute a proposal first here.
+      for (uint256 i = 0; i < gerousia.N(); i++) {
+        vm.prank(proposer);
+        assertTrue(gerousia.vote(proposal));
+        vm.warp(
+          Timestamp.unwrap(leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(1)))
+        );
+      }
+      vm.warp(
+        Timestamp.unwrap(
+          leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(gerousia.M()))
+        )
+      );
+      gerousia.pushProposal(1);
+    }
+
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__ProposalAlreadyExecuted.selector, 1));
+    gerousia.pushProposal(1);
+  }
+
+  modifier givenRoundNotExecutedBefore() {
+    _;
+  }
+
+  function test_GivenLeaderIsAddress0()
+    external
+    givenCanonicalInstanceHoldCode
+    whenRoundInPast
+    whenRoundInRecentPast
+    givenRoundNotExecutedBefore
+  {
+    // it revert
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__ProposalCannotBeAddressZero.selector));
+    gerousia.pushProposal(0);
+  }
+
+  modifier givenLeaderIsNotAddress0() {
+    _;
+  }
+
+  function test_GivenInsufficientYea()
+    external
+    givenCanonicalInstanceHoldCode
+    whenRoundInPast
+    whenRoundInRecentPast
+    givenRoundNotExecutedBefore
+    givenLeaderIsNotAddress0
+  {
+    // it revert
+
+    vm.prank(proposer);
+    gerousia.vote(proposal);
+
+    vm.warp(
+      Timestamp.unwrap(
+        leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(gerousia.M()))
+      )
+    );
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__InsufficientVotes.selector));
+    gerousia.pushProposal(1);
+  }
+
+  modifier givenSufficientYea() {
+    for (uint256 i = 0; i < gerousia.N(); i++) {
+      vm.prank(proposer);
+      assertTrue(gerousia.vote(proposal));
+      vm.warp(
+        Timestamp.unwrap(leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(1)))
+      );
+    }
+    vm.warp(
+      Timestamp.unwrap(
+        leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(gerousia.M()))
+      )
+    );
+
+    _;
+  }
+
+  function test_GivenNewCanonicalInstance()
+    external
+    givenCanonicalInstanceHoldCode
+    whenRoundInPast
+    whenRoundInRecentPast
+    givenRoundNotExecutedBefore
+    givenLeaderIsNotAddress0
+    givenSufficientYea
+  {
+    // it revert
+
+    // When using a new registry we change the gerousia's interpetation of time :O
+    Leonidas freshInstance = new Leonidas(address(this));
+    registry.upgrade(address(freshInstance));
+
+    // The old is still there, just not executable.
+    (, address leader, bool executed) = gerousia.rounds(address(leonidas), 1);
+    assertFalse(executed);
+    assertEq(leader, proposal);
+
+    // As time is perceived differently, round 1 is currently in the future
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__CanOnlyPushProposalInPast.selector));
+    gerousia.pushProposal(1);
+
+    // Jump 2 rounds, since we are currently in round 0
+    vm.warp(
+      Timestamp.unwrap(
+        freshInstance.getTimestampForSlot(
+          freshInstance.getCurrentSlot() + Slot.wrap(2 * gerousia.M())
+        )
+      )
+    );
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__ProposalCannotBeAddressZero.selector));
+    gerousia.pushProposal(1);
+  }
+
+  function test_GivenApellaCallReturnFalse()
+    external
+    givenCanonicalInstanceHoldCode
+    whenRoundInPast
+    whenRoundInRecentPast
+    givenRoundNotExecutedBefore
+    givenLeaderIsNotAddress0
+    givenSufficientYea
+  {
+    // it revert
+    FalsyApella falsy = new FalsyApella();
+    vm.etch(address(apella), address(falsy).code);
+
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__FailedToPropose.selector, proposal));
+    gerousia.pushProposal(1);
+  }
+
+  function test_GivenApellaCallFails()
+    external
+    givenCanonicalInstanceHoldCode
+    whenRoundInPast
+    whenRoundInRecentPast
+    givenRoundNotExecutedBefore
+    givenLeaderIsNotAddress0
+    givenSufficientYea
+  {
+    // it revert
+    FaultyApella faulty = new FaultyApella();
+    vm.etch(address(apella), address(faulty).code);
+
+    vm.expectRevert(abi.encodeWithSelector(FaultyApella.Faulty.selector));
+    gerousia.pushProposal(1);
+  }
+
+  function test_GivenApellaCallSucceeds()
+    external
+    givenCanonicalInstanceHoldCode
+    whenRoundInPast
+    whenRoundInRecentPast
+    givenRoundNotExecutedBefore
+    givenLeaderIsNotAddress0
+    givenSufficientYea
+  {
+    // it update executed to true
+    // it emits {ProposalPushed} event
+    // it return true
+    vm.expectEmit(true, true, true, true, address(gerousia));
+    emit IGerousia.ProposalPushed(proposal, 1);
+    assertTrue(gerousia.pushProposal(1));
+    (, address leader, bool executed) = gerousia.rounds(address(leonidas), 1);
+    assertTrue(executed);
+    assertEq(leader, proposal);
+  }
+}
diff --git a/l1-contracts/test/governance/gerousia/pushProposal.tree b/l1-contracts/test/governance/gerousia/pushProposal.tree
new file mode 100644
index 00000000000..c201b0ce67a
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/pushProposal.tree
@@ -0,0 +1,29 @@
+PushProposalTest
+├── given canonical instance hold no code
+│   └── it revert
+└── given canonical instance hold code
+    ├── when round not in past
+    │   └── it revert
+    └── when round in past
+        ├── when round too far in past
+        │   └── it revert
+        └── when round in recent past
+            ├── given round already executed
+            │   └── it revert
+            └── given round not executed before
+                ├── given leader is address 0
+                │   └── it revert
+                └── given leader is not address 0
+                    ├── given insufficient yea
+                    │   └── it revert
+                    └── given sufficient yea
+                        ├── given new canonical instance
+                        │   └── it revert
+                        ├── given apella call return false
+                        │   └── it revert
+                        ├── given apella call fails
+                        │   └── it revert
+                        └── given apella call succeeds
+                            ├── it update executed to true
+                            ├── it emits {ProposalPushed} event
+                            └── it return true
\ No newline at end of file
diff --git a/l1-contracts/test/governance/gerousia/vote.t.sol b/l1-contracts/test/governance/gerousia/vote.t.sol
new file mode 100644
index 00000000000..2999d3459d4
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/vote.t.sol
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity >=0.8.27;
+
+import {IGerousia} from "@aztec/governance/interfaces/IGerousia.sol";
+import {GerousiaBase} from "./Base.t.sol";
+import {Leonidas} from "@aztec/core/Leonidas.sol";
+import {Errors} from "@aztec/governance/libraries/Errors.sol";
+import {Slot, SlotLib, Timestamp} from "@aztec/core/libraries/TimeMath.sol";
+
+contract VoteTest is GerousiaBase {
+  using SlotLib for Slot;
+
+  address internal proposal = address(0xdeadbeef);
+  address internal proposer = address(0);
+  Leonidas internal leonidas;
+
+  function test_WhenProposalHoldNoCode() external {
+    // it revert
+    vm.expectRevert(abi.encodeWithSelector(Errors.Gerousia__ProposalHaveNoCode.selector, proposal));
+    gerousia.vote(proposal);
+  }
+
+  modifier whenProposalHoldCode() {
+    proposal = address(this);
+    _;
+  }
+
+  function test_GivenCanonicalRollupHoldNoCode() external whenProposalHoldCode {
+    // it revert
+    vm.expectRevert(
+      abi.encodeWithSelector(Errors.Gerousia__InstanceHaveNoCode.selector, address(0xdead))
+    );
+    gerousia.vote(proposal);
+  }
+
+  modifier givenCanonicalRollupHoldCode() {
+    leonidas = new Leonidas(address(this));
+    registry.upgrade(address(leonidas));
+
+    // We jump into the future since slot 0, will behave as if already voted in
+    vm.warp(Timestamp.unwrap(leonidas.getTimestampForSlot(Slot.wrap(1))));
+    _;
+  }
+
+  function test_GivenAVoteAlreadyCastInTheSlot()
+    external
+    whenProposalHoldCode
+    givenCanonicalRollupHoldCode
+  {
+    // it revert
+
+    Slot currentSlot = leonidas.getCurrentSlot();
+    assertEq(currentSlot.unwrap(), 1);
+    vm.prank(proposer);
+    gerousia.vote(proposal);
+
+    vm.expectRevert(
+      abi.encodeWithSelector(Errors.Gerousia__VoteAlreadyCastForSlot.selector, currentSlot)
+    );
+    gerousia.vote(proposal);
+  }
+
+  modifier givenNoVoteAlreadyCastInTheSlot() {
+    _;
+  }
+
+  function test_WhenCallerIsNotProposer(address _proposer)
+    external
+    whenProposalHoldCode
+    givenCanonicalRollupHoldCode
+    givenNoVoteAlreadyCastInTheSlot
+  {
+    // it revert
+    vm.assume(_proposer != proposer);
+    vm.prank(_proposer);
+    vm.expectRevert(
+      abi.encodeWithSelector(Errors.Gerousia__OnlyProposerCanVote.selector, _proposer, proposer)
+    );
+    gerousia.vote(proposal);
+  }
+
+  modifier whenCallerIsProposer() {
+    // Lets make sure that there first is a leader
+    uint256 votesOnProposal = 5;
+
+    for (uint256 i = 0; i < votesOnProposal; i++) {
+      vm.warp(
+        Timestamp.unwrap(leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(1)))
+      );
+      vm.prank(proposer);
+      gerousia.vote(proposal);
+    }
+
+    Slot currentSlot = leonidas.getCurrentSlot();
+    uint256 round = gerousia.computeRound(currentSlot);
+    (Slot lastVote, address leader, bool executed) = gerousia.rounds(address(leonidas), round);
+    assertEq(
+      gerousia.yeaCount(address(leonidas), round, leader),
+      votesOnProposal,
+      "invalid number of votes"
+    );
+    assertFalse(executed);
+    assertEq(leader, proposal);
+    assertEq(currentSlot.unwrap(), lastVote.unwrap());
+
+    vm.warp(
+      Timestamp.unwrap(leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(1)))
+    );
+
+    _;
+  }
+
+  function test_GivenNewCanonicalInstance()
+    external
+    whenProposalHoldCode
+    givenCanonicalRollupHoldCode
+    givenNoVoteAlreadyCastInTheSlot
+    whenCallerIsProposer
+  {
+    // it ignore votes from prior instance
+    // it increase the yea count
+    // it updates the leader to the proposal
+    // it emits {VoteCast} event
+    // it returns true
+
+    Slot leonidasSlot = leonidas.getCurrentSlot();
+    uint256 leonidasRound = gerousia.computeRound(leonidasSlot);
+    uint256 yeaBefore = gerousia.yeaCount(address(leonidas), leonidasRound, proposal);
+
+    Leonidas freshInstance = new Leonidas(address(this));
+    registry.upgrade(address(freshInstance));
+
+    vm.warp(Timestamp.unwrap(freshInstance.getTimestampForSlot(Slot.wrap(1))));
+
+    Slot freshSlot = freshInstance.getCurrentSlot();
+    uint256 freshRound = gerousia.computeRound(freshSlot);
+
+    vm.prank(proposer);
+    vm.expectEmit(true, true, true, true, address(gerousia));
+    emit IGerousia.VoteCast(proposal, freshRound, proposer);
+    assertTrue(gerousia.vote(proposal));
+
+    // Check the new instance
+    {
+      (Slot lastVote, address leader, bool executed) =
+        gerousia.rounds(address(freshInstance), freshRound);
+      assertEq(
+        gerousia.yeaCount(address(freshInstance), freshRound, leader), 1, "invalid number of votes"
+      );
+      assertFalse(executed);
+      assertEq(leader, proposal);
+      assertEq(freshSlot.unwrap(), lastVote.unwrap(), "invalid slot [FRESH]");
+    }
+
+    // The old instance
+    {
+      (Slot lastVote, address leader, bool executed) =
+        gerousia.rounds(address(leonidas), leonidasRound);
+      assertEq(
+        gerousia.yeaCount(address(leonidas), leonidasRound, proposal),
+        yeaBefore,
+        "invalid number of votes"
+      );
+      assertFalse(executed);
+      assertEq(leader, proposal);
+      assertEq(leonidasSlot.unwrap(), lastVote.unwrap() + 1, "invalid slot [LEONIDAS]");
+    }
+  }
+
+  function test_GivenRoundChanged()
+    external
+    whenProposalHoldCode
+    givenCanonicalRollupHoldCode
+    givenNoVoteAlreadyCastInTheSlot
+    whenCallerIsProposer
+  {
+    // it ignore votes in prior round
+    // it increase the yea count
+    // it updates the leader to the proposal
+    // it emits {VoteCast} event
+    // it returns true
+  }
+
+  modifier givenRoundAndInstanceIsStable() {
+    _;
+  }
+
+  function test_GivenProposalIsLeader()
+    external
+    whenProposalHoldCode
+    givenCanonicalRollupHoldCode
+    givenNoVoteAlreadyCastInTheSlot
+    whenCallerIsProposer
+    givenRoundAndInstanceIsStable
+  {
+    // it increase the yea count
+    // it emits {VoteCast} event
+    // it returns true
+
+    Slot currentSlot = leonidas.getCurrentSlot();
+    uint256 round = gerousia.computeRound(currentSlot);
+
+    uint256 yeaBefore = gerousia.yeaCount(address(leonidas), round, proposal);
+
+    vm.prank(proposer);
+    vm.expectEmit(true, true, true, true, address(gerousia));
+    emit IGerousia.VoteCast(proposal, round, proposer);
+    assertTrue(gerousia.vote(proposal));
+
+    (Slot lastVote, address leader, bool executed) = gerousia.rounds(address(leonidas), round);
+    assertEq(
+      gerousia.yeaCount(address(leonidas), round, leader), yeaBefore + 1, "invalid number of votes"
+    );
+    assertFalse(executed);
+    assertEq(leader, proposal);
+    assertEq(currentSlot.unwrap(), lastVote.unwrap());
+  }
+
+  function test_GivenProposalHaveFeverVotesThanLeader()
+    external
+    whenProposalHoldCode
+    givenCanonicalRollupHoldCode
+    givenNoVoteAlreadyCastInTheSlot
+    whenCallerIsProposer
+    givenRoundAndInstanceIsStable
+  {
+    // it increase the yea count
+    // it emits {VoteCast} event
+    // it returns true
+
+    Slot currentSlot = leonidas.getCurrentSlot();
+    uint256 round = gerousia.computeRound(currentSlot);
+
+    uint256 leaderYeaBefore = gerousia.yeaCount(address(leonidas), round, proposal);
+
+    vm.prank(proposer);
+    vm.expectEmit(true, true, true, true, address(gerousia));
+    emit IGerousia.VoteCast(address(leonidas), round, proposer);
+    assertTrue(gerousia.vote(address(leonidas)));
+
+    (Slot lastVote, address leader, bool executed) = gerousia.rounds(address(leonidas), round);
+    assertEq(
+      gerousia.yeaCount(address(leonidas), round, leader),
+      leaderYeaBefore,
+      "invalid number of votes"
+    );
+    assertEq(
+      gerousia.yeaCount(address(leonidas), round, address(leonidas)), 1, "invalid number of votes"
+    );
+    assertFalse(executed);
+    assertEq(leader, proposal);
+    assertEq(currentSlot.unwrap(), lastVote.unwrap());
+  }
+
+  function test_GivenProposalHaveMoreVotesThanLeader()
+    external
+    whenProposalHoldCode
+    givenCanonicalRollupHoldCode
+    givenNoVoteAlreadyCastInTheSlot
+    whenCallerIsProposer
+    givenRoundAndInstanceIsStable
+  {
+    // it increase the yea count
+    // it updates the leader to the proposal
+    // it emits {VoteCast} event
+    // it returns true
+
+    Slot currentSlot = leonidas.getCurrentSlot();
+    uint256 round = gerousia.computeRound(currentSlot);
+
+    uint256 leaderYeaBefore = gerousia.yeaCount(address(leonidas), round, proposal);
+
+    for (uint256 i = 0; i < leaderYeaBefore + 1; i++) {
+      vm.prank(proposer);
+      vm.expectEmit(true, true, true, true, address(gerousia));
+      emit IGerousia.VoteCast(address(leonidas), round, proposer);
+      assertTrue(gerousia.vote(address(leonidas)));
+
+      vm.warp(
+        Timestamp.unwrap(leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(1)))
+      );
+    }
+
+    {
+      (Slot lastVote, address leader, bool executed) = gerousia.rounds(address(leonidas), round);
+      assertEq(
+        gerousia.yeaCount(address(leonidas), round, address(leonidas)),
+        leaderYeaBefore + 1,
+        "invalid number of votes"
+      );
+      assertFalse(executed);
+      assertEq(leader, address(leonidas));
+      assertEq(
+        gerousia.yeaCount(address(leonidas), round, proposal),
+        leaderYeaBefore,
+        "invalid number of votes"
+      );
+      assertEq(lastVote.unwrap(), currentSlot.unwrap() + leaderYeaBefore);
+    }
+  }
+}
diff --git a/l1-contracts/test/governance/gerousia/vote.tree b/l1-contracts/test/governance/gerousia/vote.tree
new file mode 100644
index 00000000000..44dad7fb3fc
--- /dev/null
+++ b/l1-contracts/test/governance/gerousia/vote.tree
@@ -0,0 +1,39 @@
+VoteTest
+├── when proposal hold no code
+│   └── it revert
+└── when proposal hold code
+    ├── given canonical rollup hold no code
+    │   └── it revert
+    └── given canonical rollup hold code
+        ├── given a vote already cast in the slot
+        │   └── it revert
+        └── given no vote already cast in the slot
+            ├── when caller is not proposer
+            │   └── it revert
+            └── when caller is proposer
+                ├── given new canonical instance
+                │   ├── it ignore votes from prior instance
+                │   ├── it increase the yea count
+                │   ├── it updates the leader to the proposal
+                │   ├── it emits {VoteCast} event
+                │   └── it returns true
+                ├── given round changed
+                │   ├── it ignore votes in prior round
+                │   ├── it increase the yea count
+                │   ├── it updates the leader to the proposal
+                │   ├── it emits {VoteCast} event
+                │   └── it returns true
+                └── given round and instance is stable
+                    ├── given proposal is leader
+                    │   ├── it increase the yea count
+                    │   ├── it emits {VoteCast} event
+                    │   └── it returns true
+                    ├── given proposal have fever votes than leader
+                    │   ├── it increase the yea count
+                    │   ├── it emits {VoteCast} event
+                    │   └── it returns true
+                    └── given proposal have more votes than leader
+                        ├── it increase the yea count
+                        ├── it updates the leader to the proposal
+                        ├── it emits {VoteCast} event
+                        └── it returns true
\ No newline at end of file