Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
alexkeating committed Sep 29, 2023
1 parent 106fa5e commit 66a1216
Show file tree
Hide file tree
Showing 5 changed files with 1,046 additions and 230 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ out/
# Coverage
lcov.info
notes.txt

remappings.txt
160 changes: 140 additions & 20 deletions src/L2VoteAggregator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ pragma solidity ^0.8.20;

import {ERC20Votes} from "openzeppelin/token/ERC20/extensions/ERC20Votes.sol";
import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol";
import {EIP712} from "openzeppelin/utils/cryptography/EIP712.sol";
import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol";

import {L2GovernorMetadata} from "src/WormholeL2GovernorMetadata.sol";
import {IL1Block} from "src/interfaces/IL1Block.sol";

/// @notice A contract to collect votes on L2 to be bridged to L1.
abstract contract L2VoteAggregator {
abstract contract L2VoteAggregator is EIP712 {
/// @notice The number of blocks before L2 voting closes. We close voting 1200 blocks before the
/// end of the proposal to cast the vote.
uint32 public constant CAST_VOTE_WINDOW = 1200;

bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)");

/// @notice The token used to vote on proposals provided by the `GovernorMetadata`.
ERC20Votes public immutable VOTING_TOKEN;

Expand Down Expand Up @@ -43,13 +47,24 @@ abstract contract L2VoteAggregator {
/// @dev Contract is already initialized with an L2 token.
error AlreadyInitialized();

/// @dev We do not support the method, but provide it to be compatible with 3rd party tooling.
error UnsupportedMethod();

/// @dev The voting options corresponding to those used in the Governor.
enum VoteType {
Against,
For,
Abstain
}

/// @dev The states of a proposal on L2.
enum ProposalState {
Pending,
Active,
Cancelled,
Expired
}

/// @dev Data structure to store vote preferences expressed by depositors.
// TODO: Does it matter if we use a uint128 vs a uint256?
struct ProposalVote {
Expand All @@ -68,7 +83,11 @@ abstract contract L2VoteAggregator {

/// @dev Emitted when a vote is cast on L2.
event VoteCast(
address indexed voter, uint256 indexed proposalId, VoteType support, uint256 weight
address indexed voter,
uint256 indexed proposalId,
VoteType support,
uint256 weight,
string reason
);

event VoteBridged(
Expand All @@ -78,7 +97,9 @@ abstract contract L2VoteAggregator {
/// @param _votingToken The token used to vote on proposals.
/// @param _governorMetadata The `GovernorMetadata` contract that provides proposal information.
/// @param _l1BlockAddress The address of the L1Block contract.
constructor(address _votingToken, address _governorMetadata, address _l1BlockAddress) {
constructor(address _votingToken, address _governorMetadata, address _l1BlockAddress)
EIP712("L2VoteAggregator", "1")
{
VOTING_TOKEN = ERC20Votes(_votingToken);
GOVERNOR_METADATA = L2GovernorMetadata(_governorMetadata);
L1_BLOCK = IL1Block(_l1BlockAddress);
Expand All @@ -90,29 +111,103 @@ abstract contract L2VoteAggregator {
L1_BRIDGE_ADDRESS = l1BridgeAddress;
}

/// @notice This function does not make sense in the L2 context, but we have added it to have
/// compatibility with existing Governor tooling.
function votingDelay() public view virtual returns (uint256) {
return 0;
}

/// @notice This function does not make sense in the L2 context, but we have added it to have
/// compatibility with existing Governor tooling.
function votingPeriod() public view virtual returns (uint256) {
return 0;
}

/// @notice This function does not make sense in the L2 context, but we have added it to have
/// compatibility with existing Governor tooling.
function quorum(uint256) public view virtual returns (uint256) {
return 0;
}

/// @notice This function does not make sense in the L2 context, but we have added it to have
/// compatibility with existing Governor tooling.
function proposalThreshold() public view virtual returns (uint256) {
return 0;
}

// @notice Shows the state of of a proposal on L2. We only support a subset of the Governor
// proposal states. If the vote has not started the state is pending, if voting has started it is
// active, if it has been cancelled then the state is cancelled, and if the voting has finished
// without it being cancelled we will mark it as expired. We use expired because users can no
// longer vote and no other L2 action can be taken on the proposal.
function state(uint256 proposalId) public view virtual returns (ProposalState) {
L2GovernorMetadata.Proposal memory proposal = GOVERNOR_METADATA.getProposal(proposalId);
if (VOTING_TOKEN.clock() < proposal.voteStart) return ProposalState.Pending;
else if (proposalVoteActive(proposalId)) return ProposalState.Active;
else if (proposal.isCancelled) return ProposalState.Cancelled;
else return ProposalState.Expired;
}

/// @notice This function does not make sense in the L2 context, but we have added it to have
/// compatibility with existing Governor tooling.
function getVotes(address, uint256) public view virtual returns (uint256) {
return 0;
}

/// @notice This function does not make sense in the L2 context, but we have added it to have
/// compatibility with existing Governor tooling.
function propose(address[] memory, uint256[] memory, bytes[] memory, string memory)
public
virtual
returns (uint256)
{
revert UnsupportedMethod();
}

/// @notice This function does not make sense in the L2 context, but we have added it to have
/// compatibility with existing Governor tooling.
function execute(address[] memory, uint256[] memory, bytes[] memory, bytes32)
public
payable
virtual
returns (uint256)
{
revert UnsupportedMethod();
}

/// @notice Where a user can express their vote based on their L2 token voting power.
/// @param proposalId The id of the proposal to vote on.
/// @param support The type of vote to cast.
function castVote(uint256 proposalId, VoteType support) public returns (uint256) {
if (!proposalVoteActive(proposalId)) revert ProposalInactive();
if (_proposalVotersHasVoted[proposalId][msg.sender]) revert AlreadyVoted();
_proposalVotersHasVoted[proposalId][msg.sender] = true;
return _castVote(proposalId, msg.sender, support, "");
}

L2GovernorMetadata.Proposal memory proposal = GOVERNOR_METADATA.getProposal(proposalId);
uint256 weight = VOTING_TOKEN.getPastVotes(msg.sender, proposal.voteStart);
if (weight == 0) revert NoWeight();
/// @notice Where a user can express their vote based on their L2 token voting power, and provide
/// a reason.
/// @param proposalId The id of the proposal to vote on.
/// @param support The type of vote to cast.
/// @param reason The reason the vote was cast.
function castVoteWithReason(uint256 proposalId, VoteType support, string calldata reason)
public
virtual
returns (uint256)
{
return _castVote(proposalId, msg.sender, support, reason);
}

if (support == VoteType.Against) {
proposalVotes[proposalId].againstVotes += SafeCast.toUint128(weight);
} else if (support == VoteType.For) {
proposalVotes[proposalId].forVotes += SafeCast.toUint128(weight);
} else if (support == VoteType.Abstain) {
proposalVotes[proposalId].abstainVotes += SafeCast.toUint128(weight);
} else {
revert InvalidVoteType();
}
emit VoteCast(msg.sender, proposalId, support, weight);
return weight;
/// @notice Where a user can express their vote based on their L2 token voting power using a
/// signature.
/// @param proposalId The id of the proposal to vote on.
/// @param support The type of vote to cast.
function castVoteBySig(uint256 proposalId, VoteType support, uint8 v, bytes32 r, bytes32 s)
public
virtual
returns (uint256)
{
address voter = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support))), v, r, s
);
return _castVote(proposalId, voter, support, "");
}

/// @notice Bridges a vote to the L1.
Expand Down Expand Up @@ -144,6 +239,31 @@ abstract contract L2VoteAggregator {
_lastVotingBlock = proposal.voteEnd - CAST_VOTE_WINDOW;
}

function _castVote(uint256 proposalId, address voter, VoteType support, string memory reason)
internal
returns (uint256)
{
if (!proposalVoteActive(proposalId)) revert ProposalInactive();
if (_proposalVotersHasVoted[proposalId][voter]) revert AlreadyVoted();
_proposalVotersHasVoted[proposalId][voter] = true;

L2GovernorMetadata.Proposal memory proposal = GOVERNOR_METADATA.getProposal(proposalId);
uint256 weight = VOTING_TOKEN.getPastVotes(voter, proposal.voteStart);
if (weight == 0) revert NoWeight();

if (support == VoteType.Against) {
proposalVotes[proposalId].againstVotes += SafeCast.toUint128(weight);
} else if (support == VoteType.For) {
proposalVotes[proposalId].forVotes += SafeCast.toUint128(weight);
} else if (support == VoteType.Abstain) {
proposalVotes[proposalId].abstainVotes += SafeCast.toUint128(weight);
} else {
revert InvalidVoteType();
}
emit VoteCast(voter, proposalId, support, weight, reason);
return weight;
}

function proposalVoteActive(uint256 proposalId) public view returns (bool active) {
L2GovernorMetadata.Proposal memory proposal = GOVERNOR_METADATA.getProposal(proposalId);

Expand Down
Loading

0 comments on commit 66a1216

Please sign in to comment.