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

CompoundGovernor Upgrade #20

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b7fbd77
Add ScopeLift foundry default settings to repo (#14)
jferas Oct 1, 2024
9302835
First pass on CompoundGovernor (#16)
jferas Oct 3, 2024
5309e98
Added test framework with minimal deploy script (#17)
jferas Oct 8, 2024
7b12187
Add GovernorSettableFixedQuorumUpgradeable (#20)
jferas Oct 11, 2024
62799b8
Add flexible voting using latest OZ contracts (#21)
jferas Oct 11, 2024
002c872
Add `GovernorVotesCompUpgradeable` (#27)
garyghayrat Oct 14, 2024
c6e78c8
Add SetQuorum test via proposal test helper and refactor test files (…
jferas Oct 21, 2024
ecb1b91
Add whitelist and guardian roles (#29)
garyghayrat Oct 22, 2024
af255ba
Use `TIMELOCK_ADDRESS` as proxy owner in deploy script (#34)
garyghayrat Oct 23, 2024
706c183
Add cancel logic and tests (#36)
garyghayrat Oct 23, 2024
d329ce8
Vendor OZ files for enumerable/sequential proposal IDs (#43)
jferas Oct 31, 2024
fc00084
Add flexible voting tests (#42)
garyghayrat Nov 1, 2024
88e85e3
GovernorBravo to CompoundGovernor upgrade script and tests (#38)
jferas Nov 1, 2024
70b560f
remove virtual (#48)
wildmolasses Nov 1, 2024
0d92e8e
Consolidate test files (#50)
wildmolasses Nov 1, 2024
f66ca1d
OZ upgrade to 5.1 (#51)
jferas Nov 1, 2024
5b55d5d
Remove unused `initialize` function (#49)
garyghayrat Nov 1, 2024
1910691
Add upgrade proposal description (#55)
garyghayrat Nov 4, 2024
48577cf
Add GovernorSequential.. extension natspec (#56)
garyghayrat Nov 4, 2024
745c302
Update vote extension to be 2 days (#57)
garyghayrat Nov 4, 2024
8c5d88b
Test propose, queue, execute, cancel functions (#52)
garyghayrat Nov 4, 2024
6c43a8c
Removed empty test file (#59)
jferas Nov 4, 2024
c7c0218
add sequentialProposalId extension tests (#60)
garyghayrat Nov 4, 2024
4d9f2c9
Add security contact (#58)
garyghayrat Nov 4, 2024
f248449
Adding proposal related tests (#54)
jferas Nov 4, 2024
f78cc0f
remove duplicate test helper
garyghayrat Nov 4, 2024
ab1afdf
Fix typos of 'support' (#79)
jferas Dec 15, 2024
78899c8
Fix inconsistent OZ version comment in IGovernor (N-01) (#80)
jferas Dec 15, 2024
e6c891c
Removed unnecessary cast (#81)
jferas Dec 15, 2024
2657f27
Added initializer call (#83)
jferas Dec 15, 2024
01a104b
Fix incorrect erc7201 storage location annotation in GovernorVotesCom…
jferas Dec 15, 2024
98babdf
Fixed namespace ID in GovernorSettableFixedQuorumUpgradeable (#85)
jferas Dec 15, 2024
3dfdd7e
Changed _quorumReached function to not include abstentions (M-02) (#86)
jferas Dec 16, 2024
20f51ed
Fix missing ERC-7201 natspec tag (L-05) (#82)
jferas Dec 16, 2024
cf39115
Fix to set next proposal id correctly (M-01) (#87)
jferas Dec 16, 2024
f47c158
Fix H-01: Allow whitelisted accounts below threshold to propose (#89)
garyghayrat Dec 16, 2024
45fac85
Fix H-02: Follow regular cancel flow if `proposalGuardian` is expired…
garyghayrat Dec 17, 2024
6036b80
proposalCount fix (#91)
wildmolasses Dec 23, 2024
da83101
Single active proposal per proposer (#90)
wildmolasses Dec 23, 2024
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
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
artifacts
cache
coverage
typechain-types
typechain-types
lib
32 changes: 32 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- main

env:
FOUNDRY_PROFILE: ci
RPC_URL: ${{ secrets.RPC_URL }}

jobs:
Expand All @@ -23,6 +24,19 @@ jobs:
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build
- name: Run Forge tests
run: |
forge test -vvv
id: test
- name: Run Tests
run: npm test
coverage:
Expand All @@ -36,6 +50,15 @@ jobs:
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build
- name: Run Coverage
run: npm run coverage
- name: Coveralls GitHub Action
Expand All @@ -51,6 +74,15 @@ jobs:
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build
- name: build source
run: npm run build
- name: Run Lint
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ artifacts
yarn-error.log
.DS_Store
yarn.lock
cache_forge
out
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable
12 changes: 5 additions & 7 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ node_modules
artifacts
cache
coverage*
contracts/test*
contracts/SafeMath.sol
contracts/Timelock.sol
contracts/Comp.sol
contracts/GovernorBravoDelegator.sol
contracts/GovernorBravoInterfaces.sol
typechain-types
contracts*
typechain-types
out
lib
script
468 changes: 468 additions & 0 deletions contracts/CompoundGovernor.sol

Large diffs are not rendered by default.

220 changes: 220 additions & 0 deletions contracts/extensions/GovernorCountingFractionalUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
// Sourced from contract below, with import path changes for local GovernorUpgradeable.sol.
// OpenZeppelin Contracts (last updated v5.1.0) (governance/extensions/GovernorCountingFractional.sol)

pragma solidity ^0.8.20;

import {GovernorUpgradeable} from "contracts/extensions/GovernorUpgradeable.sol";
import {GovernorCountingSimpleUpgradeable} from "contracts/extensions/GovernorCountingSimpleUpgradeable.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

/**
* @dev Extension of {Governor} for fractional voting.
*
* Similar to {GovernorCountingSimple}, this contract is a votes counting module for {Governor} that supports 3 options:
* Against, For, Abstain. Additionally, it includes a fourth option: Fractional, which allows voters to split their voting
* power amongst the other 3 options.
*
* Votes cast with the Fractional support must be accompanied by a `params` argument that is three packed `uint128` values
* representing the weight the delegate assigns to Against, For, and Abstain respectively. For those votes cast for the other
* 3 options, the `params` argument must be empty.
*
* This is mostly useful when the delegate is a contract that implements its own rules for voting. These delegate-contracts
* can cast fractional votes according to the preferences of multiple entities delegating their voting power.
*
* Some example use cases include:
*
* * Voting from tokens that are held by a DeFi pool
* * Voting from an L2 with tokens held by a bridge
* * Voting privately from a shielded pool using zero knowledge proofs.
*
* Based on ScopeLift's GovernorCountingFractional[https://github.com/ScopeLift/flexible-voting/blob/e5de2efd1368387b840931f19f3c184c85842761/src/GovernorCountingFractional.sol]
*
* _Available since v5.1._
*/
abstract contract GovernorCountingFractionalUpgradeable is Initializable, GovernorUpgradeable {
using Math for *;

uint8 internal constant VOTE_TYPE_FRACTIONAL = 255;

struct ProposalVote {
uint256 againstVotes;
uint256 forVotes;
uint256 abstainVotes;
mapping(address voter => uint256) usedVotes;
}

/// @custom:storage-location erc7201:openzeppelin.storage.GovernorCountingFractional
struct GovernorCountingFractionalStorage {
/**
* @dev Mapping from proposal ID to vote tallies for that proposal.
*/
mapping(uint256 proposalId => ProposalVote) _proposalVotes;
}

// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.GovernorCountingFractional")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant GovernorCountingFractionalStorageLocation = 0xd073797d8f9d07d835a3fc13195afeafd2f137da609f97a44f7a3aa434170800;

function _getGovernorCountingFractionalStorage() private pure returns (GovernorCountingFractionalStorage storage $) {
assembly {
$.slot := GovernorCountingFractionalStorageLocation
}
}

/**
* @dev A fractional vote params uses more votes than are available for that user.
*/
error GovernorExceedRemainingWeight(address voter, uint256 usedVotes, uint256 remainingWeight);

function __GovernorCountingFractional_init() internal onlyInitializing {
}

function __GovernorCountingFractional_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IGovernor-COUNTING_MODE}.
*/
// solhint-disable-next-line func-name-mixedcase
function COUNTING_MODE() public pure virtual override returns (string memory) {
return "support=bravo,fractional&quorum=for,abstain&params=fractional";
}

/**
* @dev See {IGovernor-hasVoted}.
*/
function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) {
return usedVotes(proposalId, account) > 0;
}

/**
* @dev Get the number of votes already cast by `account` for a proposal with `proposalId`. Useful for
* integrations that allow delegates to cast rolling, partial votes.
*/
function usedVotes(uint256 proposalId, address account) public view virtual returns (uint256) {
GovernorCountingFractionalStorage storage $ = _getGovernorCountingFractionalStorage();
return $._proposalVotes[proposalId].usedVotes[account];
}

/**
* @dev Get current distribution of votes for a given proposal.
*/
function proposalVotes(
uint256 proposalId
) public view virtual returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) {
GovernorCountingFractionalStorage storage $ = _getGovernorCountingFractionalStorage();
ProposalVote storage proposalVote = $._proposalVotes[proposalId];
return (proposalVote.againstVotes, proposalVote.forVotes, proposalVote.abstainVotes);
}

/**
* @dev See {Governor-_quorumReached}.
*/
function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
GovernorCountingFractionalStorage storage $ = _getGovernorCountingFractionalStorage();
ProposalVote storage proposalVote = $._proposalVotes[proposalId];
return quorum(proposalSnapshot(proposalId)) <= proposalVote.forVotes + proposalVote.abstainVotes;
}

/**
* @dev See {Governor-_voteSucceeded}. In this module, forVotes must be > againstVotes.
*/
function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
GovernorCountingFractionalStorage storage $ = _getGovernorCountingFractionalStorage();
ProposalVote storage proposalVote = $._proposalVotes[proposalId];
return proposalVote.forVotes > proposalVote.againstVotes;
}

/**
* @dev See {Governor-_countVote}. Function that records the delegate's votes.
*
* Executing this function consumes (part of) the delegate's weight on the proposal. This weight can be
* distributed amongst the 3 options (Against, For, Abstain) by specifying a fractional `support`.
*
* This counting module supports two vote casting modes: nominal and fractional.
*
* - Nominal: A nominal vote is cast by setting `support` to one of the 3 bravo options (Against, For, Abstain).
* - Fractional: A fractional vote is cast by setting `support` to `type(uint8).max` (255).
*
* Casting a nominal vote requires `params` to be empty and consumes the delegate's full remaining weight on the
* proposal for the specified `support` option. This is similar to the {GovernorCountingSimple} module and follows
* the `VoteType` enum from Governor Bravo. As a consequence, no vote weight remains unspent so no further voting
* is possible (for this `proposalId` and this `account`).
*
* Casting a fractional vote consumes a fraction of the delegate's remaining weight on the proposal according to the
* weights the delegate assigns to each support option (Against, For, Abstain respectively). The sum total of the
* three decoded vote weights _must_ be less than or equal to the delegate's remaining weight on the proposal (i.e.
* their checkpointed total weight minus votes already cast on the proposal). This format can be produced using:
*
* `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))`
*
* NOTE: Consider that fractional voting restricts the number of casted vote (in each category) to 128 bits.
* Depending on how many decimals the underlying token has, a single voter may require to split their vote into
* multiple vote operations. For precision higher than ~30 decimals, large token holders may require an
* potentially large number of calls to cast all their votes. The voter has the possibility to cast all the
* remaining votes in a single operation using the traditional "bravo" vote.
*/
// slither-disable-next-line cyclomatic-complexity
function _countVote(
uint256 proposalId,
address account,
uint8 support,
uint256 totalWeight,
bytes memory params
) internal virtual override returns (uint256) {
GovernorCountingFractionalStorage storage $ = _getGovernorCountingFractionalStorage();
// Compute number of remaining votes. Returns 0 on overflow.
(, uint256 remainingWeight) = totalWeight.trySub(usedVotes(proposalId, account));
if (remainingWeight == 0) {
revert GovernorAlreadyCastVote(account);
}

uint256 againstVotes = 0;
uint256 forVotes = 0;
uint256 abstainVotes = 0;
uint256 usedWeight = 0;

// For clarity of event indexing, fractional voting must be clearly advertised in the "support" field.
//
// Supported `support` value must be:
// - "Full" voting: `support = 0` (Against), `1` (For) or `2` (Abstain), with empty params.
// - "Fractional" voting: `support = 255`, with 48 bytes params.
if (support == uint8(GovernorCountingSimpleUpgradeable.VoteType.Against)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = againstVotes = remainingWeight;
} else if (support == uint8(GovernorCountingSimpleUpgradeable.VoteType.For)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = forVotes = remainingWeight;
} else if (support == uint8(GovernorCountingSimpleUpgradeable.VoteType.Abstain)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = abstainVotes = remainingWeight;
} else if (support == VOTE_TYPE_FRACTIONAL) {
// The `params` argument is expected to be three packed `uint128`:
// `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))`
if (params.length != 0x30) revert GovernorInvalidVoteParams();

assembly ("memory-safe") {
againstVotes := shr(128, mload(add(params, 0x20)))
forVotes := shr(128, mload(add(params, 0x30)))
abstainVotes := shr(128, mload(add(params, 0x40)))
usedWeight := add(add(againstVotes, forVotes), abstainVotes) // inputs are uint128: cannot overflow
}

// check parsed arguments are valid
if (usedWeight > remainingWeight) {
revert GovernorExceedRemainingWeight(account, usedWeight, remainingWeight);
}
} else {
revert GovernorInvalidVoteType();
}

// update votes tracking
ProposalVote storage details = $._proposalVotes[proposalId];
if (againstVotes > 0) details.againstVotes += againstVotes;
if (forVotes > 0) details.forVotes += forVotes;
if (abstainVotes > 0) details.abstainVotes += abstainVotes;
details.usedVotes[account] += usedWeight;

return usedWeight;
}
}
Loading