Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: gerousia #8942

Merged
merged 1 commit into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must we constrain proposers to vote during their turn?

Copy link
Contributor Author

@LHerskind LHerskind Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Empire model is using the sampling to figure out who should cast votes to make a proposal.

So practically, you don't NEED, to do it as part of the proposer slot, but it have its benefits

  • You don't have to handle some extra accounting for what slots have been "voted on" after the fact (this is not super expensive, can be done quite efficiently with some bitpacking).
  • More importantly, it means that the full set of "voters" are not yet know early, so it should be harder for you to coordinate a malicious pushthrough.
  • By only needing the data for "now" we avoid some of the cases that could happen with new instances being deployed.

For example, say that as long as it is the proposers, they are allowed to vote later. You could wait until you see a round where you have sufficient "ballots" and then cast them all at once to make your proposal. In the current model, you would need to "publish" what your intent is early, and then hope that you get sufficient.

I don't particular care. In any case they are unable to force something through because that is handled fully separate to this sampling in the Apella.

The implementation was essentially made like this because that is what empire is proposing, and that was what you guys wanted 🤷

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay this means that for a proposal to be nominated, N / M validators must vote to propose it. This quorum is tied to the round size and no longer to the size of the validator set. i.e. If we have 10,000 validators, and M=1,000 then only 501 = 5% of proposers are needed to nominate any proposal.

I think this is fine, probability of > 501 unique proposers in a 1,000 slots is high.


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
Loading