Skip to content

Commit

Permalink
Decentralised governance of 0x protocol and treasury (#641)
Browse files Browse the repository at this point in the history
* Install open zeppelin contracts

* Init foundry in governance

* Add wrapped ZRX token

* Add governance contracts testing to CI

* Set optimizer runs to default

* Upgrade to patched version of openzeppelin/contracts

* Test stakingakng / unwrapping ZRX

* Init npm package

* Lint fix, removing lib from gitignore

* Add openzeppelin contracts git submodule for foundry

* Add vanilla governor contract

* Fix reference paths to imported packages

* Temporarily switch to using a mocked version of ZRX

* Ignore foundry's lib in link checker

* Fix a conflict in gitignore between forge lib adn built lib

* Upload governance code coverage report to coveralls

* Flesh out test scenarios for wrapping/unwrapping

* Add basic ERC20 name and symbol tests

* Wire in basic timelock controller and governor test setup

* Test basic governor properties

* Add basic voting power delegation tests

* Add proposal execution happy path test

* Split ERC20Votes logic between wrapped token
and ZeroExVotes contracts

* Exclude BaseTest from coverage in coveralls

* Add protocol specific governor with produciton governance settings

* Add a dedicated instance for the treasury governor
This is currently using the default 1 token 1 vote mechanism but will be migrated

* Add test for updating governance settings
for voting delay, voting period and proposal threshold

* Create seperate timelock contract instance for treasury and protocol

* Test updating the timlock min delay

* Set timelock delay to 2 days for protocol and 1 sec for treasury

* Remove timelock from treasury governor

* Refactor _checkpointsLookup to return entire Checkpoint
instad of just number of votes

* Update the totalSupply checkpoints updating logic

* Quadratic voting power transfers and delegations

* Fix workflow yaml

* Initialise ZeroExVotes behind a ERC1967Proxy
Test it cannot be reinitialised

* Remove obsoleted console.logs from test

* Storage pack Checkpoint enum

* Remove keeping track of total balances for voting

* Switch to using the foundry artifact in test

* Fix rebase issue

* Add timelock control over the treasury governor

* Add test for wrapped token transfer

* Emit separate events for changing linear and quadratic voting power

* Add the ability to cancel a proposal

* Limit the governors' cancel function to security council only

* Eject security council after a proposal is cancelled

* Add ability for governance to set the security council

* Merge the governors test suites into one reusable set of tests

* Add an empty test function to base test contract
to remove it from coverage reports. Fudge but no other way to ignore it in report

* Security council can rollback protocol upgrades

* Upgrade to solidity 0.8.19

* Move IZeroExGovernor to src

* Abstract Security council interface into its own

* Emit events when assigning and ejecting the security council

* Use a cast to bytes4 instead of LibBytes

Co-authored-by: duncancmt <1207590+duncancmt@users.noreply.github.com>

* Writing total supply checkpoints and setup of
quorum percentage of quadratic total supply for treasure governor

* Add test for transferring tokens when delegating

* Rename IZeroExSecurityCouncil to ISecurityCouncil

* Add security council restrictions to governors

* Remove obsolete overflow check

* Improve test coverage

* Upgrade open-zeppelin contracts to 4.8.2

* Test delegation by signature

* Test non security council requests
to rollback protocol changes cannot be executed

* Better revert messages

* Test correct interfaces are supported

* Remove obsoleted funciton

* Further test delegation by signature scenario

* Split the delegation functionality tests

* Add test for initialisation of voting contract

* Add test for reading checkpoints

* Update code comments

* Fix compilation warnings

* Run smt checker

* Add checkpoint tests

* Rename parameter in moveEntireVotingPower to match the one in movePartialVotingPower

* Switch moveEntireVotingPower to a more generic moveVotingPower implementation
as in the open-zeppelin contracts

* Install foundry earlier in CI

* Switch movePartialVotingPower to the generic moveVotingPower implementation

* Write totalSupplyCheckpoints via the generic _writeCheckpoint

* Add threshold for quadratic voting power

* Remove autoinserted code by OZ

* Add openzeppelin/contracts-upgradable

* Add initializable base to Voting contract

* Fix terminogy error in natspec

* Fix code comment

* Remove obsoleted overrides and add a missing modifier to moveVotingPower

* Remove amount check

Co-authored-by: duncancmt <1207590+duncancmt@users.noreply.github.com>

* Fix a calculation error and clean tests

* Update thresholds for treasury governor

* Fix testShouldNotBeAbleToDelegateWithSignatureAfterExpiry

* Update from @duncancmt

without "memory-safe" the IR optimizer produces significantly worse code and it disables the stack limit evader

Co-authored-by: duncancmt <1207590+duncancmt@users.noreply.github.com>

* Add onlyProxy to initializer

* Fix quadratic voting weight base

* Rename voting parameter for clarity

* Make addresses immutable (#680)

* Make addresses immutable

* Fix linting issues

---------

Co-authored-by: elenadimitrova <elena@arenabg.com>

* Prevent griefing by a malicious ZeroExVotes upgrade (#681)

* Gas optimization

* Minimal change to prevent malicious ZeroExVotes from griefing

* Add demonstration of griefing upgrade

* Fix rebase issues with tests

* Fix prettier issues

* Add checks to test

---------

Co-authored-by: elenadimitrova <elena@arenabg.com>

* Rename SecurityCouncil contract

* Add timestamp to delegator balance updates

* Make quadraticThreshold `immutable` for gas efficiency

* Remove the logic for ejecting security council

* Switch balance timestamp to be a block number

* Test votes migration for adding a new vote weight mechanism (#674)

* Add Emacs files to .gitignore

* Make some functions unproected to demonstrate a migration

* Add example (broken) migration

* Add migration test for voting logic

* Try to simplify tests

* Fix compilation errors

* Fix underflow test with new logic

* Flesh out migration test for voting

* Replace cube root library

* Fix stack too deep in coverage

---------

Co-authored-by: elenadimitrova <elena@arenabg.com>

* Change test case to testFail

* Update contracts/governance/test/ZeroExVotesMigration.sol

Co-authored-by: duncancmt <1207590+duncancmt@users.noreply.github.com>

---------

Co-authored-by: duncancmt <1207590+duncancmt@users.noreply.github.com>
Co-authored-by: Duncan Townsend <git@duncancmt.com>
  • Loading branch information
3 people committed Mar 22, 2023
1 parent 03fc744 commit bcbfbfa
Show file tree
Hide file tree
Showing 31 changed files with 3,799 additions and 8 deletions.
33 changes: 28 additions & 5 deletions .github/workflows/ci.yml
Expand Up @@ -31,6 +31,11 @@ jobs:
- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Add foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Build solution
run: yarn build

Expand Down Expand Up @@ -78,11 +83,6 @@ jobs:
-p @0x/order-utils \
-m --serial -c test:ci
- name: Add foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run Forge build for erc20
working-directory: contracts/erc20
run: |
Expand Down Expand Up @@ -135,3 +135,26 @@ jobs:
path: ./contracts/zero-ex/lcov.info
min_coverage: 6.98
exclude: '**/tests'

- name: Run Forge build on governance contracts
working-directory: ./contracts/governance
run: |
forge --version
forge build --sizes
- name: Run Forge tests on governance contracts
working-directory: ./contracts/governance
run: |
forge test -vvv --gas-report
- name: Run Forge coverage on governance contracts
working-directory: ./contracts/governance
run: |
forge coverage --report lcov
- name: Upload the coverage report to Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
base-path: ./contracts/governance/
path-to-lcov: ./contracts/governance/lcov.info
18 changes: 17 additions & 1 deletion .gitignore
Expand Up @@ -63,7 +63,16 @@ typings/
.env

# built library using in commonjs module syntax
lib/
contracts/erc20/lib/
contracts/test-utils/lib/
contracts/treasury/lib/
contracts/utils/lib/
contracts/zero-ex/lib/
packages/contract-addresses/lib/
packages/contract-artifacts/lib/
packages/contract-wrappers/lib/
packages/protocol-utils/lib/

# UMD bundles that export the global variable
_bundles

Expand Down Expand Up @@ -97,10 +106,17 @@ out/
# typechain wrappers
contracts/zero-ex/typechain-wrappers/

# foundry packages
contracts/governance/cache
contracts/governance/out

# Doc README copy
packages/*/docs/README.md

.DS_Store
*~
\#*\#
.\#*

# the snapshot that gets built for migrations sure does have a ton of files
packages/migrations/0x_ganache_snapshot*
11 changes: 11 additions & 0 deletions .gitmodules
Expand Up @@ -3,4 +3,15 @@
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/erc20/lib/forge-std"]
path = contracts/erc20/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/governance/lib/forge-std"]
path = contracts/governance/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/governance/lib/openzeppelin-contracts"]
path = contracts/governance/lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "contracts/governance/lib/openzeppelin-contracts-upgradeable"]
path = contracts/governance/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "lib/openzeppelin-contracts-upgradeable"]
branch = v4.8.2
505 changes: 505 additions & 0 deletions contracts/governance/ZRXToken.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions contracts/governance/foundry.toml
@@ -0,0 +1,25 @@
[profile.default]
src = 'src'
out = 'out'
libs = ['lib', "../utils/contracts/src/"]
fs_permissions = [{ access = "read", path = "./" }]
remappings = [
'@openzeppelin/=./lib/openzeppelin-contracts/contracts/',
'@openzeppelin-contracts-upgradeable/=./lib/openzeppelin-contracts-upgradeable/contracts/',
'@0x/contracts-utils/=../utils/',
]
solc = '0.8.19'
optimizer_runs = 20_000
via_ir = true

[profile.smt.model_checker]
engine = 'chc'
timeout = 10_000
targets = [
'assert',
'constantCondition',
'divByZero',
'outOfBounds',
'underflow'
]
contracts = { 'src/ZeroExProtocolGovernor.sol' = [ 'ZeroExProtocolGovernor' ] }
1 change: 1 addition & 0 deletions contracts/governance/lib/forge-std
Submodule forge-std added at eb980e
1 change: 1 addition & 0 deletions contracts/governance/lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at d00ace
21 changes: 21 additions & 0 deletions contracts/governance/package.json
@@ -0,0 +1,21 @@
{
"name": "@0x/governance",
"version": "1.0.0",
"description": "Governance implementation for the 0x protocol and treasury",
"main": "index.js",
"directories": {
"lib": "lib",
"test": "test"
},
"scripts": {
"test": "forge test",
"build": "forge build",
"build:smt": "FOUNDRY_PROFILE=smt forge build"
},
"repository": {
"type": "git",
"url": "https://github.com/0xProject/protocol.git"
},
"license": "Apache-2.0",
"dependencies": {}
}
169 changes: 169 additions & 0 deletions contracts/governance/src/CallWithGas.sol
@@ -0,0 +1,169 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

library CallWithGas {
/**
* @notice `staticcall` another contract forwarding a precomputed amount of
* gas.
* @dev contains protections against EIP-150-induced insufficient gas
* griefing
* @dev reverts iff the target is not a contract or we encounter an
* out-of-gas
* @return success true iff the call succeded and returned no more than
* `maxReturnBytes` of return data
* @return returnData the return data or revert reason of the call
* @param target the contract (reverts if non-contract) on which to make the
* `staticcall`
* @param data the calldata to pass
* @param callGas the gas to pass for the call. If the call requires more than
* the specified amount of gas and the caller didn't provide at
* least `callGas`, triggers an out-of-gas in the caller.
* @param maxReturnBytes Only this many bytes of return data are read back
* from the call. This prevents griefing the caller. If
* more bytes are returned or the revert reason is
* longer, success will be false and returnData will be
* `abi.encodeWithSignature("Error(string)", "CallWithGas: returnData too long")`
*/
function functionStaticCallWithGas(
address target,
bytes memory data,
uint256 callGas,
uint256 maxReturnBytes
) internal view returns (bool success, bytes memory returnData) {
assembly ("memory-safe") {
returnData := mload(0x40)
success := staticcall(callGas, target, add(data, 0x20), mload(data), add(returnData, 0x20), maxReturnBytes)

// As of the time this contract was written, `verbatim` doesn't work in
// inline assembly. Assignment of a value to a variable costs gas
// (although how much is unpredictable because it depends on the Yul/IR
// optimizer), as does the `GAS` opcode itself. Also solc tends to reorder
// the call to `gas()` with preparing the arguments for `div`. Therefore,
// the `gas()` below returns less than the actual amount of gas available
// for computation at the end of the call. That makes this check slightly
// too conservative. However, we do not correct for this because the
// correction would become outdated (possibly too permissive) if the
// opcodes are repriced.

// https://eips.ethereum.org/EIPS/eip-150
// https://ronan.eth.link/blog/ethereum-gas-dangers/
if iszero(or(success, or(returndatasize(), lt(div(callGas, 63), gas())))) {
// The call failed due to not enough gas left. We deliberately consume
// all remaining gas with `invalid` (instead of `revert`) to make this
// failure distinguishable to our caller.
invalid()
}

switch gt(returndatasize(), maxReturnBytes)
case 0 {
switch returndatasize()
case 0 {
returnData := 0x60
success := and(success, iszero(iszero(extcodesize(target))))
}
default {
mstore(returnData, returndatasize())
mstore(0x40, add(returnData, add(0x20, returndatasize())))
}
}
default {
// returnData = abi.encodeWithSignature("Error(string)", "CallWithGas: returnData too long")
success := 0
mstore(returnData, 0) // clear potentially dirty bits
mstore(add(returnData, 0x04), 0x6408c379a0) // length and selector
mstore(add(returnData, 0x24), 0x20)
mstore(add(returnData, 0x44), 0x20)
mstore(add(returnData, 0x64), "CallWithGas: returnData too long")
mstore(0x40, add(returnData, 0x84))
}
}
}

/// See `functionCallWithGasAndValue`
function functionCallWithGas(
address target,
bytes memory data,
uint256 callGas,
uint256 maxReturnBytes
) internal returns (bool success, bytes memory returnData) {
return functionCallWithGasAndValue(payable(target), data, callGas, 0, maxReturnBytes);
}

/**
* @notice `call` another contract forwarding a precomputed amount of gas.
* @notice Unlike `functionStaticCallWithGas`, a failure is not signaled if
* there is too much return data. Instead, it is simply truncated.
* @dev contains protections against EIP-150-induced insufficient gas griefing
* @dev reverts iff caller doesn't have enough native asset balance, the
* target is not a contract, or due to out-of-gas
* @return success true iff the call succeded
* @return returnData the return data or revert reason of the call
* @param target the contract (reverts if non-contract) on which to make the
* `call`
* @param data the calldata to pass
* @param callGas the gas to pass for the call. If the call requires more than
* the specified amount of gas and the caller didn't provide at
* least `callGas`, triggers an out-of-gas in the caller.
* @param value the amount of the native asset in wei to pass to the callee
* with the call
* @param maxReturnBytes Only this many bytes of return data/revert reason are
* read back from the call. This prevents griefing the
* caller. If more bytes are returned or the revert
* reason is longer, returnData will be truncated
*/
function functionCallWithGasAndValue(
address payable target,
bytes memory data,
uint256 callGas,
uint256 value,
uint256 maxReturnBytes
) internal returns (bool success, bytes memory returnData) {
if (value > 0 && (address(this).balance < value || target.code.length == 0)) {
return (success, returnData);
}

assembly ("memory-safe") {
returnData := mload(0x40)
success := call(callGas, target, value, add(data, 0x20), mload(data), add(returnData, 0x20), maxReturnBytes)

// As of the time this contract was written, `verbatim` doesn't work in
// inline assembly. Assignment of a value to a variable costs gas
// (although how much is unpredictable because it depends on the Yul/IR
// optimizer), as does the `GAS` opcode itself. Also solc tends to reorder
// the call to `gas()` with preparing the arguments for `div`. Therefore,
// the `gas()` below returns less than the actual amount of gas available
// for computation at the end of the call. That makes this check slightly
// too conservative. However, we do not correct for this because the
// correction would become outdated (possibly too permissive) if the
// opcodes are repriced.

// https://eips.ethereum.org/EIPS/eip-150
// https://ronan.eth.link/blog/ethereum-gas-dangers/
if iszero(or(success, or(returndatasize(), lt(div(callGas, 63), gas())))) {
// The call failed due to not enough gas left. We deliberately consume
// all remaining gas with `invalid` (instead of `revert`) to make this
// failure distinguishable to our caller.
invalid()
}

switch gt(returndatasize(), maxReturnBytes)
case 0 {
switch returndatasize()
case 0 {
returnData := 0x60
if iszero(value) {
success := and(success, iszero(iszero(extcodesize(target))))
}
}
default {
mstore(returnData, returndatasize())
mstore(0x40, add(returnData, add(0x20, returndatasize())))
}
}
default {
mstore(returnData, maxReturnBytes)
mstore(0x40, add(returnData, add(0x20, maxReturnBytes)))
}
}
}
}
39 changes: 39 additions & 0 deletions contracts/governance/src/IZeroExGovernor.sol
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2023 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.8.19;

import "./SecurityCouncil.sol";
import "@openzeppelin/governance/IGovernor.sol";
import "@openzeppelin/governance/extensions/IGovernorTimelock.sol";

abstract contract IZeroExGovernor is SecurityCouncil, IGovernor, IGovernorTimelock {
function token() public virtual returns (address);

function proposalThreshold() public view virtual returns (uint256);

function setVotingDelay(uint256 newVotingDelay) public virtual;

function setVotingPeriod(uint256 newVotingPeriod) public virtual;

function setProposalThreshold(uint256 newProposalThreshold) public virtual;

function proposalVotes(
uint256 proposalId
) public view virtual returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes);
}

0 comments on commit bcbfbfa

Please sign in to comment.