Skip to content

Commit

Permalink
feat: gerousia (#8942)
Browse files Browse the repository at this point in the history
Fixes #8138.
  • Loading branch information
LHerskind authored Oct 22, 2024
1 parent 5fa660d commit 54b5ba2
Show file tree
Hide file tree
Showing 13 changed files with 918 additions and 1 deletion.
154 changes: 154 additions & 0 deletions l1-contracts/src/governance/Gerousia.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 6 additions & 0 deletions l1-contracts/src/governance/interfaces/IApella.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.27;

interface IApella {
function propose(address _proposal) external returns (bool);
}
17 changes: 17 additions & 0 deletions l1-contracts/src/governance/interfaces/IGerousia.sol
Original file line number Diff line number Diff line change
@@ -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);
}
16 changes: 15 additions & 1 deletion l1-contracts/src/governance/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
39 changes: 39 additions & 0 deletions l1-contracts/test/governance/gerousia/Base.t.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
45 changes: 45 additions & 0 deletions l1-contracts/test/governance/gerousia/constructor.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 7 additions & 0 deletions l1-contracts/test/governance/gerousia/constructor.tree
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions l1-contracts/test/governance/gerousia/mocks/FalsyApella.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 13 additions & 0 deletions l1-contracts/test/governance/gerousia/mocks/FaultyApella.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 54b5ba2

Please sign in to comment.