Decentralised governance of 0x protocol and treasury (#641)
* 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>
This commit is contained in:
parent
03fc744bbd
commit
bcbfbfa16c
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@ -31,6 +31,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Add foundry
|
||||||
|
uses: foundry-rs/foundry-toolchain@v1
|
||||||
|
with:
|
||||||
|
version: nightly
|
||||||
|
|
||||||
- name: Build solution
|
- name: Build solution
|
||||||
run: yarn build
|
run: yarn build
|
||||||
|
|
||||||
@ -78,11 +83,6 @@ jobs:
|
|||||||
-p @0x/order-utils \
|
-p @0x/order-utils \
|
||||||
-m --serial -c test:ci
|
-m --serial -c test:ci
|
||||||
|
|
||||||
- name: Add foundry
|
|
||||||
uses: foundry-rs/foundry-toolchain@v1
|
|
||||||
with:
|
|
||||||
version: nightly
|
|
||||||
|
|
||||||
- name: Run Forge build for erc20
|
- name: Run Forge build for erc20
|
||||||
working-directory: contracts/erc20
|
working-directory: contracts/erc20
|
||||||
run: |
|
run: |
|
||||||
@ -135,3 +135,26 @@ jobs:
|
|||||||
path: ./contracts/zero-ex/lcov.info
|
path: ./contracts/zero-ex/lcov.info
|
||||||
min_coverage: 6.98
|
min_coverage: 6.98
|
||||||
exclude: '**/tests'
|
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
.gitignore
vendored
18
.gitignore
vendored
@ -63,7 +63,16 @@ typings/
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
# built library using in commonjs module syntax
|
# 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
|
# UMD bundles that export the global variable
|
||||||
_bundles
|
_bundles
|
||||||
|
|
||||||
@ -97,10 +106,17 @@ out/
|
|||||||
# typechain wrappers
|
# typechain wrappers
|
||||||
contracts/zero-ex/typechain-wrappers/
|
contracts/zero-ex/typechain-wrappers/
|
||||||
|
|
||||||
|
# foundry packages
|
||||||
|
contracts/governance/cache
|
||||||
|
contracts/governance/out
|
||||||
|
|
||||||
# Doc README copy
|
# Doc README copy
|
||||||
packages/*/docs/README.md
|
packages/*/docs/README.md
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*~
|
||||||
|
\#*\#
|
||||||
|
.\#*
|
||||||
|
|
||||||
# the snapshot that gets built for migrations sure does have a ton of files
|
# the snapshot that gets built for migrations sure does have a ton of files
|
||||||
packages/migrations/0x_ganache_snapshot*
|
packages/migrations/0x_ganache_snapshot*
|
||||||
|
11
.gitmodules
vendored
11
.gitmodules
vendored
@ -3,4 +3,15 @@
|
|||||||
url = https://github.com/foundry-rs/forge-std
|
url = https://github.com/foundry-rs/forge-std
|
||||||
[submodule "contracts/erc20/lib/forge-std"]
|
[submodule "contracts/erc20/lib/forge-std"]
|
||||||
path = 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
|
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
contracts/governance/ZRXToken.json
Normal file
505
contracts/governance/ZRXToken.json
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
{
|
||||||
|
"abi": [
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "name",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "approve",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "totalSupply",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "transferFrom",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "balanceOf",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "symbol",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "transfer",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "allowance",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"payable": false,
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "_from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Transfer",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Approval",
|
||||||
|
"type": "event"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bytecode": {
|
||||||
|
"object": "0x60606040526b033b2e3c9fd0803ce8000000600355341561001c57fe5b5b600354600160a060020a0333166000908152602081905260409020555b5b61078d8061004a6000396000f300606060405236156100965763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde038114610098578063095ea7b31461014657806318160ddd1461018657806323b872dd146101a8578063313ce567146101ee57806370a082311461021457806395d89b411461024f578063a9059cbb146102fd578063dd62ed3e1461033d575bfe5b34156100a057fe5b6100a861037e565b60408051602080825283518183015283519192839290830191850190808383821561010c575b80518252602083111561010c577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016100ce565b505050905090810190601f1680156101385780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561014e57fe5b61017273ffffffffffffffffffffffffffffffffffffffff600435166024356103b5565b604080519115158252519081900360200190f35b341561018e57fe5b61019661042d565b60408051918252519081900360200190f35b34156101b057fe5b61017273ffffffffffffffffffffffffffffffffffffffff60043581169060243516604435610433565b604080519115158252519081900360200190f35b34156101f657fe5b6101fe6105d4565b6040805160ff9092168252519081900360200190f35b341561021c57fe5b61019673ffffffffffffffffffffffffffffffffffffffff600435166105d9565b60408051918252519081900360200190f35b341561025757fe5b6100a8610605565b60408051602080825283518183015283519192839290830191850190808383821561010c575b80518252602083111561010c577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016100ce565b505050905090810190601f1680156101385780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561030557fe5b61017273ffffffffffffffffffffffffffffffffffffffff6004351660243561063c565b604080519115158252519081900360200190f35b341561034557fe5b61019673ffffffffffffffffffffffffffffffffffffffff60043581169060243516610727565b60408051918252519081900360200190f35b60408051808201909152601181527f30782050726f746f636f6c20546f6b656e000000000000000000000000000000602082015281565b73ffffffffffffffffffffffffffffffffffffffff338116600081815260016020908152604080832094871680845294825280832086905580518681529051929493927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060015b92915050565b60035481565b73ffffffffffffffffffffffffffffffffffffffff808416600081815260016020908152604080832033909516835293815283822054928252819052918220548390108015906104835750828110155b80156104b6575073ffffffffffffffffffffffffffffffffffffffff841660009081526020819052604090205483810110155b156105c65773ffffffffffffffffffffffffffffffffffffffff808516600090815260208190526040808220805487019055918716815220805484900390557fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8110156105585773ffffffffffffffffffffffffffffffffffffffff808616600090815260016020908152604080832033909416835292905220805484900390555b8373ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef856040518082815260200191505060405180910390a3600191506105cb565b600091505b5b509392505050565b601281565b73ffffffffffffffffffffffffffffffffffffffff81166000908152602081905260409020545b919050565b60408051808201909152600381527f5a52580000000000000000000000000000000000000000000000000000000000602082015281565b73ffffffffffffffffffffffffffffffffffffffff3316600090815260208190526040812054829010801590610699575073ffffffffffffffffffffffffffffffffffffffff831660009081526020819052604090205482810110155b156107185773ffffffffffffffffffffffffffffffffffffffff33811660008181526020818152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a3506001610427565b506000610427565b5b92915050565b73ffffffffffffffffffffffffffffffffffffffff8083166000908152600160209081526040808320938516835292905220545b929150505600a165627a7a723058202dbef854545f38e5b78ec251d65db5fa0f12b6f2f0a0039063735c2dc416d6310029",
|
||||||
|
"sourceMap": "4935:353:0:-;;;5056:8;5027:37;;5208:78;;;;;;;5268:11;;-1:-1:-1;;;;;5254:10:0;5245:20;:8;:20;;;;;;;;;;:34;5208:78;4935:353;;;;;;;",
|
||||||
|
"linkReferences": {}
|
||||||
|
},
|
||||||
|
"deployedBytecode": {
|
||||||
|
"object": "0x606060405236156100965763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde038114610098578063095ea7b31461014657806318160ddd1461018657806323b872dd146101a8578063313ce567146101ee57806370a082311461021457806395d89b411461024f578063a9059cbb146102fd578063dd62ed3e1461033d575bfe5b34156100a057fe5b6100a861037e565b60408051602080825283518183015283519192839290830191850190808383821561010c575b80518252602083111561010c577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016100ce565b505050905090810190601f1680156101385780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561014e57fe5b61017273ffffffffffffffffffffffffffffffffffffffff600435166024356103b5565b604080519115158252519081900360200190f35b341561018e57fe5b61019661042d565b60408051918252519081900360200190f35b34156101b057fe5b61017273ffffffffffffffffffffffffffffffffffffffff60043581169060243516604435610433565b604080519115158252519081900360200190f35b34156101f657fe5b6101fe6105d4565b6040805160ff9092168252519081900360200190f35b341561021c57fe5b61019673ffffffffffffffffffffffffffffffffffffffff600435166105d9565b60408051918252519081900360200190f35b341561025757fe5b6100a8610605565b60408051602080825283518183015283519192839290830191850190808383821561010c575b80518252602083111561010c577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016100ce565b505050905090810190601f1680156101385780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561030557fe5b61017273ffffffffffffffffffffffffffffffffffffffff6004351660243561063c565b604080519115158252519081900360200190f35b341561034557fe5b61019673ffffffffffffffffffffffffffffffffffffffff60043581169060243516610727565b60408051918252519081900360200190f35b60408051808201909152601181527f30782050726f746f636f6c20546f6b656e000000000000000000000000000000602082015281565b73ffffffffffffffffffffffffffffffffffffffff338116600081815260016020908152604080832094871680845294825280832086905580518681529051929493927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060015b92915050565b60035481565b73ffffffffffffffffffffffffffffffffffffffff808416600081815260016020908152604080832033909516835293815283822054928252819052918220548390108015906104835750828110155b80156104b6575073ffffffffffffffffffffffffffffffffffffffff841660009081526020819052604090205483810110155b156105c65773ffffffffffffffffffffffffffffffffffffffff808516600090815260208190526040808220805487019055918716815220805484900390557fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8110156105585773ffffffffffffffffffffffffffffffffffffffff808616600090815260016020908152604080832033909416835292905220805484900390555b8373ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef856040518082815260200191505060405180910390a3600191506105cb565b600091505b5b509392505050565b601281565b73ffffffffffffffffffffffffffffffffffffffff81166000908152602081905260409020545b919050565b60408051808201909152600381527f5a52580000000000000000000000000000000000000000000000000000000000602082015281565b73ffffffffffffffffffffffffffffffffffffffff3316600090815260208190526040812054829010801590610699575073ffffffffffffffffffffffffffffffffffffffff831660009081526020819052604090205482810110155b156107185773ffffffffffffffffffffffffffffffffffffffff33811660008181526020818152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a3506001610427565b506000610427565b5b92915050565b73ffffffffffffffffffffffffffffffffffffffff8083166000908152600160209081526040808320938516835292905220545b929150505600a165627a7a723058202dbef854545f38e5b78ec251d65db5fa0f12b6f2f0a0039063735c2dc416d6310029",
|
||||||
|
"sourceMap": "4935:353:0:-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5109:49;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;18:2:-1;;13:3;7:5;32;59:3;53:5;48:3;41:6;93:2;88:3;85:2;78:6;73:3;67:5;152:3;;;;;117:2;108:3;;;;130;172:5;167:4;181:3;3:186;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;3523:190:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5027:37;;;;;;;;;;;;;;;;;;;;;;;;;;4369:562;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4986:35;;;;;;;;;;;;;;;;;;;;;;;;;;;;;3415:102;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5164:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;18:2:-1;;13:3;7:5;32;59:3;53:5;48:3;41:6;93:2;88:3;85:2;78:6;73:3;67:5;152:3;;;;;117:2;108:3;;;;130;172:5;167:4;181:3;3:186;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2490:433:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;3719:129;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5109:49;;;;;;;;;;;;;;;;;;;:::o;3523:190::-;3599:19;3607:10;3599:19;;3583:4;3599:19;;;:7;:19;;;;;;;;:29;;;;;;;;;;;;:38;;;3647;;;;;;;3583:4;;3599:29;:19;3647:38;;;;;;;;;;;-1:-1:-1;3702:4:0;3523:190;;;;;:::o;5027:37::-;;;;:::o;4369:562::-;4487:14;;;;4451:4;4487:14;;;:7;:14;;;;;;;;4502:10;4487:26;;;;;;;;;;;;4527:15;;;;;;;;;;:25;;;;;;:48;;;4569:6;4556:9;:19;;4527:48;:91;;;;-1:-1:-1;4605:13:0;;;:8;:13;;;;;;;;;;;4579:22;;;:39;;4527:91;4523:402;;;4634:13;;;;:8;:13;;;;;;;;;;;:23;;;;;;4671:15;;;;;;:25;;;;;;;4069:12;4714:20;;4710:95;;;4754:14;;;;;;;;:7;:14;;;;;;;;4769:10;4754:26;;;;;;;;;:36;;;;;;;4710:95;4834:3;4818:28;;4827:5;4818:28;;;4839:6;4818:28;;;;;;;;;;;;;;;;;;4867:4;4860:11;;;;4523:402;4909:5;4902:12;;4523:402;4369:562;;;;;;;:::o;4986:35::-;5019:2;4986:35;:::o;3415:102::-;3494:16;;;3468:7;3494:16;;;;;;;;;;;3415:102;;;;:::o;5164:37::-;;;;;;;;;;;;;;;;;;;:::o;2490:433::-;2635:20;2644:10;2635:20;2546:4;2635:20;;;;;;;;;;;:30;;;;;;:73;;-1:-1:-1;2695:13:0;;;:8;:13;;;;;;;;;;;2669:22;;;:39;;2635:73;2631:286;;;2724:20;2733:10;2724:20;;:8;:20;;;;;;;;;;;:30;;;;;;;2768:13;;;;;;;;;;:23;;;;;;2805:33;;;;;;;2768:13;;2805:33;;;;;;;;;;;-1:-1:-1;2859:4:0;2852:11;;2631:286;-1:-1:-1;2901:5:0;2894:12;;2631:286;2490:433;;;;;:::o;3719:129::-;3816:15;;;;3790:7;3816:15;;;:7;:15;;;;;;;;:25;;;;;;;;;;3719:129;;;;;:::o",
|
||||||
|
"linkReferences": {}
|
||||||
|
},
|
||||||
|
"methodIdentifiers": {
|
||||||
|
"allowance(address,address)": "dd62ed3e",
|
||||||
|
"approve(address,uint256)": "095ea7b3",
|
||||||
|
"balanceOf(address)": "70a08231",
|
||||||
|
"decimals()": "313ce567",
|
||||||
|
"name()": "06fdde03",
|
||||||
|
"symbol()": "95d89b41",
|
||||||
|
"totalSupply()": "18160ddd",
|
||||||
|
"transfer(address,uint256)": "a9059cbb",
|
||||||
|
"transferFrom(address,address,uint256)": "23b872dd"
|
||||||
|
},
|
||||||
|
"rawMetadata": "{\"compiler\":{\"version\":\"0.4.11+commit.68ef5810\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"constant\":true,\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_spender\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"name\":\"\",\"type\":\"uint8\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"},{\"name\":\"_spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"inputs\":[],\"payable\":false,\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_spender\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"}],\"devdoc\":{\"methods\":{\"transferFrom(address,address,uint256)\":{\"details\":\"ERC20 transferFrom, modified such that an allowance of MAX_UINT represents an unlimited allowance.\",\"params\":{\"_from\":\"Address to transfer from.\",\"_to\":\"Address to transfer to.\",\"_value\":\"Amount to transfer.\"},\"return\":\"Success of transfer.\"}}},\"userdoc\":{\"methods\":{}}},\"settings\":{\"compilationTarget\":{\"contracts/erc20/src/ZRXToken.sol\":\"ZRXToken\"},\"libraries\":{},\"optimizer\":{\"enabled\":true,\"runs\":1000000},\"remappings\":[\":@0x/contracts-erc20/=contracts/erc20/\",\":@0x/contracts-utils/=contracts/utils/\",\":ds-test/=contracts/erc20/lib/forge-std/lib/ds-test/src/\",\":forge-std/=contracts/erc20/lib/forge-std/src/\"]},\"sources\":{\"contracts/erc20/src/ZRXToken.sol\":{\"keccak256\":\"0x8582c06b20f8b7d3d603b485b5d26f840e01d1986381b334a856833edcff0d47\",\"urls\":[\"bzzr://ef728dddbaa1e26baa6cc9fe0f83de5055bc0b17dfe488018f4ee59d68ccb5dd\"]}},\"version\":1}",
|
||||||
|
"metadata": {
|
||||||
|
"compiler": {
|
||||||
|
"version": "0.4.11+commit.68ef5810"
|
||||||
|
},
|
||||||
|
"language": "Solidity",
|
||||||
|
"output": {
|
||||||
|
"abi": [
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"type": "function",
|
||||||
|
"name": "name",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "function",
|
||||||
|
"name": "approve",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"type": "function",
|
||||||
|
"name": "totalSupply",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "function",
|
||||||
|
"name": "transferFrom",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"type": "function",
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "uint8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "function",
|
||||||
|
"name": "balanceOf",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"type": "function",
|
||||||
|
"name": "symbol",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "function",
|
||||||
|
"name": "transfer",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "function",
|
||||||
|
"name": "allowance",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_from",
|
||||||
|
"type": "address",
|
||||||
|
"indexed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address",
|
||||||
|
"indexed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256",
|
||||||
|
"indexed": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "event",
|
||||||
|
"name": "Transfer",
|
||||||
|
"anonymous": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address",
|
||||||
|
"indexed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address",
|
||||||
|
"indexed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": null,
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256",
|
||||||
|
"indexed": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "event",
|
||||||
|
"name": "Approval",
|
||||||
|
"anonymous": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"devdoc": {
|
||||||
|
"methods": {
|
||||||
|
"transferFrom(address,address,uint256)": {
|
||||||
|
"details": "ERC20 transferFrom, modified such that an allowance of MAX_UINT represents an unlimited allowance.",
|
||||||
|
"params": {
|
||||||
|
"_from": "Address to transfer from.",
|
||||||
|
"_to": "Address to transfer to.",
|
||||||
|
"_value": "Amount to transfer."
|
||||||
|
},
|
||||||
|
"return": "Success of transfer."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"userdoc": {
|
||||||
|
"methods": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"remappings": [
|
||||||
|
":@0x/contracts-erc20/=contracts/erc20/",
|
||||||
|
":@0x/contracts-utils/=contracts/utils/",
|
||||||
|
":ds-test/=contracts/erc20/lib/forge-std/lib/ds-test/src/",
|
||||||
|
":forge-std/=contracts/erc20/lib/forge-std/src/"
|
||||||
|
],
|
||||||
|
"optimizer": {
|
||||||
|
"enabled": true,
|
||||||
|
"runs": 1000000
|
||||||
|
},
|
||||||
|
"compilationTarget": {
|
||||||
|
"contracts/erc20/src/ZRXToken.sol": "ZRXToken"
|
||||||
|
},
|
||||||
|
"libraries": {}
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"contracts/erc20/src/ZRXToken.sol": {
|
||||||
|
"keccak256": "0x8582c06b20f8b7d3d603b485b5d26f840e01d1986381b334a856833edcff0d47",
|
||||||
|
"urls": ["bzzr://ef728dddbaa1e26baa6cc9fe0f83de5055bc0b17dfe488018f4ee59d68ccb5dd"],
|
||||||
|
"license": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"id": 0
|
||||||
|
}
|
25
contracts/governance/foundry.toml
Normal file
25
contracts/governance/foundry.toml
Normal file
@ -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
contracts/governance/lib/forge-std
Submodule
1
contracts/governance/lib/forge-std
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit eb980e1d4f0e8173ec27da77297ae411840c8ccb
|
1
contracts/governance/lib/openzeppelin-contracts
Submodule
1
contracts/governance/lib/openzeppelin-contracts
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit d00acef4059807535af0bd0dd0ddf619747a044b
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit f6c4c9c4ec601665ca74d2c9dddf547fc425658c
|
21
contracts/governance/package.json
Normal file
21
contracts/governance/package.json
Normal file
@ -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
contracts/governance/src/CallWithGas.sol
Normal file
169
contracts/governance/src/CallWithGas.sol
Normal file
@ -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
contracts/governance/src/IZeroExGovernor.sol
Normal file
39
contracts/governance/src/IZeroExGovernor.sol
Normal file
@ -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);
|
||||||
|
}
|
132
contracts/governance/src/IZeroExVotes.sol
Normal file
132
contracts/governance/src/IZeroExVotes.sol
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
interface IZeroExVotes {
|
||||||
|
struct Checkpoint {
|
||||||
|
uint32 fromBlock;
|
||||||
|
uint96 votes;
|
||||||
|
uint96 quadraticVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Emitted when a token transfer or delegate change,
|
||||||
|
* results in changes to a delegate's quadratic number of votes.
|
||||||
|
*/
|
||||||
|
event DelegateQuadraticVotesChanged(
|
||||||
|
address indexed delegate,
|
||||||
|
uint256 previousQuadraticBalance,
|
||||||
|
uint256 newQuadraticBalance
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Emitted when a token transfer or delegate change, results in changes to a delegate's number of votes.
|
||||||
|
*/
|
||||||
|
event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Emitted when the total supply of the token is changed due to minting and burning which results in
|
||||||
|
* the total supply checkpoint being writtenor updated.
|
||||||
|
*/
|
||||||
|
event TotalSupplyChanged(uint256 totalSupplyVotes, uint256 totalSupplyQuadraticVotes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Get the `pos`-th checkpoint for `account`.
|
||||||
|
*/
|
||||||
|
function checkpoints(address account, uint32 pos) external view returns (Checkpoint memory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Get number of checkpoints for `account`.
|
||||||
|
*/
|
||||||
|
function numCheckpoints(address account) external view returns (uint32);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Gets the current votes balance for `account`
|
||||||
|
*/
|
||||||
|
function getVotes(address account) external view returns (uint256);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Gets the current quadratic votes balance for `account`
|
||||||
|
*/
|
||||||
|
function getQuadraticVotes(address account) external view returns (uint256);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Retrieve the number of votes for `account` at the end of `blockNumber`.
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
*
|
||||||
|
* - `blockNumber` must have been already mined
|
||||||
|
*/
|
||||||
|
function getPastVotes(address account, uint256 blockNumber) external view returns (uint256);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Retrieve the number of quadratic votes for `account` at the end of `blockNumber`.
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
*
|
||||||
|
* - `blockNumber` must have been already mined
|
||||||
|
*/
|
||||||
|
function getPastQuadraticVotes(address account, uint256 blockNumber) external view returns (uint256);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances.
|
||||||
|
* It is but NOT the sum of all the delegated votes!
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
*
|
||||||
|
* - `blockNumber` must have been already mined
|
||||||
|
*/
|
||||||
|
function getPastTotalSupply(uint256 blockNumber) external view returns (uint256);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Retrieve the sqrt of `totalSupply` at the end of `blockNumber`. Note, this value is the square root of the
|
||||||
|
* sum of all balances.
|
||||||
|
* It is but NOT the sum of all the sqrt of the delegated votes!
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
*
|
||||||
|
* - `blockNumber` must have been already mined
|
||||||
|
*/
|
||||||
|
function getPastQuadraticTotalSupply(uint256 blockNumber) external view returns (uint256);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Moves the voting power corresponding to `amount` number of tokens from `src` to `dst`.
|
||||||
|
* Note that if the delegator isn't delegating to anyone before the function call `src` = address(0)
|
||||||
|
* @param src the delegatee we are moving voting power away from
|
||||||
|
* @param dst the delegatee we are moving voting power to
|
||||||
|
* @param srcBalance balance of the delegator whose delegatee is `src`. This is value _after_ the transfer.
|
||||||
|
* @param dstBalance balance of the delegator whose delegatee is `dst`. This is value _after_ the transfer.
|
||||||
|
* @param srcBalanceLastUpdated block number when balance of `src` was last updated.
|
||||||
|
* @param dstBalanceLastUpdated block number when balance of `dst` was last updated.
|
||||||
|
* @param amount The amount of tokens transferred from the source delegate to destination delegate.
|
||||||
|
*/
|
||||||
|
function moveVotingPower(
|
||||||
|
address src,
|
||||||
|
address dst,
|
||||||
|
uint256 srcBalance,
|
||||||
|
uint256 dstBalance,
|
||||||
|
uint96 srcBalanceLastUpdated,
|
||||||
|
uint96 dstBalanceLastUpdated,
|
||||||
|
uint256 amount
|
||||||
|
) external returns (bool);
|
||||||
|
|
||||||
|
function writeCheckpointTotalSupplyMint(uint256 accountBalance, uint256 amount) external returns (bool);
|
||||||
|
|
||||||
|
function writeCheckpointTotalSupplyBurn(uint256 accountBalance, uint256 amount) external returns (bool);
|
||||||
|
}
|
81
contracts/governance/src/SecurityCouncil.sol
Normal file
81
contracts/governance/src/SecurityCouncil.sol
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
abstract contract SecurityCouncil {
|
||||||
|
address public securityCouncil;
|
||||||
|
|
||||||
|
event SecurityCouncilAssigned(address securityCouncil);
|
||||||
|
|
||||||
|
event SecurityCouncilEjected();
|
||||||
|
|
||||||
|
modifier onlySecurityCouncil() {
|
||||||
|
require(msg.sender == securityCouncil, "ZeroExProtocolGovernor: only security council allowed");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Checks that either a security council is assigned or the payloads array is a council assignment call.
|
||||||
|
*/
|
||||||
|
modifier securityCouncilAssigned(bytes[] memory payloads) {
|
||||||
|
if (securityCouncil == address(0) && !_payloadIsAssignSecurityCouncil(payloads)) {
|
||||||
|
revert("SecurityCouncil: security council not assigned and this is not an assignment call");
|
||||||
|
}
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Assigns new security council.
|
||||||
|
*/
|
||||||
|
function assignSecurityCouncil(address _securityCouncil) public virtual {
|
||||||
|
securityCouncil = _securityCouncil;
|
||||||
|
|
||||||
|
emit SecurityCouncilAssigned(securityCouncil);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Ejects the current security council via setting the security council address to 0.
|
||||||
|
* Security council is ejected after they either cancel a proposal or execute a protocol rollback.
|
||||||
|
*/
|
||||||
|
function ejectSecurityCouncil() internal {
|
||||||
|
securityCouncil = address(0);
|
||||||
|
emit SecurityCouncilEjected();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Cancel existing proposal with the submitted `targets`, `values`, `calldatas` and `descriptionHash`.
|
||||||
|
*/
|
||||||
|
function cancel(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) public virtual;
|
||||||
|
|
||||||
|
function _payloadIsAssignSecurityCouncil(bytes[] memory payloads) private pure returns (bool) {
|
||||||
|
require(payloads.length == 1, "SecurityCouncil: more than 1 transaction in proposal");
|
||||||
|
bytes memory payload = payloads[0];
|
||||||
|
// Check this is as assignSecurityCouncil(address) transaction
|
||||||
|
// function signature for assignSecurityCouncil(address)
|
||||||
|
// = bytes4(keccak256("assignSecurityCouncil(address)"))
|
||||||
|
// = 0x2761c3cd
|
||||||
|
if (bytes4(payload) == bytes4(0x2761c3cd)) return true;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
}
|
164
contracts/governance/src/ZRXWrappedToken.sol
Normal file
164
contracts/governance/src/ZRXWrappedToken.sol
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// 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 "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
import "@openzeppelin/token/ERC20/extensions/draft-ERC20Permit.sol";
|
||||||
|
import "@openzeppelin/token/ERC20/extensions/ERC20Wrapper.sol";
|
||||||
|
import "@openzeppelin/governance/utils/IVotes.sol";
|
||||||
|
import "@openzeppelin/utils/math/SafeCast.sol";
|
||||||
|
import "./IZeroExVotes.sol";
|
||||||
|
import "./CallWithGas.sol";
|
||||||
|
|
||||||
|
contract ZRXWrappedToken is ERC20, ERC20Permit, ERC20Wrapper {
|
||||||
|
using CallWithGas for address;
|
||||||
|
|
||||||
|
struct DelegateInfo {
|
||||||
|
address delegate;
|
||||||
|
uint96 balanceLastUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
IERC20 wrappedToken,
|
||||||
|
IZeroExVotes _zeroExVotes
|
||||||
|
) ERC20("Wrapped ZRX", "wZRX") ERC20Permit("Wrapped ZRX") ERC20Wrapper(wrappedToken) {
|
||||||
|
zeroExVotes = _zeroExVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
IZeroExVotes public immutable zeroExVotes;
|
||||||
|
mapping(address => DelegateInfo) private _delegates;
|
||||||
|
|
||||||
|
bytes32 private constant _DELEGATION_TYPEHASH =
|
||||||
|
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Emitted when an account changes their delegate.
|
||||||
|
*/
|
||||||
|
event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);
|
||||||
|
|
||||||
|
// The functions below are the required overrides from the base contracts
|
||||||
|
|
||||||
|
function decimals() public pure override(ERC20, ERC20Wrapper) returns (uint8) {
|
||||||
|
return 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _afterTokenTransfer(address from, address to, uint256 amount) internal override(ERC20) {
|
||||||
|
super._afterTokenTransfer(from, to, amount);
|
||||||
|
|
||||||
|
DelegateInfo memory fromDelegate = delegateInfo(from);
|
||||||
|
DelegateInfo memory toDelegate = delegateInfo(to);
|
||||||
|
|
||||||
|
uint256 fromBalance = fromDelegate.delegate == address(0) ? 0 : balanceOf(from) + amount;
|
||||||
|
uint256 toBalance = toDelegate.delegate == address(0) ? 0 : balanceOf(to) - amount;
|
||||||
|
|
||||||
|
if (fromDelegate.delegate != address(0)) _delegates[from].balanceLastUpdated = SafeCast.toUint96(block.number);
|
||||||
|
|
||||||
|
if (toDelegate.delegate != address(0)) _delegates[to].balanceLastUpdated = SafeCast.toUint96(block.number);
|
||||||
|
|
||||||
|
zeroExVotes.moveVotingPower(
|
||||||
|
fromDelegate.delegate,
|
||||||
|
toDelegate.delegate,
|
||||||
|
fromBalance,
|
||||||
|
toBalance,
|
||||||
|
fromDelegate.balanceLastUpdated,
|
||||||
|
toDelegate.balanceLastUpdated,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _mint(address account, uint256 amount) internal override(ERC20) {
|
||||||
|
super._mint(account, amount);
|
||||||
|
|
||||||
|
zeroExVotes.writeCheckpointTotalSupplyMint(balanceOf(account) - amount, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _burn(address account, uint256 amount) internal override(ERC20) {
|
||||||
|
super._burn(account, amount);
|
||||||
|
|
||||||
|
address(zeroExVotes).functionCallWithGas(
|
||||||
|
abi.encodeCall(zeroExVotes.writeCheckpointTotalSupplyBurn, (balanceOf(account) + amount, amount)),
|
||||||
|
500_000,
|
||||||
|
32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Get the address `account` is currently delegating to.
|
||||||
|
*/
|
||||||
|
function delegates(address account) public view returns (address) {
|
||||||
|
return _delegates[account].delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Get the last block number when `account`'s balance changed.
|
||||||
|
*/
|
||||||
|
function delegatorBalanceLastUpdated(address account) public view returns (uint96) {
|
||||||
|
return _delegates[account].balanceLastUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delegateInfo(address account) public view returns (DelegateInfo memory) {
|
||||||
|
return _delegates[account];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Delegate votes from the sender to `delegatee`.
|
||||||
|
*/
|
||||||
|
function delegate(address delegatee) public {
|
||||||
|
_delegate(_msgSender(), delegatee);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Delegates votes from signer to `delegatee`
|
||||||
|
*/
|
||||||
|
function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) public {
|
||||||
|
require(block.timestamp <= expiry, "ERC20Votes: signature expired");
|
||||||
|
address signer = ECDSA.recover(
|
||||||
|
_hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))),
|
||||||
|
v,
|
||||||
|
r,
|
||||||
|
s
|
||||||
|
);
|
||||||
|
require(nonce == _useNonce(signer), "ERC20Votes: invalid nonce");
|
||||||
|
_delegate(signer, delegatee);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Change delegation for `delegator` to `delegatee`.
|
||||||
|
*
|
||||||
|
* Emits events {DelegateChanged} and {IZeroExVotes-DelegateVotesChanged}.
|
||||||
|
*/
|
||||||
|
function _delegate(address delegator, address delegatee) internal virtual {
|
||||||
|
DelegateInfo memory delegateInfo = delegateInfo(delegator);
|
||||||
|
uint256 delegatorBalance = balanceOf(delegator);
|
||||||
|
|
||||||
|
_delegates[delegator] = DelegateInfo(delegatee, SafeCast.toUint96(block.timestamp));
|
||||||
|
|
||||||
|
emit DelegateChanged(delegator, delegateInfo.delegate, delegatee);
|
||||||
|
|
||||||
|
zeroExVotes.moveVotingPower(
|
||||||
|
delegateInfo.delegate,
|
||||||
|
delegatee,
|
||||||
|
delegatorBalance,
|
||||||
|
0,
|
||||||
|
delegateInfo.balanceLastUpdated,
|
||||||
|
0,
|
||||||
|
delegatorBalance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
147
contracts/governance/src/ZeroExProtocolGovernor.sol
Normal file
147
contracts/governance/src/ZeroExProtocolGovernor.sol
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// 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 "./ZeroExTimelock.sol";
|
||||||
|
import "@openzeppelin/governance/Governor.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorSettings.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorCountingSimple.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorVotes.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorTimelockControl.sol";
|
||||||
|
|
||||||
|
contract ZeroExProtocolGovernor is
|
||||||
|
SecurityCouncil,
|
||||||
|
Governor,
|
||||||
|
GovernorSettings,
|
||||||
|
GovernorCountingSimple,
|
||||||
|
GovernorVotes,
|
||||||
|
GovernorTimelockControl
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
IVotes _votes,
|
||||||
|
ZeroExTimelock _timelock,
|
||||||
|
address _securityCouncil
|
||||||
|
)
|
||||||
|
Governor("ZeroExProtocolGovernor")
|
||||||
|
GovernorSettings(14400 /* 2 days */, 50400 /* 7 days */, 1000000e18)
|
||||||
|
GovernorVotes(_votes)
|
||||||
|
GovernorTimelockControl(TimelockController(payable(_timelock)))
|
||||||
|
{
|
||||||
|
securityCouncil = _securityCouncil;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quorum(uint256 blockNumber) public pure override returns (uint256) {
|
||||||
|
return 10000000e18;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following functions are overrides required by Solidity.
|
||||||
|
|
||||||
|
function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) {
|
||||||
|
return super.votingDelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) {
|
||||||
|
return super.votingPeriod();
|
||||||
|
}
|
||||||
|
|
||||||
|
function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) {
|
||||||
|
return super.state(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function propose(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
string memory description
|
||||||
|
) public override(Governor, IGovernor) securityCouncilAssigned(calldatas) returns (uint256) {
|
||||||
|
return super.propose(targets, values, calldatas, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
|
||||||
|
return super.proposalThreshold();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) public override onlySecurityCouncil {
|
||||||
|
_cancel(targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like the GovernorTimelockControl.queue function but without the proposal checks,
|
||||||
|
// (as there's effectively no proposal).
|
||||||
|
// And also using a delay of 0 as opposed to the minimum delay of the timelock
|
||||||
|
function executeRollback(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) public {
|
||||||
|
require(msg.sender == securityCouncil, "ZeroExProtocolGovernor: only security council allowed");
|
||||||
|
|
||||||
|
// Execute the batch of rollbacks via the timelock controller
|
||||||
|
ZeroExTimelock timelockController = ZeroExTimelock(payable(timelock()));
|
||||||
|
timelockController.executeRollbackBatch(targets, values, calldatas, 0, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignSecurityCouncil(address _securityCouncil) public override onlyGovernance {
|
||||||
|
super.assignSecurityCouncil(_securityCouncil);
|
||||||
|
}
|
||||||
|
|
||||||
|
function queue(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) public override securityCouncilAssigned(calldatas) returns (uint256) {
|
||||||
|
return super.queue(targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _execute(
|
||||||
|
uint256 proposalId,
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) internal override(Governor, GovernorTimelockControl) {
|
||||||
|
super._execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cancel(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
|
||||||
|
return super._cancel(targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
|
||||||
|
return super._executor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsInterface(
|
||||||
|
bytes4 interfaceId
|
||||||
|
) public view override(Governor, GovernorTimelockControl) returns (bool) {
|
||||||
|
return super.supportsInterface(interfaceId);
|
||||||
|
}
|
||||||
|
}
|
69
contracts/governance/src/ZeroExTimelock.sol
Normal file
69
contracts/governance/src/ZeroExTimelock.sol
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// 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 "@openzeppelin/governance/TimelockController.sol";
|
||||||
|
|
||||||
|
contract ZeroExTimelock is TimelockController {
|
||||||
|
// minDelay is how long you have to wait before executing
|
||||||
|
// proposers is the list of addresses that can propose
|
||||||
|
// executors is the list of addresses that can execute
|
||||||
|
constructor(
|
||||||
|
uint256 minDelay,
|
||||||
|
address[] memory proposers,
|
||||||
|
address[] memory executors,
|
||||||
|
address admin
|
||||||
|
) TimelockController(minDelay, proposers, executors, admin) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Execute a batch of rollback transactions. Similar to TimelockController.executeBatch function but without
|
||||||
|
* the timelock checks.
|
||||||
|
* Emits one {CallExecuted} event per transaction in the batch.
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
*
|
||||||
|
* - the caller must have the 'executor' role.
|
||||||
|
*/
|
||||||
|
function executeRollbackBatch(
|
||||||
|
address[] calldata targets,
|
||||||
|
uint256[] calldata values,
|
||||||
|
bytes[] calldata payloads,
|
||||||
|
bytes32 predecessor,
|
||||||
|
bytes32 salt
|
||||||
|
) public payable onlyRoleOrOpenRole(EXECUTOR_ROLE) {
|
||||||
|
require(targets.length == values.length, "ZeroExTimelock: length mismatch");
|
||||||
|
require(targets.length == payloads.length, "ZeroExTimelock: length mismatch");
|
||||||
|
|
||||||
|
bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt);
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < targets.length; ++i) {
|
||||||
|
address target = targets[i];
|
||||||
|
uint256 value = values[i];
|
||||||
|
bytes calldata payload = payloads[i];
|
||||||
|
// Check this is a rollback transaction
|
||||||
|
// function signature for rollback(bytes4,address)
|
||||||
|
// = bytes4(keccak256("rollback(bytes4,address)"))
|
||||||
|
// = 0x9db64a40
|
||||||
|
require(bytes4(payload) == bytes4(0x9db64a40), "ZeroExTimelock: not rollback");
|
||||||
|
|
||||||
|
_execute(target, value, payload);
|
||||||
|
emit CallExecuted(id, i, target, value, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
contracts/governance/src/ZeroExTreasuryGovernor.sol
Normal file
154
contracts/governance/src/ZeroExTreasuryGovernor.sol
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// 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 "@openzeppelin/governance/Governor.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorSettings.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorCountingSimple.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorVotes.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorVotesQuorumFraction.sol";
|
||||||
|
import "@openzeppelin/governance/extensions/GovernorTimelockControl.sol";
|
||||||
|
|
||||||
|
import "./IZeroExVotes.sol";
|
||||||
|
import "./SecurityCouncil.sol";
|
||||||
|
|
||||||
|
contract ZeroExTreasuryGovernor is
|
||||||
|
SecurityCouncil,
|
||||||
|
Governor,
|
||||||
|
GovernorSettings,
|
||||||
|
GovernorCountingSimple,
|
||||||
|
GovernorVotes,
|
||||||
|
GovernorVotesQuorumFraction,
|
||||||
|
GovernorTimelockControl
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
IVotes votes,
|
||||||
|
TimelockController _timelock,
|
||||||
|
address _securityCouncil
|
||||||
|
)
|
||||||
|
Governor("ZeroExTreasuryGovernor")
|
||||||
|
GovernorSettings(14400 /* 2 days */, 50400 /* 7 days */, 250000e18)
|
||||||
|
GovernorVotes(votes)
|
||||||
|
GovernorVotesQuorumFraction(10)
|
||||||
|
GovernorTimelockControl(_timelock)
|
||||||
|
{
|
||||||
|
securityCouncil = _securityCouncil;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the "quadratic" quorum for a block number, in terms of number of votes:
|
||||||
|
* `quadratic total supply * numerator / denominator`
|
||||||
|
*/
|
||||||
|
function quorum(
|
||||||
|
uint256 blockNumber
|
||||||
|
) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) {
|
||||||
|
IZeroExVotes votes = IZeroExVotes(address(token));
|
||||||
|
return (votes.getPastQuadraticTotalSupply(blockNumber) * quorumNumerator(blockNumber)) / quorumDenominator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following functions are overrides required by Solidity.
|
||||||
|
|
||||||
|
function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) {
|
||||||
|
return super.votingDelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) {
|
||||||
|
return super.votingPeriod();
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
|
||||||
|
return super.proposalThreshold();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwritten GovernorVotes implementation
|
||||||
|
* Read the quadratic voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}).
|
||||||
|
*/
|
||||||
|
function _getVotes(
|
||||||
|
address account,
|
||||||
|
uint256 blockNumber,
|
||||||
|
bytes memory /*params*/
|
||||||
|
) internal view virtual override(Governor, GovernorVotes) returns (uint256) {
|
||||||
|
return IZeroExVotes(address(token)).getPastQuadraticVotes(account, blockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) {
|
||||||
|
return super.state(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function propose(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
string memory description
|
||||||
|
) public override(Governor, IGovernor) securityCouncilAssigned(calldatas) returns (uint256) {
|
||||||
|
return super.propose(targets, values, calldatas, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) public override onlySecurityCouncil {
|
||||||
|
_cancel(targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignSecurityCouncil(address _securityCouncil) public override onlyGovernance {
|
||||||
|
super.assignSecurityCouncil(_securityCouncil);
|
||||||
|
}
|
||||||
|
|
||||||
|
function queue(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) public override securityCouncilAssigned(calldatas) returns (uint256) {
|
||||||
|
return super.queue(targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _execute(
|
||||||
|
uint256 proposalId,
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) internal override(Governor, GovernorTimelockControl) {
|
||||||
|
super._execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cancel(
|
||||||
|
address[] memory targets,
|
||||||
|
uint256[] memory values,
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
|
||||||
|
return super._cancel(targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
|
||||||
|
return super._executor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsInterface(
|
||||||
|
bytes4 interfaceId
|
||||||
|
) public view override(Governor, GovernorTimelockControl) returns (bool) {
|
||||||
|
return super.supportsInterface(interfaceId);
|
||||||
|
}
|
||||||
|
}
|
329
contracts/governance/src/ZeroExVotes.sol
Normal file
329
contracts/governance/src/ZeroExVotes.sol
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
// 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 "@openzeppelin/utils/math/SafeCast.sol";
|
||||||
|
import "@openzeppelin/utils/math/Math.sol";
|
||||||
|
import "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
import "@openzeppelin/governance/utils/IVotes.sol";
|
||||||
|
import "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||||
|
import "@openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol";
|
||||||
|
import "@openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||||
|
import "./IZeroExVotes.sol";
|
||||||
|
|
||||||
|
contract ZeroExVotes is IZeroExVotes, Initializable, OwnableUpgradeable, UUPSUpgradeable {
|
||||||
|
address public immutable token;
|
||||||
|
uint256 public immutable quadraticThreshold;
|
||||||
|
|
||||||
|
mapping(address => Checkpoint[]) internal _checkpoints;
|
||||||
|
Checkpoint[] private _totalSupplyCheckpoints;
|
||||||
|
|
||||||
|
constructor(address _token, uint256 _quadraticThreshold) {
|
||||||
|
require(_token != address(0), "ZeroExVotes: token cannot be 0");
|
||||||
|
token = _token;
|
||||||
|
quadraticThreshold = _quadraticThreshold;
|
||||||
|
_disableInitializers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
|
||||||
|
|
||||||
|
modifier onlyToken() {
|
||||||
|
require(msg.sender == token, "ZeroExVotes: only token allowed");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize() public virtual onlyProxy initializer {
|
||||||
|
__Ownable_init();
|
||||||
|
__UUPSUpgradeable_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function checkpoints(address account, uint32 pos) public view returns (Checkpoint memory) {
|
||||||
|
return _checkpoints[account][pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function numCheckpoints(address account) public view returns (uint32) {
|
||||||
|
return SafeCast.toUint32(_checkpoints[account].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function getVotes(address account) public view returns (uint256) {
|
||||||
|
uint256 pos = _checkpoints[account].length;
|
||||||
|
return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function getQuadraticVotes(address account) public view returns (uint256) {
|
||||||
|
uint256 pos = _checkpoints[account].length;
|
||||||
|
return pos == 0 ? 0 : _checkpoints[account][pos - 1].quadraticVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) {
|
||||||
|
require(blockNumber < block.number, "ZeroExVotes: block not yet mined");
|
||||||
|
|
||||||
|
Checkpoint memory checkpoint = _checkpointsLookup(_checkpoints[account], blockNumber);
|
||||||
|
return checkpoint.votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function getPastQuadraticVotes(address account, uint256 blockNumber) public view returns (uint256) {
|
||||||
|
require(blockNumber < block.number, "ZeroExVotes: block not yet mined");
|
||||||
|
|
||||||
|
Checkpoint memory checkpoint = _checkpointsLookup(_checkpoints[account], blockNumber);
|
||||||
|
return checkpoint.quadraticVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) {
|
||||||
|
require(blockNumber < block.number, "ZeroExVotes: block not yet mined");
|
||||||
|
|
||||||
|
// Note that due to the disabled updates of `_totalSupplyCheckpoints` in `writeCheckpointTotalSupply` function
|
||||||
|
// this always returns 0.
|
||||||
|
Checkpoint memory checkpoint = _checkpointsLookup(_totalSupplyCheckpoints, blockNumber);
|
||||||
|
return checkpoint.votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function getPastQuadraticTotalSupply(uint256 blockNumber) public view returns (uint256) {
|
||||||
|
require(blockNumber < block.number, "ZeroExVotes: block not yet mined");
|
||||||
|
|
||||||
|
// Note that due to the disabled updates of `_totalSupplyCheckpoints` in `writeCheckpointTotalSupply` function
|
||||||
|
// this always returns 0.
|
||||||
|
Checkpoint memory checkpoint = _checkpointsLookup(_totalSupplyCheckpoints, blockNumber);
|
||||||
|
return checkpoint.quadraticVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function moveVotingPower(
|
||||||
|
address src,
|
||||||
|
address dst,
|
||||||
|
uint256 srcBalance,
|
||||||
|
uint256 dstBalance,
|
||||||
|
uint96 srcBalanceLastUpdated,
|
||||||
|
uint96 dstBalanceLastUpdated,
|
||||||
|
uint256 amount
|
||||||
|
) public virtual onlyToken returns (bool) {
|
||||||
|
if (src != dst) {
|
||||||
|
if (src != address(0)) {
|
||||||
|
(
|
||||||
|
uint256 oldWeight,
|
||||||
|
uint256 newWeight,
|
||||||
|
uint256 oldQuadraticWeight,
|
||||||
|
uint256 newQuadraticWeight
|
||||||
|
) = _writeCheckpoint(_checkpoints[src], _subtract, srcBalance, srcBalanceLastUpdated, amount);
|
||||||
|
|
||||||
|
emit DelegateVotesChanged(src, oldWeight, newWeight);
|
||||||
|
emit DelegateQuadraticVotesChanged(src, oldQuadraticWeight, newQuadraticWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dst != address(0)) {
|
||||||
|
(
|
||||||
|
uint256 oldWeight,
|
||||||
|
uint256 newWeight,
|
||||||
|
uint256 oldQuadraticWeight,
|
||||||
|
uint256 newQuadraticWeight
|
||||||
|
) = _writeCheckpoint(_checkpoints[dst], _add, dstBalance, dstBalanceLastUpdated, amount);
|
||||||
|
|
||||||
|
emit DelegateVotesChanged(dst, oldWeight, newWeight);
|
||||||
|
emit DelegateQuadraticVotesChanged(dst, oldQuadraticWeight, newQuadraticWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function writeCheckpointTotalSupplyMint(
|
||||||
|
uint256 accountBalance,
|
||||||
|
uint256 amount
|
||||||
|
) public virtual onlyToken returns (bool) {
|
||||||
|
(, uint256 newWeight, , uint256 newQuadraticWeight) = _writeCheckpoint(
|
||||||
|
_totalSupplyCheckpoints,
|
||||||
|
_add,
|
||||||
|
accountBalance,
|
||||||
|
0,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
|
||||||
|
emit TotalSupplyChanged(newWeight, newQuadraticWeight);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc IZeroExVotes
|
||||||
|
*/
|
||||||
|
function writeCheckpointTotalSupplyBurn(
|
||||||
|
uint256 accountBalance,
|
||||||
|
uint256 amount
|
||||||
|
) public virtual onlyToken returns (bool) {
|
||||||
|
(, uint256 newWeight, , uint256 newQuadraticWeight) = _writeCheckpoint(
|
||||||
|
_totalSupplyCheckpoints,
|
||||||
|
_subtract,
|
||||||
|
accountBalance,
|
||||||
|
0,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
|
||||||
|
emit TotalSupplyChanged(newWeight, newQuadraticWeight);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Lookup a value in a list of (sorted) checkpoints.
|
||||||
|
* Implementation as in openzeppelin/token/ERC20/extensions/ERC20Votes.sol except here we return the entire
|
||||||
|
* checkpoint rather than part of it
|
||||||
|
*/
|
||||||
|
function _checkpointsLookup(
|
||||||
|
Checkpoint[] storage ckpts,
|
||||||
|
uint256 blockNumber
|
||||||
|
) internal view returns (Checkpoint memory) {
|
||||||
|
// We run a binary search to look for the earliest checkpoint taken after `blockNumber`.
|
||||||
|
//
|
||||||
|
// Initially we check if the block is recent to narrow the search range.
|
||||||
|
// During the loop, the index of the wanted checkpoint remains in the range [low-1, high).
|
||||||
|
// With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the
|
||||||
|
// invariant.
|
||||||
|
// - If the middle checkpoint is after `blockNumber`, we look in [low, mid)
|
||||||
|
// - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high)
|
||||||
|
// Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not
|
||||||
|
// out of bounds (in which case we're looking too far in the past and the result is 0).
|
||||||
|
// Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is
|
||||||
|
// past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out
|
||||||
|
// the same.
|
||||||
|
uint256 length = ckpts.length;
|
||||||
|
|
||||||
|
uint256 low = 0;
|
||||||
|
uint256 high = length;
|
||||||
|
|
||||||
|
if (length > 5) {
|
||||||
|
uint256 mid = length - Math.sqrt(length);
|
||||||
|
if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) {
|
||||||
|
high = mid;
|
||||||
|
} else {
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (low < high) {
|
||||||
|
uint256 mid = Math.average(low, high);
|
||||||
|
if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) {
|
||||||
|
high = mid;
|
||||||
|
} else {
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaving here for posterity this is the original OZ implementation which we've replaced
|
||||||
|
// return high == 0 ? 0 : _unsafeAccess(ckpts, high - 1).votes;
|
||||||
|
Checkpoint memory checkpoint = high == 0 ? Checkpoint(0, 0, 0) : _unsafeAccess(ckpts, high - 1);
|
||||||
|
return checkpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _writeCheckpoint(
|
||||||
|
Checkpoint[] storage ckpts,
|
||||||
|
function(uint256, uint256) view returns (uint256) op,
|
||||||
|
uint256 userBalance,
|
||||||
|
uint96 balanceLastUpdated,
|
||||||
|
uint256 delta
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
virtual
|
||||||
|
returns (uint256 oldWeight, uint256 newWeight, uint256 oldQuadraticWeight, uint256 newQuadraticWeight)
|
||||||
|
{
|
||||||
|
uint256 pos = ckpts.length;
|
||||||
|
|
||||||
|
Checkpoint memory oldCkpt = pos == 0 ? Checkpoint(0, 0, 0) : _unsafeAccess(ckpts, pos - 1);
|
||||||
|
|
||||||
|
oldWeight = oldCkpt.votes;
|
||||||
|
newWeight = op(oldWeight, delta);
|
||||||
|
|
||||||
|
oldQuadraticWeight = oldCkpt.quadraticVotes;
|
||||||
|
|
||||||
|
// Remove the entire sqrt userBalance from quadratic voting power.
|
||||||
|
// Note that `userBalance` is value _after_ transfer.
|
||||||
|
if (pos > 0) {
|
||||||
|
uint256 oldQuadraticVotingPower = userBalance <= quadraticThreshold
|
||||||
|
? userBalance
|
||||||
|
: quadraticThreshold + Math.sqrt((userBalance - quadraticThreshold) * 1e18);
|
||||||
|
oldCkpt.quadraticVotes -= SafeCast.toUint96(oldQuadraticVotingPower);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if wallet > threshold, calculate quadratic power over the treshold only, below threshold is linear
|
||||||
|
uint256 newBalance = op(userBalance, delta);
|
||||||
|
uint256 newQuadraticBalance = newBalance <= quadraticThreshold
|
||||||
|
? newBalance
|
||||||
|
: quadraticThreshold + Math.sqrt((newBalance - quadraticThreshold) * 1e18);
|
||||||
|
newQuadraticWeight = oldCkpt.quadraticVotes + newQuadraticBalance;
|
||||||
|
|
||||||
|
if (pos > 0 && oldCkpt.fromBlock == block.number) {
|
||||||
|
Checkpoint storage chpt = _unsafeAccess(ckpts, pos - 1);
|
||||||
|
chpt.votes = SafeCast.toUint96(newWeight);
|
||||||
|
chpt.quadraticVotes = SafeCast.toUint96(newQuadraticWeight);
|
||||||
|
} else {
|
||||||
|
ckpts.push(
|
||||||
|
Checkpoint({
|
||||||
|
fromBlock: SafeCast.toUint32(block.number),
|
||||||
|
votes: SafeCast.toUint96(newWeight),
|
||||||
|
quadraticVotes: SafeCast.toUint96(newQuadraticWeight)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _add(uint256 a, uint256 b) private pure returns (uint256) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _subtract(uint256 a, uint256 b) private pure returns (uint256) {
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
|
||||||
|
* Implementation from openzeppelin/token/ERC20/extensions/ERC20Votes.sol
|
||||||
|
* https://github.com/ethereum/solidity/issues/9117
|
||||||
|
*/
|
||||||
|
function _unsafeAccess(Checkpoint[] storage ckpts, uint256 pos) internal pure returns (Checkpoint storage result) {
|
||||||
|
assembly ("memory-safe") {
|
||||||
|
mstore(0, ckpts.slot)
|
||||||
|
result.slot := add(keccak256(0, 0x20), pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
contracts/governance/test/BaseTest.t.sol
Normal file
116
contracts/governance/test/BaseTest.t.sol
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// 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 "forge-std/Test.sol";
|
||||||
|
import "forge-std/console.sol";
|
||||||
|
import "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
import "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
|
||||||
|
import "./ZRXMock.sol";
|
||||||
|
import "../src/ZRXWrappedToken.sol";
|
||||||
|
import "../src/ZeroExVotes.sol";
|
||||||
|
import "../src/ZeroExTimelock.sol";
|
||||||
|
import "../src/ZeroExProtocolGovernor.sol";
|
||||||
|
import "../src/ZeroExTreasuryGovernor.sol";
|
||||||
|
|
||||||
|
function predict(address deployer, uint256 nonce) pure returns (address) {
|
||||||
|
require(nonce > 0 && nonce < 128);
|
||||||
|
return address(uint160(uint256(keccak256(abi.encodePacked(bytes2(0xd694), deployer, bytes1(uint8(nonce)))))));
|
||||||
|
}
|
||||||
|
|
||||||
|
contract BaseTest is Test {
|
||||||
|
address payable internal account1 = payable(vm.addr(1));
|
||||||
|
address payable internal account2 = payable(vm.addr(2));
|
||||||
|
address payable internal account3 = payable(vm.addr(3));
|
||||||
|
address payable internal account4 = payable(vm.addr(4));
|
||||||
|
address payable internal securityCouncil = payable(vm.addr(5));
|
||||||
|
uint256 internal quadraticThreshold = 1000000e18;
|
||||||
|
|
||||||
|
bytes32 internal constant DELEGATION_TYPEHASH =
|
||||||
|
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
vm.deal(account1, 1e20);
|
||||||
|
vm.deal(account2, 1e20);
|
||||||
|
vm.deal(account3, 1e20);
|
||||||
|
vm.deal(account4, 1e20);
|
||||||
|
vm.deal(securityCouncil, 1e20);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupGovernance()
|
||||||
|
internal
|
||||||
|
returns (IERC20, ZRXWrappedToken, ZeroExVotes, ZeroExTimelock, ZeroExTimelock, address, address)
|
||||||
|
{
|
||||||
|
(IERC20 zrxToken, ZRXWrappedToken token, ZeroExVotes votes) = setupZRXWrappedToken();
|
||||||
|
|
||||||
|
vm.startPrank(account1);
|
||||||
|
address[] memory proposers = new address[](0);
|
||||||
|
address[] memory executors = new address[](0);
|
||||||
|
|
||||||
|
ZeroExTimelock protocolTimelock = new ZeroExTimelock(3 days, proposers, executors, account1);
|
||||||
|
ZeroExProtocolGovernor protocolGovernor = new ZeroExProtocolGovernor(
|
||||||
|
IVotes(address(votes)),
|
||||||
|
protocolTimelock,
|
||||||
|
securityCouncil
|
||||||
|
);
|
||||||
|
protocolTimelock.grantRole(protocolTimelock.PROPOSER_ROLE(), address(protocolGovernor));
|
||||||
|
protocolTimelock.grantRole(protocolTimelock.EXECUTOR_ROLE(), address(protocolGovernor));
|
||||||
|
protocolTimelock.grantRole(protocolTimelock.CANCELLER_ROLE(), address(protocolGovernor));
|
||||||
|
|
||||||
|
ZeroExTimelock treasuryTimelock = new ZeroExTimelock(2 days, proposers, executors, account1);
|
||||||
|
ZeroExTreasuryGovernor treasuryGovernor = new ZeroExTreasuryGovernor(
|
||||||
|
IVotes(address(votes)),
|
||||||
|
treasuryTimelock,
|
||||||
|
securityCouncil
|
||||||
|
);
|
||||||
|
|
||||||
|
treasuryTimelock.grantRole(treasuryTimelock.PROPOSER_ROLE(), address(treasuryGovernor));
|
||||||
|
treasuryTimelock.grantRole(treasuryTimelock.EXECUTOR_ROLE(), address(treasuryGovernor));
|
||||||
|
treasuryTimelock.grantRole(treasuryTimelock.CANCELLER_ROLE(), address(treasuryGovernor));
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
return (
|
||||||
|
zrxToken,
|
||||||
|
token,
|
||||||
|
votes,
|
||||||
|
protocolTimelock,
|
||||||
|
treasuryTimelock,
|
||||||
|
address(protocolGovernor),
|
||||||
|
address(treasuryGovernor)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupZRXWrappedToken() internal returns (IERC20, ZRXWrappedToken, ZeroExVotes) {
|
||||||
|
vm.startPrank(account1);
|
||||||
|
bytes memory _bytecode = vm.getCode("./ZRXToken.json");
|
||||||
|
IERC20 zrxToken;
|
||||||
|
assembly {
|
||||||
|
zrxToken := create(0, add(_bytecode, 0x20), mload(_bytecode))
|
||||||
|
}
|
||||||
|
address wTokenPrediction = predict(account1, vm.getNonce(account1) + 2);
|
||||||
|
ZeroExVotes votesImpl = new ZeroExVotes(wTokenPrediction, quadraticThreshold);
|
||||||
|
ERC1967Proxy votesProxy = new ERC1967Proxy(address(votesImpl), abi.encodeCall(votesImpl.initialize, ()));
|
||||||
|
ZRXWrappedToken wToken = new ZRXWrappedToken(zrxToken, ZeroExVotes(address(votesProxy)));
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assert(address(wToken) == wTokenPrediction);
|
||||||
|
return (zrxToken, wToken, ZeroExVotes(address(votesProxy)));
|
||||||
|
}
|
||||||
|
}
|
21
contracts/governance/test/CubeRoot.sol
Normal file
21
contracts/governance/test/CubeRoot.sol
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.19;
|
||||||
|
|
||||||
|
library CubeRoot {
|
||||||
|
/// @dev Returns the cube root of `x`.
|
||||||
|
/// Credit to pleasemarkdarkly under MIT license
|
||||||
|
// Originaly from https://github.com/pleasemarkdarkly/fei-protocol-core-hh/blob/main/contracts/utils/Roots.sol
|
||||||
|
function cbrt(uint y) internal pure returns (uint z) {
|
||||||
|
// Newton's method https://en.wikipedia.org/wiki/Cube_root#Numerical_methods
|
||||||
|
if (y > 7) {
|
||||||
|
z = y;
|
||||||
|
uint x = y / 3 + 1;
|
||||||
|
while (x < z) {
|
||||||
|
z = x;
|
||||||
|
x = (y / (x * x) + (2 * x)) / 3;
|
||||||
|
}
|
||||||
|
} else if (y != 0) {
|
||||||
|
z = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
contracts/governance/test/ZRXMock.sol
Normal file
31
contracts/governance/test/ZRXMock.sol
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// 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 "forge-std/Test.sol";
|
||||||
|
import "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
|
||||||
|
// TODO remove this contract and work with an instance of ZRX compiled with 0.4
|
||||||
|
// when the following is resolved https://linear.app/0xproject/issue/PRO-44/zrx-artifact-is-incompatible-with-foundry
|
||||||
|
contract ZRXMock is ERC20 {
|
||||||
|
constructor() ERC20("0x Protocol Token", "ZRX") {
|
||||||
|
_mint(msg.sender, 10 ** 27);
|
||||||
|
}
|
||||||
|
}
|
314
contracts/governance/test/ZRXWrappedTokenTest.t.sol
Normal file
314
contracts/governance/test/ZRXWrappedTokenTest.t.sol
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
// 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 "./BaseTest.t.sol";
|
||||||
|
import "../src/ZRXWrappedToken.sol";
|
||||||
|
import "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
|
||||||
|
contract ZRXWrappedTokenTest is BaseTest {
|
||||||
|
IERC20 private token;
|
||||||
|
ZRXWrappedToken private wToken;
|
||||||
|
ZeroExVotes private votes;
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
(token, wToken, votes, , , , ) = setupGovernance();
|
||||||
|
vm.startPrank(account1);
|
||||||
|
token.transfer(account2, 100e18);
|
||||||
|
token.transfer(account3, 200e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectSymbol() public {
|
||||||
|
string memory wZRXSymbol = wToken.symbol();
|
||||||
|
assertEq(wZRXSymbol, "wZRX");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectName() public {
|
||||||
|
string memory wZRXName = wToken.name();
|
||||||
|
assertEq(wZRXName, "Wrapped ZRX");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectNumberOfDecimals() public {
|
||||||
|
uint8 wZRXDecimals = wToken.decimals();
|
||||||
|
assertEq(wZRXDecimals, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToWrapZRX() public {
|
||||||
|
vm.startPrank(account2);
|
||||||
|
|
||||||
|
// Approve the wrapped token and deposit 1e18 ZRX
|
||||||
|
token.approve(address(wToken), 1e18);
|
||||||
|
wToken.depositFor(account2, 1e18);
|
||||||
|
|
||||||
|
// Check the token balances even out
|
||||||
|
uint256 wTokenBalance = wToken.balanceOf(account2);
|
||||||
|
assertEq(wTokenBalance, 1e18);
|
||||||
|
uint256 tokenBalance = token.balanceOf(account2);
|
||||||
|
assertEq(tokenBalance, 100e18 - wTokenBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToUnwrapToZRX() public {
|
||||||
|
vm.startPrank(account2);
|
||||||
|
|
||||||
|
// Approve the wrapped token and deposit 1e18 ZRX
|
||||||
|
token.approve(address(wToken), 1e18);
|
||||||
|
wToken.depositFor(account2, 1e18);
|
||||||
|
|
||||||
|
// Withdraw 1e6 wZRX back to ZRX to own account
|
||||||
|
wToken.withdrawTo(account2, 1e6);
|
||||||
|
|
||||||
|
// Check token balances even out
|
||||||
|
uint256 wTokenBalance = wToken.balanceOf(account2);
|
||||||
|
assertEq(wTokenBalance, 1e18 - 1e6);
|
||||||
|
uint256 tokenBalance = token.balanceOf(account2);
|
||||||
|
assertEq(tokenBalance, 100e18 - wTokenBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToUnwrapToZRXToAnotherAccount() public {
|
||||||
|
vm.startPrank(account2);
|
||||||
|
|
||||||
|
// Approve the wrapped token and deposit 1e18 ZRX
|
||||||
|
token.approve(address(wToken), 1e18);
|
||||||
|
wToken.depositFor(account2, 1e18);
|
||||||
|
|
||||||
|
// Withdraw 1e7 wZRX back to ZRX to account4 (which owns no tokens to start with)
|
||||||
|
wToken.withdrawTo(account4, 1e7);
|
||||||
|
|
||||||
|
// Check token balances even out
|
||||||
|
uint256 wTokenBalance2 = wToken.balanceOf(account2);
|
||||||
|
assertEq(wTokenBalance2, 1e18 - 1e7);
|
||||||
|
|
||||||
|
uint256 tokenBalance4 = token.balanceOf(account4);
|
||||||
|
assertEq(tokenBalance4, 1e7);
|
||||||
|
|
||||||
|
uint256 tokenBalance2 = token.balanceOf(account2);
|
||||||
|
assertEq(tokenBalance2, 100e18 - wTokenBalance2 - tokenBalance4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWrappedZRXTotalsAreCorrect() public {
|
||||||
|
// Wrap 1e18 and check total supply is correct
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1e18);
|
||||||
|
wToken.depositFor(account2, 1e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
uint256 wTokenBalance = wToken.totalSupply();
|
||||||
|
assertEq(wTokenBalance, 1e18);
|
||||||
|
|
||||||
|
// Wrap 2e18 more and check total supply is correct
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 2e18);
|
||||||
|
wToken.depositFor(account3, 2e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
wTokenBalance = wToken.totalSupply();
|
||||||
|
assertEq(wTokenBalance, 1e18 + 2e18);
|
||||||
|
|
||||||
|
// Unwrap 1e7 and check total supply is correct
|
||||||
|
vm.startPrank(account2);
|
||||||
|
wToken.withdrawTo(account2, 1e7);
|
||||||
|
vm.stopPrank();
|
||||||
|
wTokenBalance = wToken.totalSupply();
|
||||||
|
assertEq(wTokenBalance, 3e18 - 1e7);
|
||||||
|
|
||||||
|
// Unwrap 8e17 and check total supply is correct
|
||||||
|
vm.startPrank(account2);
|
||||||
|
wToken.withdrawTo(account2, 8e17);
|
||||||
|
vm.stopPrank();
|
||||||
|
wTokenBalance = wToken.totalSupply();
|
||||||
|
assertEq(wTokenBalance, 3e18 - 1e7 - 8e17);
|
||||||
|
|
||||||
|
// We are not keeping record of total balances so check they are zero
|
||||||
|
assertEq(votes.getPastTotalSupply(0), 0);
|
||||||
|
assertEq(votes.getPastQuadraticTotalSupply(0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWhenMintingFirstTimeForAccountTotalSupplyCheckpointsAreCorrect() public {
|
||||||
|
vm.startPrank(account2);
|
||||||
|
|
||||||
|
// Approve the wrapped token and deposit 1e18 ZRX
|
||||||
|
token.approve(address(wToken), 1e18);
|
||||||
|
vm.roll(2);
|
||||||
|
wToken.depositFor(account2, 1e18);
|
||||||
|
vm.roll(3);
|
||||||
|
|
||||||
|
// Check the totals are correct
|
||||||
|
uint256 totalSupplyVotes = votes.getPastTotalSupply(2);
|
||||||
|
uint256 totalSupplyQuadraticVotes = votes.getPastQuadraticTotalSupply(2);
|
||||||
|
assertEq(totalSupplyVotes, 1e18);
|
||||||
|
assertEq(totalSupplyQuadraticVotes, 1e18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWhenMintingForAccountWithExistingBalanceTotalSupplyCheckpointsAreCorrect() public {
|
||||||
|
vm.startPrank(account2);
|
||||||
|
|
||||||
|
// Approve the wrapped token and deposit 1e18 ZRX
|
||||||
|
token.approve(address(wToken), 5e18);
|
||||||
|
wToken.depositFor(account2, 1e18);
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
// Depost 3e18 more for the same account
|
||||||
|
wToken.depositFor(account2, 3e18);
|
||||||
|
vm.roll(3);
|
||||||
|
|
||||||
|
// Check the totals are correct
|
||||||
|
uint256 totalSupplyVotes = votes.getPastTotalSupply(2);
|
||||||
|
uint256 totalSupplyQuadraticVotes = votes.getPastQuadraticTotalSupply(2);
|
||||||
|
assertEq(totalSupplyVotes, 4e18);
|
||||||
|
assertEq(totalSupplyQuadraticVotes, 4e18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWhenMintingForMultipleAccountsTotalSupplyCheckpointsAreCorrect() public {
|
||||||
|
// Deposit 1e18 ZRX by account2
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 5e18);
|
||||||
|
wToken.depositFor(account2, 1e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Deposit 2e18 ZRX by account3
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 2e18);
|
||||||
|
wToken.depositFor(account3, 2e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Deposit 4e18 ZRX by account2
|
||||||
|
vm.startPrank(account2);
|
||||||
|
vm.roll(2);
|
||||||
|
wToken.depositFor(account2, 4e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
vm.roll(3);
|
||||||
|
|
||||||
|
// Check the totals are correct
|
||||||
|
uint256 totalSupplyVotes = votes.getPastTotalSupply(2);
|
||||||
|
uint256 totalSupplyQuadraticVotes = votes.getPastQuadraticTotalSupply(2);
|
||||||
|
assertEq(totalSupplyVotes, 7e18);
|
||||||
|
assertEq(totalSupplyQuadraticVotes, 5e18 + 2e18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWhenBurningForMultipleAccountsTotalSupplyCheckpointsAreCorrect() public {
|
||||||
|
// Deposit 5e18 ZRX by account2
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 5e18);
|
||||||
|
wToken.depositFor(account2, 5e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Deposit 2e18 ZRX by account3
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 2e18);
|
||||||
|
wToken.depositFor(account3, 2e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Burn 4e18 ZRX by account2
|
||||||
|
vm.startPrank(account2);
|
||||||
|
vm.roll(2);
|
||||||
|
wToken.withdrawTo(account2, 4e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
vm.roll(3);
|
||||||
|
|
||||||
|
// Check the totals are correct
|
||||||
|
uint256 totalSupplyVotes = votes.getPastTotalSupply(2);
|
||||||
|
uint256 totalSupplyQuadraticVotes = votes.getPastQuadraticTotalSupply(2);
|
||||||
|
assertEq(totalSupplyVotes, 3e18);
|
||||||
|
assertEq(totalSupplyQuadraticVotes, 1e18 + 2e18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToTransferCorrectly() public {
|
||||||
|
assertEq(wToken.balanceOf(account4), 0);
|
||||||
|
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1e18);
|
||||||
|
wToken.depositFor(account2, 1e18);
|
||||||
|
wToken.transfer(account4, 1e17);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(wToken.balanceOf(account4), 1e17);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldTransferVotingPowerWhenTransferringTokens() public {
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to itself
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 10e18);
|
||||||
|
wToken.depositFor(account2, 10e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
|
||||||
|
wToken.transfer(account3, 3e18);
|
||||||
|
|
||||||
|
assertEq(wToken.balanceOf(account2), 7e18);
|
||||||
|
assertEq(wToken.balanceOf(account3), 3e18);
|
||||||
|
|
||||||
|
assertEq(votes.getVotes(account2), 7e18);
|
||||||
|
assertEq(votes.getQuadraticVotes(account2), 7e18);
|
||||||
|
|
||||||
|
// Since account3 is not delegating to anyone, they should have no voting power
|
||||||
|
assertEq(votes.getVotes(account3), 0);
|
||||||
|
assertEq(votes.getQuadraticVotes(account3), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldUpdateVotingPowerWhenDepositing() public {
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to itself
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 10e18);
|
||||||
|
wToken.depositFor(account2, 7e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
|
||||||
|
assertEq(votes.getVotes(account2), 7e18);
|
||||||
|
assertEq(votes.getQuadraticVotes(account2), 7e18);
|
||||||
|
|
||||||
|
wToken.depositFor(account2, 2e18);
|
||||||
|
assertEq(votes.getVotes(account2), 9e18);
|
||||||
|
assertEq(votes.getQuadraticVotes(account2), 9e18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldUpdateVotingPowerWhenWithdrawing() public {
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to itself
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 10e18);
|
||||||
|
wToken.depositFor(account2, 10e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
|
||||||
|
assertEq(votes.getVotes(account2), 10e18);
|
||||||
|
assertEq(votes.getQuadraticVotes(account2), 10e18);
|
||||||
|
|
||||||
|
wToken.withdrawTo(account2, 2e18);
|
||||||
|
assertEq(votes.getVotes(account2), 8e18);
|
||||||
|
assertEq(votes.getQuadraticVotes(account2), 8e18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldSetDelegateBalanceLastUpdatedOnTransfer() public {
|
||||||
|
ZRXWrappedToken.DelegateInfo memory account2DelegateInfo = wToken.delegateInfo(account2);
|
||||||
|
assertEq(account2DelegateInfo.delegate, address(0));
|
||||||
|
assertEq(account2DelegateInfo.balanceLastUpdated, 0);
|
||||||
|
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to account3
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 10e18);
|
||||||
|
wToken.depositFor(account2, 10e18);
|
||||||
|
wToken.delegate(account3);
|
||||||
|
|
||||||
|
account2DelegateInfo = wToken.delegateInfo(account2);
|
||||||
|
assertEq(account2DelegateInfo.delegate, account3);
|
||||||
|
assertEq(account2DelegateInfo.balanceLastUpdated, 1); // Set to the block.number
|
||||||
|
|
||||||
|
vm.roll(3);
|
||||||
|
wToken.transfer(account3, 3e18);
|
||||||
|
|
||||||
|
account2DelegateInfo = wToken.delegateInfo(account2);
|
||||||
|
assertEq(account2DelegateInfo.delegate, account3);
|
||||||
|
assertEq(account2DelegateInfo.balanceLastUpdated, 3);
|
||||||
|
}
|
||||||
|
}
|
460
contracts/governance/test/ZeroExGovernorBaseTest.t.sol
Normal file
460
contracts/governance/test/ZeroExGovernorBaseTest.t.sol
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
// 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 IZeroExGovernorANY 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 "./BaseTest.t.sol";
|
||||||
|
import "../src/IZeroExGovernor.sol";
|
||||||
|
import "../src/ZeroExTimelock.sol";
|
||||||
|
import "../src/ZeroExProtocolGovernor.sol";
|
||||||
|
import "../src/ZRXWrappedToken.sol";
|
||||||
|
import "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
import "@openzeppelin/mocks/CallReceiverMock.sol";
|
||||||
|
|
||||||
|
abstract contract ZeroExGovernorBaseTest is BaseTest {
|
||||||
|
IERC20 public token;
|
||||||
|
ZRXWrappedToken internal wToken;
|
||||||
|
ZeroExVotes internal votes;
|
||||||
|
ZeroExTimelock internal timelock;
|
||||||
|
IZeroExGovernor internal governor;
|
||||||
|
CallReceiverMock internal callReceiverMock;
|
||||||
|
|
||||||
|
string internal governorName;
|
||||||
|
uint256 internal proposalThreshold;
|
||||||
|
|
||||||
|
event SecurityCouncilAssigned(address securityCouncil);
|
||||||
|
event SecurityCouncilEjected();
|
||||||
|
|
||||||
|
function initialiseAccounts() public {
|
||||||
|
vm.startPrank(account1);
|
||||||
|
token.transfer(account2, 10000000e18);
|
||||||
|
token.transfer(account3, 2000000e18);
|
||||||
|
token.transfer(account4, 3000000e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Setup accounts 2,3 and 4 to vote
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 10000000e18);
|
||||||
|
wToken.depositFor(account2, 10000000e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 2000000e18);
|
||||||
|
wToken.depositFor(account3, 2000000e18);
|
||||||
|
wToken.delegate(account3);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
vm.startPrank(account4);
|
||||||
|
token.approve(address(wToken), 3000000e18);
|
||||||
|
wToken.depositFor(account4, 3000000e18);
|
||||||
|
wToken.delegate(account4);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
callReceiverMock = new CallReceiverMock();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSecurityCouncil(address council) internal {
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(governor);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSelector(governor.assignSecurityCouncil.selector, council);
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Assign new security council");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Assign new security council")));
|
||||||
|
vm.warp(governor.proposalEta(proposalId) + 1);
|
||||||
|
|
||||||
|
// Execute proposal
|
||||||
|
governor.execute(targets, values, calldatas, keccak256("Assign new security council"));
|
||||||
|
|
||||||
|
assertEq(governor.securityCouncil(), council);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectName() public {
|
||||||
|
assertEq(governor.name(), governorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectVotingDelay() public {
|
||||||
|
assertEq(governor.votingDelay(), 14400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectVotingPeriod() public {
|
||||||
|
assertEq(governor.votingPeriod(), 50400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectProposalThreshold() public {
|
||||||
|
assertEq(governor.proposalThreshold(), proposalThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectToken() public {
|
||||||
|
assertEq(address(governor.token()), address(votes));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectTimelock() public {
|
||||||
|
assertEq(address(governor.timelock()), address(timelock));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectSecurityCouncil() public {
|
||||||
|
assertEq(governor.securityCouncil(), securityCouncil);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCanAssignSecurityCouncil() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(governor);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSelector(governor.assignSecurityCouncil.selector, account1);
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Assign new security council");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Assign new security council")));
|
||||||
|
vm.warp(governor.proposalEta(proposalId) + 1);
|
||||||
|
|
||||||
|
// Execute proposal
|
||||||
|
vm.expectEmit(true, false, false, false);
|
||||||
|
emit SecurityCouncilAssigned(account1);
|
||||||
|
governor.execute(targets, values, calldatas, keccak256("Assign new security council"));
|
||||||
|
|
||||||
|
assertEq(governor.securityCouncil(), account1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCannotAssignSecurityCouncilOutsideOfGovernance() public {
|
||||||
|
vm.expectRevert("Governor: onlyGovernance");
|
||||||
|
governor.assignSecurityCouncil(account1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This functionality is currently not enabled
|
||||||
|
// Leaving this test for potential future use.
|
||||||
|
function testFailSecurityCouncilAreEjectedAfterCancellingAProposal() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(callReceiverMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSignature("mockFunction()");
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Proposal description");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
IGovernor.ProposalState state = governor.state(proposalId);
|
||||||
|
assertEq(uint256(state), uint256(IGovernor.ProposalState.Succeeded));
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Proposal description")));
|
||||||
|
|
||||||
|
// Cancel the proposal
|
||||||
|
vm.warp(governor.proposalEta(proposalId));
|
||||||
|
|
||||||
|
vm.prank(securityCouncil);
|
||||||
|
|
||||||
|
vm.expectEmit(true, false, false, false);
|
||||||
|
emit SecurityCouncilEjected();
|
||||||
|
governor.cancel(targets, values, calldatas, keccak256(bytes("Proposal description")));
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
state = governor.state(proposalId);
|
||||||
|
assertEq(uint256(state), uint256(IGovernor.ProposalState.Canceled));
|
||||||
|
|
||||||
|
assertEq(governor.securityCouncil(), address(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWhenNoSecurityCouncilCannottSubmitProposals() public {
|
||||||
|
setSecurityCouncil(address(0));
|
||||||
|
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(callReceiverMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSignature("mockFunction()");
|
||||||
|
|
||||||
|
vm.expectRevert("SecurityCouncil: security council not assigned and this is not an assignment call");
|
||||||
|
governor.propose(targets, values, calldatas, "Proposal description");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWhenNoSecurityCouncilCannotQueueSuccessfulProposals() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(callReceiverMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSignature("mockFunction()");
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Proposal description");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Set security council to address(0)
|
||||||
|
setSecurityCouncil(address(0));
|
||||||
|
|
||||||
|
vm.expectRevert("SecurityCouncil: security council not assigned and this is not an assignment call");
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Proposal description")));
|
||||||
|
|
||||||
|
IGovernor.ProposalState state = governor.state(proposalId);
|
||||||
|
assertEq(uint256(state), uint256(IGovernor.ProposalState.Succeeded));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWhenNoSecurityCouncilCanPassProposalToAssignSecurityCouncil() public {
|
||||||
|
setSecurityCouncil(address(0));
|
||||||
|
|
||||||
|
setSecurityCouncil(account1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCannotPassABadProposalToSetSecurityCouncil() public {
|
||||||
|
setSecurityCouncil(address(0));
|
||||||
|
|
||||||
|
address[] memory targets = new address[](2);
|
||||||
|
targets[0] = address(governor);
|
||||||
|
targets[1] = address(callReceiverMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](2);
|
||||||
|
values[0] = 0;
|
||||||
|
values[1] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](2);
|
||||||
|
calldatas[0] = abi.encodeWithSelector(governor.assignSecurityCouncil.selector, account1);
|
||||||
|
calldatas[1] = abi.encodeWithSignature("mockFunction()");
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
vm.expectRevert("SecurityCouncil: more than 1 transaction in proposal");
|
||||||
|
governor.propose(targets, values, calldatas, "Assign new security council");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCanUpdateVotingDelaySetting() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(governor);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSelector(governor.setVotingDelay.selector, 3 days);
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Increase voting delay to 3 days");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Increase voting delay to 3 days")));
|
||||||
|
vm.warp(governor.proposalEta(proposalId) + 1);
|
||||||
|
// Execute proposal
|
||||||
|
governor.execute(targets, values, calldatas, keccak256("Increase voting delay to 3 days"));
|
||||||
|
|
||||||
|
assertEq(governor.votingDelay(), 3 days);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCanUpdateVotingPeriodSetting() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(governor);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSelector(governor.setVotingPeriod.selector, 14 days);
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Increase voting period to 14 days");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Increase voting period to 14 days")));
|
||||||
|
vm.warp(governor.proposalEta(proposalId) + 1);
|
||||||
|
// Execute proposal
|
||||||
|
governor.execute(targets, values, calldatas, keccak256("Increase voting period to 14 days"));
|
||||||
|
|
||||||
|
assertEq(governor.votingPeriod(), 14 days);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCanUpdateProposalThresholdSetting() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(governor);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSelector(governor.setProposalThreshold.selector, 2000000e18);
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Increase proposal threshold to 2000000e18");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Increase proposal threshold to 2000000e18")));
|
||||||
|
vm.warp(governor.proposalEta(proposalId) + 1);
|
||||||
|
// Execute proposal
|
||||||
|
governor.execute(targets, values, calldatas, keccak256("Increase proposal threshold to 2000000e18"));
|
||||||
|
|
||||||
|
assertEq(governor.proposalThreshold(), 2000000e18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCanUpdateTimelockDelay() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(timelock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSelector(timelock.updateDelay.selector, 7 days);
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Increase timelock delay to 7 days");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Increase timelock delay to 7 days")));
|
||||||
|
vm.warp(governor.proposalEta(proposalId) + 1);
|
||||||
|
// Execute proposal
|
||||||
|
governor.execute(targets, values, calldatas, keccak256("Increase timelock delay to 7 days"));
|
||||||
|
|
||||||
|
assertEq(timelock.getMinDelay(), 7 days);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSupportsGovernanceInterfaces() public {
|
||||||
|
assertTrue(governor.supportsInterface(type(IGovernorTimelock).interfaceId));
|
||||||
|
assertTrue(governor.supportsInterface(type(IGovernor).interfaceId));
|
||||||
|
assertTrue(governor.supportsInterface(type(IERC1155Receiver).interfaceId));
|
||||||
|
}
|
||||||
|
}
|
11
contracts/governance/test/ZeroExMock.sol
Normal file
11
contracts/governance/test/ZeroExMock.sol
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.8.19;
|
||||||
|
|
||||||
|
contract ZeroExMock {
|
||||||
|
mapping(bytes4 => address) public implementations;
|
||||||
|
|
||||||
|
function rollback(bytes4 selector, address targetImpl) public {
|
||||||
|
implementations[selector] = targetImpl;
|
||||||
|
}
|
||||||
|
}
|
167
contracts/governance/test/ZeroExProtocolGovernor.t.sol
Normal file
167
contracts/governance/test/ZeroExProtocolGovernor.t.sol
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// 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 "./ZeroExGovernorBaseTest.t.sol";
|
||||||
|
import "./ZeroExMock.sol";
|
||||||
|
import "../src/ZeroExProtocolGovernor.sol";
|
||||||
|
|
||||||
|
contract ZeroExProtocolGovernorTest is ZeroExGovernorBaseTest {
|
||||||
|
ZeroExProtocolGovernor internal protocolGovernor;
|
||||||
|
ZeroExMock internal zeroExMock;
|
||||||
|
uint256 internal quorum;
|
||||||
|
|
||||||
|
event CallExecuted(bytes32 indexed id, uint256 indexed index, address target, uint256 value, bytes data);
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
governorName = "ZeroExProtocolGovernor";
|
||||||
|
proposalThreshold = 1000000e18;
|
||||||
|
quorum = 10000000e18;
|
||||||
|
|
||||||
|
address governorAddress;
|
||||||
|
(token, wToken, votes, timelock, , governorAddress, ) = setupGovernance();
|
||||||
|
governor = IZeroExGovernor(governorAddress);
|
||||||
|
protocolGovernor = ZeroExProtocolGovernor(payable(governorAddress));
|
||||||
|
zeroExMock = new ZeroExMock();
|
||||||
|
initialiseAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectQuorum() public {
|
||||||
|
assertEq(governor.quorum(block.number), quorum);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToExecuteASuccessfulProposal() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(callReceiverMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSignature("mockFunction()");
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Proposal description");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
vm.prank(account3);
|
||||||
|
governor.castVote(proposalId, 0); // Vote "against"
|
||||||
|
vm.stopPrank();
|
||||||
|
vm.prank(account4);
|
||||||
|
governor.castVote(proposalId, 2); // Vote "abstain"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Get vote results
|
||||||
|
(uint256 votesAgainst, uint256 votesFor, uint256 votesAbstain) = governor.proposalVotes(proposalId);
|
||||||
|
assertEq(votesFor, 10000000e18);
|
||||||
|
assertEq(votesAgainst, 2000000e18);
|
||||||
|
assertEq(votesAbstain, 3000000e18);
|
||||||
|
|
||||||
|
IGovernor.ProposalState state = governor.state(proposalId);
|
||||||
|
assertEq(uint256(state), uint256(IGovernor.ProposalState.Succeeded));
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Proposal description")));
|
||||||
|
vm.warp(governor.proposalEta(proposalId) + 1);
|
||||||
|
|
||||||
|
governor.execute(targets, values, calldatas, keccak256("Proposal description"));
|
||||||
|
state = governor.state(proposalId);
|
||||||
|
assertEq(uint256(state), uint256(IGovernor.ProposalState.Executed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSecurityCouncilShouldBeAbleToExecuteRollback() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(zeroExMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
bytes4 testFunctionSig = 0xc853c969;
|
||||||
|
address testFunctionImpl = 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f;
|
||||||
|
calldatas[0] = abi.encodeWithSignature("rollback(bytes4,address)", testFunctionSig, testFunctionImpl);
|
||||||
|
|
||||||
|
// Security council adds the batch of rollbacks to the queue
|
||||||
|
vm.startPrank(securityCouncil);
|
||||||
|
|
||||||
|
bytes32 proposalId = timelock.hashOperationBatch(
|
||||||
|
targets,
|
||||||
|
values,
|
||||||
|
calldatas,
|
||||||
|
0,
|
||||||
|
keccak256(bytes("Emergency rollback"))
|
||||||
|
);
|
||||||
|
vm.expectEmit(true, true, true, true);
|
||||||
|
emit CallExecuted(proposalId, 0, targets[0], values[0], calldatas[0]);
|
||||||
|
|
||||||
|
// This functionality is currently not enabled
|
||||||
|
// Leaving this test for potential future use.
|
||||||
|
// vm.expectEmit(true, false, false, false);
|
||||||
|
// emit SecurityCouncilEjected();
|
||||||
|
|
||||||
|
protocolGovernor.executeRollback(targets, values, calldatas, keccak256(bytes("Emergency rollback")));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSecurityCouncilShouldNotBeAbleToExecuteArbitraryFunctions() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(callReceiverMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSignature("mockFunction()");
|
||||||
|
|
||||||
|
vm.startPrank(securityCouncil);
|
||||||
|
vm.expectRevert("ZeroExTimelock: not rollback");
|
||||||
|
protocolGovernor.executeRollback(targets, values, calldatas, keccak256(bytes("Proposal description")));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testRollbackShouldNotBeExecutableByNonSecurityCouncilAccounts() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(zeroExMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
bytes4 testFunctionSig = 0xc853c969;
|
||||||
|
address testFunctionImpl = 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f;
|
||||||
|
calldatas[0] = abi.encodeWithSignature("rollback(bytes4,address)", testFunctionSig, testFunctionImpl);
|
||||||
|
|
||||||
|
vm.startPrank(account2);
|
||||||
|
vm.expectRevert("ZeroExProtocolGovernor: only security council allowed");
|
||||||
|
protocolGovernor.executeRollback(targets, values, calldatas, keccak256(bytes("Emergency rollback")));
|
||||||
|
}
|
||||||
|
}
|
96
contracts/governance/test/ZeroExTreasuryGovernor.t.sol
Normal file
96
contracts/governance/test/ZeroExTreasuryGovernor.t.sol
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// 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 "./ZeroExGovernorBaseTest.t.sol";
|
||||||
|
|
||||||
|
contract ZeroExTreasuryGovernorTest is ZeroExGovernorBaseTest {
|
||||||
|
function setUp() public {
|
||||||
|
governorName = "ZeroExTreasuryGovernor";
|
||||||
|
proposalThreshold = 250000e18;
|
||||||
|
|
||||||
|
address governorAddress;
|
||||||
|
(token, wToken, votes, , timelock, , governorAddress) = setupGovernance();
|
||||||
|
governor = IZeroExGovernor(governorAddress);
|
||||||
|
|
||||||
|
initialiseAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldReturnCorrectQuorum() public {
|
||||||
|
vm.roll(3);
|
||||||
|
uint256 totalSupplyQuadraticVotes = quadraticThreshold *
|
||||||
|
3 +
|
||||||
|
Math.sqrt((10000000e18 - quadraticThreshold) * 1e18) +
|
||||||
|
Math.sqrt((2000000e18 - quadraticThreshold) * 1e18) +
|
||||||
|
Math.sqrt((3000000e18 - quadraticThreshold) * 1e18);
|
||||||
|
uint256 quorum = (totalSupplyQuadraticVotes * 10) / 100;
|
||||||
|
assertEq(governor.quorum(2), quorum);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToExecuteASuccessfulProposal() public {
|
||||||
|
// Create a proposal
|
||||||
|
address[] memory targets = new address[](1);
|
||||||
|
targets[0] = address(callReceiverMock);
|
||||||
|
|
||||||
|
uint256[] memory values = new uint256[](1);
|
||||||
|
values[0] = 0;
|
||||||
|
|
||||||
|
bytes[] memory calldatas = new bytes[](1);
|
||||||
|
calldatas[0] = abi.encodeWithSignature("mockFunction()");
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
vm.startPrank(account2);
|
||||||
|
uint256 proposalId = governor.propose(targets, values, calldatas, "Proposal description");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to after vote start
|
||||||
|
vm.roll(governor.proposalSnapshot(proposalId) + 1);
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
vm.prank(account2);
|
||||||
|
governor.castVote(proposalId, 1); // Vote "for"
|
||||||
|
vm.stopPrank();
|
||||||
|
vm.prank(account3);
|
||||||
|
governor.castVote(proposalId, 0); // Vote "against"
|
||||||
|
vm.stopPrank();
|
||||||
|
vm.prank(account4);
|
||||||
|
governor.castVote(proposalId, 2); // Vote "abstain"
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Fast forward to vote end
|
||||||
|
vm.roll(governor.proposalDeadline(proposalId) + 1);
|
||||||
|
|
||||||
|
// Get vote results
|
||||||
|
(uint256 votesAgainst, uint256 votesFor, uint256 votesAbstain) = governor.proposalVotes(proposalId);
|
||||||
|
assertEq(votesFor, (quadraticThreshold + Math.sqrt((10000000e18 - quadraticThreshold) * 1e18)));
|
||||||
|
assertEq(votesAgainst, quadraticThreshold + Math.sqrt((2000000e18 - quadraticThreshold) * 1e18));
|
||||||
|
assertEq(votesAbstain, quadraticThreshold + Math.sqrt((3000000e18 - quadraticThreshold) * 1e18));
|
||||||
|
|
||||||
|
IGovernor.ProposalState state = governor.state(proposalId);
|
||||||
|
assertEq(uint256(state), uint256(IGovernor.ProposalState.Succeeded));
|
||||||
|
|
||||||
|
// Queue proposal
|
||||||
|
governor.queue(targets, values, calldatas, keccak256(bytes("Proposal description")));
|
||||||
|
vm.warp(governor.proposalEta(proposalId) + 1);
|
||||||
|
|
||||||
|
governor.execute(targets, values, calldatas, keccak256("Proposal description"));
|
||||||
|
state = governor.state(proposalId);
|
||||||
|
assertEq(uint256(state), uint256(IGovernor.ProposalState.Executed));
|
||||||
|
}
|
||||||
|
}
|
32
contracts/governance/test/ZeroExVotesMalicious.sol
Normal file
32
contracts/governance/test/ZeroExVotesMalicious.sol
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// 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 "../src/ZeroExVotes.sol";
|
||||||
|
|
||||||
|
contract ZeroExVotesMalicious is ZeroExVotes {
|
||||||
|
constructor(address _token, uint256 _quadraticThreshold) ZeroExVotes(_token, _quadraticThreshold) {}
|
||||||
|
|
||||||
|
function writeCheckpointTotalSupplyBurn(
|
||||||
|
uint256 amount,
|
||||||
|
uint256 accountBalance
|
||||||
|
) public virtual override onlyToken returns (bool) {
|
||||||
|
revert("I am evil");
|
||||||
|
}
|
||||||
|
}
|
215
contracts/governance/test/ZeroExVotesMigration.sol
Normal file
215
contracts/governance/test/ZeroExVotesMigration.sol
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
// 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 {ZeroExVotes} from "../src/ZeroExVotes.sol";
|
||||||
|
import {SafeCast} from "@openzeppelin/utils/math/SafeCast.sol";
|
||||||
|
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
||||||
|
import {CubeRoot} from "./CubeRoot.sol";
|
||||||
|
|
||||||
|
contract ZeroExVotesMigration is ZeroExVotes {
|
||||||
|
uint32 public migrationBlock;
|
||||||
|
|
||||||
|
constructor(address _token, uint256 _quadraticThreshold) ZeroExVotes(_token, _quadraticThreshold) {}
|
||||||
|
|
||||||
|
function initialize() public virtual override onlyProxy reinitializer(2) {
|
||||||
|
migrationBlock = uint32(block.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CheckpointMigration {
|
||||||
|
uint32 fromBlock;
|
||||||
|
uint96 votes;
|
||||||
|
uint96 quadraticVotes;
|
||||||
|
uint32 migratedVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _toMigration(Checkpoint storage ckpt) internal pure returns (CheckpointMigration storage result) {
|
||||||
|
assembly {
|
||||||
|
result.slot := ckpt.slot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _toMigration(Checkpoint[] storage ckpt) internal pure returns (CheckpointMigration[] storage result) {
|
||||||
|
assembly {
|
||||||
|
result.slot := ckpt.slot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMigratedVotes(address account) public view returns (uint256) {
|
||||||
|
uint256 pos = _checkpoints[account].length;
|
||||||
|
if (pos == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Checkpoint storage ckpt = _unsafeAccess(_checkpoints[account], pos - 1);
|
||||||
|
if (ckpt.fromBlock <= migrationBlock) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return _toMigration(ckpt).migratedVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPastMigratedVotes(address account, uint256 blockNumber) public view returns (uint256) {
|
||||||
|
require(blockNumber < block.number, "ZeroExVotesMigration: block not yet mined");
|
||||||
|
if (blockNumber <= migrationBlock) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Checkpoint storage checkpoint = _checkpointsLookupStorage(_checkpoints[account], blockNumber);
|
||||||
|
if (checkpoint.fromBlock <= migrationBlock) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return _toMigration(checkpoint).migratedVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _checkpointsLookupStorage(
|
||||||
|
Checkpoint[] storage ckpts,
|
||||||
|
uint256 blockNumber
|
||||||
|
) internal view returns (Checkpoint storage result) {
|
||||||
|
// We run a binary search to look for the earliest checkpoint taken after `blockNumber`.
|
||||||
|
//
|
||||||
|
// Initially we check if the block is recent to narrow the search range.
|
||||||
|
// During the loop, the index of the wanted checkpoint remains in the range [low-1, high).
|
||||||
|
// With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the
|
||||||
|
// invariant.
|
||||||
|
// - If the middle checkpoint is after `blockNumber`, we look in [low, mid)
|
||||||
|
// - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high)
|
||||||
|
// Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not
|
||||||
|
// out of bounds (in which case we're looking too far in the past and the result is 0).
|
||||||
|
// Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is
|
||||||
|
// past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out
|
||||||
|
// the same.
|
||||||
|
uint256 length = ckpts.length;
|
||||||
|
|
||||||
|
uint256 low = 0;
|
||||||
|
uint256 high = length;
|
||||||
|
|
||||||
|
if (length > 5) {
|
||||||
|
uint256 mid = length - Math.sqrt(length);
|
||||||
|
if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) {
|
||||||
|
high = mid;
|
||||||
|
} else {
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (low < high) {
|
||||||
|
uint256 mid = Math.average(low, high);
|
||||||
|
if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) {
|
||||||
|
high = mid;
|
||||||
|
} else {
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaving here for posterity this is the original OZ implementation which we've replaced
|
||||||
|
// return high == 0 ? 0 : _unsafeAccess(ckpts, high - 1).votes;
|
||||||
|
// Checkpoint memory checkpoint = high == 0 ? Checkpoint(0, 0, 0) : _unsafeAccess(ckpts, high - 1);
|
||||||
|
// return checkpoint;
|
||||||
|
// TODO: bad. very bad. only works on accident
|
||||||
|
if (high > 0) {
|
||||||
|
result = _unsafeAccess(ckpts, high - 1);
|
||||||
|
} else {
|
||||||
|
// suppress compiler warning, which really shouldn't be suppressed
|
||||||
|
assembly {
|
||||||
|
result.slot := 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we're not handling totalSupply
|
||||||
|
|
||||||
|
// TODO: need to return the migrated weight
|
||||||
|
function _writeCheckpoint(
|
||||||
|
Checkpoint[] storage ckpts,
|
||||||
|
function(uint256, uint256) view returns (uint256) op,
|
||||||
|
uint256 userBalance,
|
||||||
|
uint96 balanceLastUpdated,
|
||||||
|
uint256 delta
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
virtual
|
||||||
|
override
|
||||||
|
returns (uint256 oldWeight, uint256 newWeight, uint256 oldQuadraticWeight, uint256 newQuadraticWeight)
|
||||||
|
{
|
||||||
|
uint256 pos = ckpts.length;
|
||||||
|
|
||||||
|
CheckpointMigration memory oldCkpt = pos == 0
|
||||||
|
? CheckpointMigration(0, 0, 0, 0)
|
||||||
|
: _toMigration(_unsafeAccess(ckpts, pos - 1));
|
||||||
|
|
||||||
|
oldWeight = oldCkpt.votes;
|
||||||
|
newWeight = op(oldWeight, delta);
|
||||||
|
|
||||||
|
oldQuadraticWeight = oldCkpt.quadraticVotes;
|
||||||
|
|
||||||
|
if (pos > 0) {
|
||||||
|
deductOldWeightFromCheckpoint(oldCkpt, userBalance, balanceLastUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if wallet > threshold, calculate quadratic power over the treshold only, below threshold is linear
|
||||||
|
uint256 newBalance = op(userBalance, delta);
|
||||||
|
uint256 newQuadraticBalance = newBalance <= quadraticThreshold
|
||||||
|
? newBalance
|
||||||
|
: quadraticThreshold + Math.sqrt((newBalance - quadraticThreshold) * 1e18);
|
||||||
|
newQuadraticWeight = oldCkpt.quadraticVotes + newQuadraticBalance;
|
||||||
|
uint256 newMigratedWeight = oldCkpt.migratedVotes + CubeRoot.cbrt(newBalance);
|
||||||
|
|
||||||
|
if (pos > 0 && oldCkpt.fromBlock == block.number) {
|
||||||
|
addCheckpoint(ckpts, pos, newWeight, newQuadraticWeight, newMigratedWeight);
|
||||||
|
} else {
|
||||||
|
_toMigration(ckpts).push(
|
||||||
|
CheckpointMigration({
|
||||||
|
fromBlock: SafeCast.toUint32(block.number),
|
||||||
|
votes: SafeCast.toUint96(newWeight),
|
||||||
|
quadraticVotes: SafeCast.toUint96(newQuadraticWeight),
|
||||||
|
migratedVotes: SafeCast.toUint32(newMigratedWeight)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deductOldWeightFromCheckpoint(
|
||||||
|
CheckpointMigration memory oldCkpt,
|
||||||
|
uint256 userBalance,
|
||||||
|
uint96 balanceLastUpdated
|
||||||
|
) internal {
|
||||||
|
// Remove the entire sqrt userBalance from quadratic voting power.
|
||||||
|
// Note that `userBalance` is value _after_ transfer.
|
||||||
|
uint256 oldQuadraticVotingPower = userBalance <= quadraticThreshold
|
||||||
|
? userBalance
|
||||||
|
: quadraticThreshold + Math.sqrt((userBalance - quadraticThreshold) * 1e18);
|
||||||
|
oldCkpt.quadraticVotes -= SafeCast.toUint96(oldQuadraticVotingPower);
|
||||||
|
|
||||||
|
if (balanceLastUpdated > migrationBlock) {
|
||||||
|
oldCkpt.migratedVotes -= SafeCast.toUint32(CubeRoot.cbrt(userBalance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCheckpoint(
|
||||||
|
Checkpoint[] storage ckpts,
|
||||||
|
uint256 pos,
|
||||||
|
uint256 newWeight,
|
||||||
|
uint256 newQuadraticWeight,
|
||||||
|
uint256 newMigratedWeight
|
||||||
|
) internal {
|
||||||
|
CheckpointMigration storage chpt = _toMigration(_unsafeAccess(ckpts, pos - 1));
|
||||||
|
chpt.votes = SafeCast.toUint96(newWeight);
|
||||||
|
chpt.quadraticVotes = SafeCast.toUint96(newQuadraticWeight);
|
||||||
|
chpt.migratedVotes = SafeCast.toUint32(newMigratedWeight);
|
||||||
|
}
|
||||||
|
}
|
433
contracts/governance/test/ZeroExVotesTest.t.sol
Normal file
433
contracts/governance/test/ZeroExVotesTest.t.sol
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
// 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 "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
import "./BaseTest.t.sol";
|
||||||
|
import "./ZeroExVotesMalicious.sol";
|
||||||
|
import "./ZeroExVotesMigration.sol";
|
||||||
|
import "../src/ZRXWrappedToken.sol";
|
||||||
|
|
||||||
|
contract ZeroExVotesTest is BaseTest {
|
||||||
|
IERC20 private token;
|
||||||
|
ZRXWrappedToken private wToken;
|
||||||
|
ZeroExVotes private votes;
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
(token, wToken, votes) = setupZRXWrappedToken();
|
||||||
|
vm.startPrank(account1);
|
||||||
|
token.transfer(account2, 1700000e18);
|
||||||
|
token.transfer(account3, 1600000e18);
|
||||||
|
token.transfer(account4, 1000000e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldCorrectlyInitialiseToken() public {
|
||||||
|
assertEq(votes.token(), address(wToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldNotBeAbleToReinitialise() public {
|
||||||
|
vm.expectRevert("Initializable: contract is already initialized");
|
||||||
|
votes.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToMigrate() public {
|
||||||
|
vm.roll(block.number + 1);
|
||||||
|
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 100e18);
|
||||||
|
wToken.depositFor(account2, 100e18);
|
||||||
|
wToken.delegate(account3);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 200e18);
|
||||||
|
wToken.depositFor(account3, 200e18);
|
||||||
|
wToken.delegate(account3);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.getVotes(account3), 300e18);
|
||||||
|
assertEq(votes.getQuadraticVotes(account3), 300e18);
|
||||||
|
|
||||||
|
vm.roll(block.number + 1);
|
||||||
|
|
||||||
|
ZeroExVotesMigration newImpl = new ZeroExVotesMigration(address(wToken), quadraticThreshold);
|
||||||
|
assertFalse(
|
||||||
|
address(
|
||||||
|
uint160(
|
||||||
|
uint256(vm.load(address(votes), 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc))
|
||||||
|
)
|
||||||
|
) == address(newImpl)
|
||||||
|
);
|
||||||
|
vm.prank(account1);
|
||||||
|
votes.upgradeToAndCall(address(newImpl), abi.encodeWithSignature("initialize()"));
|
||||||
|
assertEq(
|
||||||
|
address(
|
||||||
|
uint160(
|
||||||
|
uint256(vm.load(address(votes), 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
address(newImpl)
|
||||||
|
);
|
||||||
|
|
||||||
|
ZeroExVotesMigration upgradedVotes = ZeroExVotesMigration(address(votes));
|
||||||
|
|
||||||
|
assertEq(upgradedVotes.getVotes(account3), 300e18);
|
||||||
|
assertEq(upgradedVotes.getQuadraticVotes(account3), 300e18);
|
||||||
|
|
||||||
|
vm.roll(block.number + 1);
|
||||||
|
|
||||||
|
vm.prank(account2);
|
||||||
|
wToken.transfer(address(this), 50e18);
|
||||||
|
|
||||||
|
assertEq(upgradedVotes.getVotes(account3), 250e18);
|
||||||
|
assertEq(upgradedVotes.getQuadraticVotes(account3), 250e18);
|
||||||
|
assertEq(upgradedVotes.getMigratedVotes(account3), CubeRoot.cbrt(50e18));
|
||||||
|
|
||||||
|
vm.prank(account3);
|
||||||
|
wToken.transfer(address(this), 100e18);
|
||||||
|
assertEq(upgradedVotes.getVotes(account3), 150e18);
|
||||||
|
assertEq(upgradedVotes.getQuadraticVotes(account3), 150e18);
|
||||||
|
assertEq(upgradedVotes.getMigratedVotes(account3), CubeRoot.cbrt(50e18) + CubeRoot.cbrt(100e18));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldNotBeAbleToStopBurn() public {
|
||||||
|
// wrap some token
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1700000e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
assertEq(token.balanceOf(account2), 0);
|
||||||
|
assertEq(wToken.balanceOf(account2), 1700000e18);
|
||||||
|
|
||||||
|
// malicious upgrade
|
||||||
|
vm.startPrank(account1);
|
||||||
|
IZeroExVotes maliciousImpl = new ZeroExVotesMalicious(votes.token(), votes.quadraticThreshold());
|
||||||
|
votes.upgradeTo(address(maliciousImpl));
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// try to withdraw withdraw
|
||||||
|
vm.prank(account2);
|
||||||
|
wToken.withdrawTo(account2, 1700000e18);
|
||||||
|
assertEq(token.balanceOf(account2), 1700000e18);
|
||||||
|
assertEq(wToken.balanceOf(account2), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToReadCheckpoints() public {
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to account3
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1700000e18);
|
||||||
|
vm.roll(2);
|
||||||
|
wToken.delegate(account3);
|
||||||
|
|
||||||
|
assertEq(votes.numCheckpoints(account3), 1);
|
||||||
|
|
||||||
|
IZeroExVotes.Checkpoint memory checkpoint = votes.checkpoints(account3, 0);
|
||||||
|
assertEq(checkpoint.fromBlock, 2);
|
||||||
|
assertEq(checkpoint.votes, 1700000e18);
|
||||||
|
assertEq(checkpoint.quadraticVotes, quadraticThreshold + Math.sqrt((1700000e18 - quadraticThreshold) * 1e18));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToSelfDelegateVotingPower() public {
|
||||||
|
// Check voting power initially is 0
|
||||||
|
assertEq(votes.getVotes(account2), 0);
|
||||||
|
assertEq(votes.getQuadraticVotes(account2), 0);
|
||||||
|
|
||||||
|
// Wrap ZRX and delegate voting power to themselves
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1700000e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
|
||||||
|
// Check voting power
|
||||||
|
assertEq(votes.getVotes(account2), 1700000e18);
|
||||||
|
assertEq(
|
||||||
|
votes.getQuadraticVotes(account2),
|
||||||
|
quadraticThreshold + Math.sqrt((1700000e18 - quadraticThreshold) * 1e18)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToDelegateVotingPowerToAnotherAccount() public {
|
||||||
|
// Check voting power initially is 0
|
||||||
|
assertEq(votes.getVotes(account3), 0);
|
||||||
|
assertEq(votes.getQuadraticVotes(account3), 0);
|
||||||
|
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to account3
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1700000e18);
|
||||||
|
wToken.delegate(account3);
|
||||||
|
|
||||||
|
// Check voting power
|
||||||
|
assertEq(votes.getVotes(account3), 1700000e18);
|
||||||
|
assertEq(
|
||||||
|
votes.getQuadraticVotes(account3),
|
||||||
|
quadraticThreshold + Math.sqrt((1700000e18 - quadraticThreshold) * 1e18)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldBeAbleToDelegateVotingPowerToAnotherAccountWithSignature() public {
|
||||||
|
uint256 nonce = 0;
|
||||||
|
uint256 expiry = type(uint256).max;
|
||||||
|
uint256 privateKey = 2;
|
||||||
|
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to account3
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1700000e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(wToken.delegates(account2), address(0));
|
||||||
|
assertEq(votes.getVotes(account3), 0);
|
||||||
|
assertEq(votes.getQuadraticVotes(account3), 0);
|
||||||
|
|
||||||
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
||||||
|
privateKey,
|
||||||
|
keccak256(
|
||||||
|
abi.encodePacked(
|
||||||
|
"\x19\x01",
|
||||||
|
wToken.DOMAIN_SEPARATOR(),
|
||||||
|
keccak256(abi.encode(DELEGATION_TYPEHASH, account3, nonce, expiry))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
wToken.delegateBySig(account3, nonce, expiry, v, r, s);
|
||||||
|
|
||||||
|
assertEq(wToken.delegates(account2), account3);
|
||||||
|
assertEq(votes.getVotes(account3), 1700000e18);
|
||||||
|
assertEq(
|
||||||
|
votes.getQuadraticVotes(account3),
|
||||||
|
quadraticThreshold + Math.sqrt((1700000e18 - quadraticThreshold) * 1e18)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testShouldNotBeAbleToDelegateWithSignatureAfterExpiry() public {
|
||||||
|
uint256 nonce = 0;
|
||||||
|
uint256 expiry = block.timestamp - 1;
|
||||||
|
uint256 privateKey = 2;
|
||||||
|
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to account3
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1700000e18);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
||||||
|
privateKey,
|
||||||
|
keccak256(
|
||||||
|
abi.encodePacked(
|
||||||
|
"\x19\x01",
|
||||||
|
wToken.DOMAIN_SEPARATOR(),
|
||||||
|
keccak256(abi.encode(DELEGATION_TYPEHASH, account3, nonce, expiry))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
vm.expectRevert("ERC20Votes: signature expired");
|
||||||
|
wToken.delegateBySig(account3, nonce, expiry, v, r, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMultipleAccountsShouldBeAbleToDelegateVotingPowerToAccountWithNoTokensOnSameBlock() public {
|
||||||
|
// Check account4 voting power initially is 0
|
||||||
|
assertEq(votes.getVotes(account4), 0);
|
||||||
|
assertEq(votes.getQuadraticVotes(account4), 0);
|
||||||
|
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to account4
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1700000e18);
|
||||||
|
wToken.delegate(account4);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Account 3 also wraps ZRX and delegates voting power to account4
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 1600000e18);
|
||||||
|
wToken.depositFor(account3, 1600000e18);
|
||||||
|
wToken.delegate(account4);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Check voting power
|
||||||
|
assertEq(votes.getVotes(account4), 3300000e18);
|
||||||
|
assertEq(
|
||||||
|
votes.getQuadraticVotes(account4),
|
||||||
|
quadraticThreshold *
|
||||||
|
2 +
|
||||||
|
Math.sqrt((1700000e18 - quadraticThreshold) * 1e18) +
|
||||||
|
Math.sqrt((1600000e18 - quadraticThreshold) * 1e18)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMultipleAccountsShouldBeAbleToDelegateVotingPowerToAccountWithNoTokensOnDifferentBlock() public {
|
||||||
|
// Check account4 voting power initially is 0
|
||||||
|
assertEq(votes.getVotes(account4), 0);
|
||||||
|
assertEq(votes.getQuadraticVotes(account4), 0);
|
||||||
|
|
||||||
|
// Account 2 wraps ZRX and delegates voting power to account4
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1700000e18);
|
||||||
|
wToken.delegate(account4);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Different block height
|
||||||
|
vm.roll(2);
|
||||||
|
// Account 3 also wraps ZRX and delegates voting power to account4
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 1600000e18);
|
||||||
|
wToken.depositFor(account3, 1600000e18);
|
||||||
|
wToken.delegate(account4);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Check voting power
|
||||||
|
assertEq(votes.getVotes(account4), 3300000e18);
|
||||||
|
assertEq(
|
||||||
|
votes.getQuadraticVotes(account4),
|
||||||
|
quadraticThreshold *
|
||||||
|
2 +
|
||||||
|
Math.sqrt((1700000e18 - quadraticThreshold) * 1e18) +
|
||||||
|
Math.sqrt((1600000e18 - quadraticThreshold) * 1e18)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testComplexDelegationScenario() public {
|
||||||
|
// Account 2 wraps ZRX and delegates to itself
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 1700000e18);
|
||||||
|
wToken.depositFor(account2, 1000000e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.getVotes(account2), 1000000e18);
|
||||||
|
assertEq(votes.getQuadraticVotes(account2), 1000000e18);
|
||||||
|
|
||||||
|
// Account 3 wraps ZRX and delegates to account4
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 500000e18);
|
||||||
|
wToken.depositFor(account3, 500000e18);
|
||||||
|
wToken.delegate(account4);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.getVotes(account4), 500000e18);
|
||||||
|
assertEq(votes.getQuadraticVotes(account4), 500000e18);
|
||||||
|
|
||||||
|
// Voting power distribution now is as follows
|
||||||
|
// account2 -> account2 1000000e18 | 1000000e18
|
||||||
|
// account3 -> account4 500000e18 | 500000e18
|
||||||
|
|
||||||
|
// Account 2 deposits the remaining 700000e18 and delegates to account3
|
||||||
|
vm.startPrank(account2);
|
||||||
|
wToken.depositFor(account2, 700000e18);
|
||||||
|
wToken.delegate(account3);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.getVotes(account3), 1700000e18);
|
||||||
|
assertEq(
|
||||||
|
votes.getQuadraticVotes(account3),
|
||||||
|
quadraticThreshold + Math.sqrt((1700000e18 - quadraticThreshold) * 1e18)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Voting power distribution now is as follows
|
||||||
|
// account2 -> account3 1700000e18 | 1000000e18 + Math.sqrt((1700000e18 - 1000000e18) * 1e18)
|
||||||
|
// account3 -> account4 500000e18 | 500000e18
|
||||||
|
|
||||||
|
// Account 3 delegates to itself
|
||||||
|
vm.startPrank(account3);
|
||||||
|
wToken.delegate(account3);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.getVotes(account3), 2200000e18);
|
||||||
|
assertEq(
|
||||||
|
votes.getQuadraticVotes(account3),
|
||||||
|
quadraticThreshold + Math.sqrt((1700000e18 - quadraticThreshold) * 1e18) + 500000e18
|
||||||
|
);
|
||||||
|
|
||||||
|
// Voting power distribution now is as follows
|
||||||
|
// account2, account3 -> account3 2200000e18 | 1000000e18 + Math.sqrt((2200000e18-1000000e18) *1e18) + 500000e18
|
||||||
|
|
||||||
|
// Check account2 and account4 no longer have voting power
|
||||||
|
assertEq(votes.getVotes(account2), 0);
|
||||||
|
assertEq(votes.getQuadraticVotes(account2), 0);
|
||||||
|
assertEq(votes.getVotes(account4), 0);
|
||||||
|
assertEq(votes.getQuadraticVotes(account4), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCheckpointIsCorrectlyUpdatedOnTheSameBlock() public {
|
||||||
|
// Account 2 wraps ZRX and delegates 20e18 to itself
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 20e18);
|
||||||
|
wToken.depositFor(account2, 20e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.numCheckpoints(account2), 1);
|
||||||
|
IZeroExVotes.Checkpoint memory checkpoint1Account2 = votes.checkpoints(account2, 0);
|
||||||
|
assertEq(checkpoint1Account2.fromBlock, 1);
|
||||||
|
assertEq(checkpoint1Account2.votes, 20e18);
|
||||||
|
assertEq(checkpoint1Account2.quadraticVotes, 20e18);
|
||||||
|
|
||||||
|
// Account 3 wraps ZRX and delegates 10e18 to account2
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 10e18);
|
||||||
|
wToken.depositFor(account3, 10e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.numCheckpoints(account2), 1);
|
||||||
|
checkpoint1Account2 = votes.checkpoints(account2, 0);
|
||||||
|
assertEq(checkpoint1Account2.fromBlock, 1);
|
||||||
|
assertEq(checkpoint1Account2.votes, 30e18);
|
||||||
|
assertEq(checkpoint1Account2.quadraticVotes, 20e18 + 10e18);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCheckpointIsCorrectlyUpdatedOnDifferentBlocks() public {
|
||||||
|
// Account 2 wraps ZRX and delegates 20e18 to itself
|
||||||
|
vm.startPrank(account2);
|
||||||
|
token.approve(address(wToken), 20e18);
|
||||||
|
wToken.depositFor(account2, 20e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.numCheckpoints(account2), 1);
|
||||||
|
IZeroExVotes.Checkpoint memory checkpoint1Account2 = votes.checkpoints(account2, 0);
|
||||||
|
assertEq(checkpoint1Account2.fromBlock, 1);
|
||||||
|
assertEq(checkpoint1Account2.votes, 20e18);
|
||||||
|
assertEq(checkpoint1Account2.quadraticVotes, 20e18);
|
||||||
|
|
||||||
|
vm.roll(2);
|
||||||
|
// Account 3 wraps ZRX and delegates 10e18 to account2
|
||||||
|
vm.startPrank(account3);
|
||||||
|
token.approve(address(wToken), 10e18);
|
||||||
|
wToken.depositFor(account3, 10e18);
|
||||||
|
wToken.delegate(account2);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
assertEq(votes.numCheckpoints(account2), 2);
|
||||||
|
IZeroExVotes.Checkpoint memory checkpoint2Account2 = votes.checkpoints(account2, 1);
|
||||||
|
assertEq(checkpoint2Account2.fromBlock, 2);
|
||||||
|
assertEq(checkpoint2Account2.votes, 30e18);
|
||||||
|
assertEq(checkpoint2Account2.quadraticVotes, 20e18 + 10e18);
|
||||||
|
|
||||||
|
// Check the old checkpoint hasn't changed
|
||||||
|
checkpoint1Account2 = votes.checkpoints(account2, 0);
|
||||||
|
assertEq(checkpoint1Account2.fromBlock, 1);
|
||||||
|
assertEq(checkpoint1Account2.votes, 20e18);
|
||||||
|
assertEq(checkpoint1Account2.quadraticVotes, 20e18);
|
||||||
|
}
|
||||||
|
}
|
@ -40,7 +40,7 @@
|
|||||||
"test:all": "wsrun --fast-exit --serial --exclude-missing -p $PKG -c test",
|
"test:all": "wsrun --fast-exit --serial --exclude-missing -p $PKG -c test",
|
||||||
"test:contracts": "wsrun --serial -p $(echo ${npm_package_config_contractsPackages} ${npm_package_config_ignoreTestsForPackages} | tr ' ' '\n' | sort | uniq -u | tr '\n' ' ') --fast-exit --exclude-missing -c test",
|
"test:contracts": "wsrun --serial -p $(echo ${npm_package_config_contractsPackages} ${npm_package_config_ignoreTestsForPackages} | tr ' ' '\n' | sort | uniq -u | tr '\n' ' ') --fast-exit --exclude-missing -c test",
|
||||||
"test:contracts:all": "wsrun --serial -p ${npm_package_config_contractsPackages} --fast-exit --exclude-missing -c test",
|
"test:contracts:all": "wsrun --serial -p ${npm_package_config_contractsPackages} --fast-exit --exclude-missing -c test",
|
||||||
"test:links": "yarn check-md --ignore **/forge-std/README.md,**/lib/openzeppelin-contracts,**/node_modules",
|
"test:links": "yarn check-md --ignore **/forge-std/README.md,**/lib/openzeppelin-contracts,**/node_modules,**/lib",
|
||||||
"generate_doc": "node ./node_modules/@0x/monorepo-scripts/lib/doc_generate.js --config ./doc-gen-config.json",
|
"generate_doc": "node ./node_modules/@0x/monorepo-scripts/lib/doc_generate.js --config ./doc-gen-config.json",
|
||||||
"upload_md_docs": "aws s3 rm --recursive s3://docs-markdown; wsrun --exclude-missing -c s3:sync_md_docs",
|
"upload_md_docs": "aws s3 rm --recursive s3://docs-markdown; wsrun --exclude-missing -c s3:sync_md_docs",
|
||||||
"diff_md_docs:ci": "wsrun --exclude-missing -c diff_docs",
|
"diff_md_docs:ci": "wsrun --exclude-missing -c diff_docs",
|
||||||
@ -93,5 +93,7 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/bignumber.js": "^9.0.2"
|
"**/bignumber.js": "^9.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {
|
||||||
|
"@openzeppelin/contracts": "^4.8.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2775,6 +2775,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@octokit/openapi-types" "^12.11.0"
|
"@octokit/openapi-types" "^12.11.0"
|
||||||
|
|
||||||
|
"@openzeppelin/contracts@^4.8.1":
|
||||||
|
version "4.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.1.tgz#709cfc4bbb3ca9f4460d60101f15dac6b7a2d5e4"
|
||||||
|
integrity sha512-xQ6eUZl+RDyb/FiZe1h+U7qr/f4p/SrTSQcTPH2bjur3C5DbuW/zFgCU/b1P/xcIaEqJep+9ju4xDRi3rmChdQ==
|
||||||
|
|
||||||
"@sindresorhus/slugify@^0.8.0":
|
"@sindresorhus/slugify@^0.8.0":
|
||||||
version "0.8.0"
|
version "0.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sindresorhus/slugify/-/slugify-0.8.0.tgz#5550b7fa064f3a8a82651463ad635378054c72d0"
|
resolved "https://registry.yarnpkg.com/@sindresorhus/slugify/-/slugify-0.8.0.tgz#5550b7fa064f3a8a82651463ad635378054c72d0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user