├── .buildkite └── pipeline.yaml ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── .solhint.json ├── CHANGELOG.md ├── DCO ├── DEVELOPING.md ├── LICENSE ├── README.md ├── ci ├── base-image │ └── Dockerfile └── run ├── contracts ├── AttestationRegistry.sol ├── Claims.sol ├── ClaimsV2.sol ├── DummyUpgradable.sol ├── ErrorReporter.sol ├── Governance │ ├── Governor.sol │ ├── RadicleToken.sol │ ├── Timelock.sol │ └── VestingToken.sol ├── Pool.sol ├── PoolTest.sol ├── Proxy.sol ├── ProxyAdminStorage.sol ├── Registrar.sol ├── TestDai.sol ├── deploy │ └── phase0.sol └── libraries │ ├── ProxyDeltas.sol │ ├── ReceiverWeights.sol │ └── SafeMath.sol ├── docs ├── how_the_pool_works.md ├── how_the_pool_works.odg ├── how_the_pool_works_1.png ├── how_the_pool_works_2.png ├── how_the_pool_works_3.png ├── how_the_pool_works_4.png ├── how_the_pool_works_5.png ├── how_the_pool_works_6.png ├── how_the_pool_works_7.png ├── how_the_pool_works_8.png └── how_the_pool_works_9.png ├── ethregistrar └── build │ └── contracts │ ├── BaseRegistrar.json │ └── BaseRegistrarImplementation.json ├── hardhat.config.ts ├── package.json ├── scripts └── copy-contract-declaration-files.ts ├── src ├── deploy-to-network.ts ├── deploy.ts ├── ens.ts ├── index.ts └── utils.ts ├── test ├── attestations.test.ts ├── governance.test.ts ├── pool.test.ts ├── proxy-deltas.test.ts ├── proxy.test.ts ├── radicle-token.test.ts ├── receiver-weights.test.ts ├── registrar.test.ts ├── support.ts └── vesting-token.test.ts ├── tsconfig.eslint.json ├── tsconfig.json └── yarn.lock /.buildkite/pipeline.yaml: -------------------------------------------------------------------------------- 1 | .test: &test 2 | label: "Test" 3 | command: "ci/run" 4 | env: 5 | SHARED_MASTER_CACHE: true 6 | DOCKER_IMAGE: gcr.io/opensourcecoin/radicle-registry-eth/ci-base:e0cb6a1dfa2a6fba34128b52294c60451367ee94 7 | DOCKER_FILE: ci/base-image/Dockerfile 8 | agents: 9 | platform: "linux" 10 | production: "true" 11 | artifact_paths: 12 | - "radicle-contracts-*.tgz" 13 | 14 | steps: 15 | - branches: master 16 | concurrency: 1 17 | concurrency_group: master 18 | <<: *test 19 | - branches: "!master" 20 | <<: *test 21 | 22 | notify: 23 | - email: "registry-devs@monadic.xyz" 24 | if: | 25 | build.state == "failed" && build.branch == "master" 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: "./tsconfig.eslint.json", 9 | }, 10 | plugins: ["@typescript-eslint"], 11 | extends: [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 15 | ], 16 | ignorePatterns: ["build/**", "contract-bindings/**", "cache/**"], 17 | rules: { 18 | "@typescript-eslint/explicit-function-return-type": "error", 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /artifacts 2 | /build 3 | /cache 4 | /contract-bindings 5 | /yarn-error.log 6 | /node_modules 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | files: "*.ts", 5 | options: { 6 | printWidth: 100, 7 | }, 8 | }, 9 | { 10 | files: "*.sol", 11 | options: { 12 | printWidth: 100, 13 | tabWidth: 4, 14 | bracketSpacing: false, 15 | explicitTypes: "always", 16 | }, 17 | }, 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.7.5"], 5 | "reason-string": ["warn", {"maxLength": 96}], 6 | "not-rely-on-time": "off", 7 | "func-visibility": ["error", { "ignoreConstructors": true }] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) 6 | with regard to its JavaScript and TypeScript bindings' APIs and the Ethereum ABI. 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | - Burning capability to Radicle Token 12 | - Radicle Link ID claims contract 13 | - Pool events when a direct stream from a sender to a receiver is updated 14 | - Pool events when a sender is updated 15 | - Pool events when a receiver collects funds 16 | 17 | ### Changed 18 | - Replaced vesting contract with one adapted from Melonport 19 | - Switch the funding pool from measuring time in blocks to seconds 20 | 21 | ### Removed 22 | - Rad token 23 | 24 | ## [0.1.0] - 2020-12-01 25 | ### Added 26 | - The initial version of the funding pool contract 27 | - The initial version of the attestations contract 28 | - The initial version of the orgs contract 29 | - The initial version of the RAD and VRAD tokens contracts 30 | 31 | [Unreleased]: https://github.com/radicle-dev/radicle-contracts/compare/v0.1.0...HEAD 32 | [0.1.0]: https://github.com/radicle-dev/radicle-contracts/releases/tag/v0.1.0 33 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | 2 | Developer Certificate of Origin 3 | 4 | By making your contribution, you are (1) making the declaration set 5 | out in the Linux Foundation’s Developer Certificate of Origin 6 | version 1.1 as set out below, in which the “open source licence indicated 7 | in the file” is GPLv3 with the Radicle Linking Exception, and (2) granting the 8 | additional permission referred to in the Radicle Linking Exception to 9 | downstream recipients of current and future versions of radicle-contracts 10 | released by the Radicle Foundation. 11 | 12 | Developer Certificate of Origin 13 | Version 1.1 14 | 15 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 16 | 1 Letterman Drive 17 | Suite D4700 18 | San Francisco, CA, 94129 19 | 20 | Everyone is permitted to copy and distribute verbatim copies of this 21 | license document, but changing it is not allowed. 22 | 23 | Developer's Certificate of Origin 1.1 24 | 25 | By making a contribution to this project, I certify that: 26 | (a) The contribution was created in whole or in part by me and I have the right 27 | to submit it under the open source license indicated in the file; or 28 | 29 | (b) The contribution is based upon previous work that, to the best of my knowledge, 30 | is covered under an appropriate open source license and I have the right under that 31 | license to submit that work with modifications, whether created in whole or in part 32 | by me, under the same open source license (unless I am permitted to submit under a 33 | different license), as indicated in the file; or 34 | 35 | (c) The contribution was provided directly to me by some other person who certified 36 | (a), (b) or (c) and I have not modified it. 37 | 38 | (d) I understand and agree that this project and the contribution are public and that 39 | a record of the contribution (including all personal information I submit with it, 40 | including my sign-off) is maintained indefinitely and may be redistributed consistent 41 | with this project or the open source license(s) involved. 42 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developer Manual 2 | 3 | ## Tasks 4 | 5 | * `yarn run build` Build the contracts, client bindings and compile with 6 | typescript. 7 | * `yarn run test` Rebuild the contract and client bindings and run all tests. 8 | * `yarn run lint` Check with `prettier` and `solhint`. The tasks `lint:solhint` 9 | and `lint:prettier` are also available. 10 | 11 | ## Changelog and versioning 12 | 13 | The project follows [Semantic Versioning] with regard to 14 | its JavaScript and TypeScript bindings' APIs and the Ethereum ABI. 15 | Any changes visible through any of these interfaces must be noted 16 | in the changelog and reflected in the version number when a new release is made. 17 | The changelog is manually updated in every commit that makes a change 18 | and it follows the [Keep a Changelog] convention. 19 | 20 | ### Releasing a new version 21 | 22 | Whenever a new version is released, a separate commit is created. 23 | It contains all the version bumping work, which is: 24 | 25 | - Wrap up the changes for the new version in `CHANGELOG.md` and open a new 26 | `Unreleased` version. 27 | - Bump version in `package.json` 28 | 29 | The version bumping commit is the head of the branch merged into `master`. 30 | The branch must be rebased and mergeable using the fast-forward option. 31 | After the merge is finished, the `master`s head is tagged with 32 | a git tag and a GitHub release. 33 | Both of them are named using the version number with a `v` prefix, 34 | e.g. `v0.0.1`, `v1.0.0`, `v1.2.3` or `v1.0.0-alpha`. 35 | 36 | [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ 37 | [Semantic Versioning]: https://semver.org/spec/v2.0.0.html 38 | 39 | ## Updating CI's base Docker image 40 | 41 | 1. Update Docker's image tag to an unexistent tag 42 | 43 | In `.buildkite/pipeline.yaml` > `.test` > `env` > `DOCKER_IMAGE`, 44 | replace the image tag with a nonexistent tag (e.g. `does_not_exist`). 45 | 46 | Example: 47 | 48 | ``` 49 | DOCKER_IMAGE: gcr.io/opensourcecoin/radicle-registry-eth/ci-base:d78a964e22d65fe45e1dcacdf5538de286e3624e 50 | ``` 51 | to 52 | 53 | ``` 54 | DOCKER_IMAGE: gcr.io/opensourcecoin/radicle-registry-eth/ci-base:does_not_exist 55 | ``` 56 | 57 | Now, commit and push this change. 58 | 59 | 2. Wait for the build agent to build this commit 60 | 61 | **Make sure that this commit is preserved!** 62 | Do not amend, squash, rebase or delete it. 63 | It should be merged unmodified into master. 64 | This way it will be easy to look up the state 65 | of the project used by the build agent. 66 | 67 | **What happens on the build agent:** when no docker image 68 | is found for a given tag, the agent will run the full pipeline 69 | and save the docker image under a tag associated with the current 70 | commit ID.` 71 | 72 | 3. Update the docker image tag with step 1's commit ID 73 | 74 | Example: 75 | ``` 76 | DOCKER_IMAGE: gcr.io/opensourcecoin/radicle-registry-eth/ci-base:does_not_exist 77 | ``` 78 | 79 | to 80 | 81 | ``` 82 | DOCKER_IMAGE: gcr.io/opensourcecoin/radicle-registry-eth/ci-base:e8c699d4827ed893d8dcdab6e72de40732ad5f3c 83 | ``` 84 | 85 | **What happens on the build agent:** when any commit with this change is pushed, 86 | the build agent will find the image under the configured tag. 87 | It will reuse it instead of rebuilding, which saves a lot of time. 88 | 89 | 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radicle Ethereum Contracts 2 | 3 | See [`DEVELOPING.md`](./DEVELOPING.md) for the developer manual. 4 | 5 | See [`how_the_pool_works.md`](./docs/how_the_pool_works.md) for the introduction to 6 | how the funding pool contract works. 7 | 8 | ## Installation 9 | 10 | We provide a tarball of the package through [our 11 | CI](https://buildkite.com/monadic/radicle-contracts). See the “Artifacts” 12 | section of a build. 13 | 14 | ## Deployment 15 | 16 | Run one of the following commands and follow the instructions provided: 17 | 18 | yarn deploy:claims 19 | yarn deploy:erc20FundingPool 20 | yarn deploy:phase0 21 | yarn deploy:playground 22 | yarn deploy:testEns 23 | yarn deploy:vestingTokens 24 | 25 | ### Contracts deployed on Mainnet 26 | 27 | - `claims`: `0x4a7DFda4F2e9F062965cC87f775841fB58AEA83e` at height 12613127 28 | - `RAD token`: `0x31c8EAcBFFdD875c74b94b077895Bd78CF1E64A3` at height 11863739 29 | 30 | ### Contracts deployed on Goerli 31 | 32 | - `phase0`: `0x4a7DFda4F2e9F062965cC87f775841fB58AEA83e` at height 7751624 33 | - `RAD token`: `0x3EE94D192397aAFAe438C9803825eb1Aa4402e09` at height 7751624 34 | - `timelock`: `0x5815Ec3BaA7392c4b52A94F4Bda6B0aA09563428` at height 7751624 35 | - `governor`: `0xc1DB01b8a3cD5ef52f7a83798Ee21EdC7A7e9668` at height 7751624 36 | - `ENS registrar`: `0xD88303A92577bFDF5A82FddeF342F3A27A972405` at height 7757112 37 | - controller -> radicle-goerli.eth 38 | - `vesting`: 39 | - `0x9c882463B02221b0558112dec05F60D5B3D99b6a` 40 | - `0xAADcbc69f955523B0ff0A271229961E950538EbE` 41 | - `0x27BCA0692e13C122E6Fc105b3974B5df7246D464` 42 | - `0x13b2Fc1f601Fb72b86BFAB59090f22bB6E73005A` 43 | -------------------------------------------------------------------------------- /ci/base-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.18.3-buster 2 | -------------------------------------------------------------------------------- /ci/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | export YARN_CACHE_FOLDER=/cache/yarn 6 | 7 | # The home folder is not writable on CI 8 | export HOME=/tmp/home 9 | mkdir -p $HOME 10 | 11 | echo "--- yarn install" 12 | yarn install --frozen-lockfile 13 | 14 | echo "--- lint" 15 | yarn run lint 16 | 17 | echo "--- build && pack" 18 | yarn pack 19 | 20 | echo "--- test" 21 | yarn run test 22 | 23 | if [[ -n "${BUILDKITE_TAG:-}" ]] 24 | then 25 | declare -r artifact_scope="${BUILDKITE_TAG}" 26 | elif [[ "${BUILDKITE_BRANCH}" == "master" ]] 27 | then 28 | declare -r artifact_scope="master/${BUILDKITE_COMMIT}" 29 | else 30 | declare -r artifact_scope="$BUILDKITE_JOB_ID" 31 | fi 32 | declare -r artifact_prefix="https://builds.radicle.xyz/radicle-contracts/${artifact_scope}" 33 | 34 | { 35 | echo "Artifacts" 36 | for path in radicle-contracts-v*.tgz; do 37 | url="${artifact_prefix}/${path}" 38 | echo "* [\`${path}\`](${url})" 39 | done 40 | } | buildkite-agent annotate --context node-binary --style success 41 | -------------------------------------------------------------------------------- /contracts/AttestationRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | contract AttestationRegistry { 5 | struct Attestation { 6 | bytes32 id; // Holds up to a 256-bit hash 7 | bytes32 revision; // Holds up to a 256-bit hash 8 | bytes32 publicKey; // Holds an ed25519 key 9 | bytes1[64] signature; // Holds an ed25519 signature 10 | } 11 | 12 | /// The set of recorded attestations. Maps between an Ethereum address 13 | /// and an attestation. 14 | mapping(address => Attestation) public attestations; 15 | 16 | /// Create a new attestation. Overwrites any existing attestation by 17 | /// the sender. 18 | function attest( 19 | bytes32 id, 20 | bytes32 revision, 21 | bytes32 publicKey, 22 | bytes1[64] calldata signature 23 | ) public { 24 | attestations[msg.sender].id = id; 25 | attestations[msg.sender].revision = revision; 26 | attestations[msg.sender].publicKey = publicKey; 27 | attestations[msg.sender].signature = signature; 28 | } 29 | 30 | /// Revoke any attestation made by the sender. 31 | function revokeAttestation() public { 32 | delete attestations[msg.sender]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/Claims.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | // @notice Used to create Radicle Link identity claims 5 | contract Claims { 6 | /// @notice Emitted on every Radicle Link identity claim 7 | /// @param addr The account address which made the claim 8 | event Claimed(address indexed addr); 9 | 10 | /// @notice Creates a new claim of a Radicle Link identity. 11 | /// Every new claim invalidates previous ones made with the same account. 12 | /// The claims have no expiration date and don't need to be renewed. 13 | /// If either `format` is unsupported or `payload` is malformed as per `format`, 14 | /// the previous claim is revoked, but a new one isn't created. 15 | /// Don't send a malformed transactions on purpose, to properly revoke a claim see `format`. 16 | /// @param format The format of `payload`, currently supported values: 17 | /// - `1` - `payload` is exactly 20 bytes and contains an SHA-1 Radicle Identity root hash 18 | /// - `2` - `payload` is exactly 32 bytes and contains an SHA-256 Radicle Identity root hash 19 | /// To revoke a claim without creating a new one, pass payload `0`, 20 | /// which is guaranteed to not match any existing identity. 21 | /// @param payload The claim payload 22 | function claim(uint256 format, bytes calldata payload) public { 23 | format; 24 | payload; 25 | emit Claimed(msg.sender); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/ClaimsV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | // @notice Used to create Radicle Link identity claims 5 | contract ClaimsV2 { 6 | /// @notice Emitted on every Radicle Link identity claim 7 | /// @param addr The account address which made the claim 8 | /// @param format See `claim` parameters 9 | /// @param payload See `claim` parameters 10 | event Claimed(address indexed addr, uint256 format, bytes payload); 11 | 12 | /// @notice Creates a new claim of a Radicle Link identity. 13 | /// Every new claim invalidates previous ones made with the same account. 14 | /// The claims have no expiration date and don't need to be renewed. 15 | /// If either `format` is unsupported or `payload` is malformed as per `format`, 16 | /// the previous claim is revoked, but a new one isn't created. 17 | /// Don't send a malformed transactions on purpose, to properly revoke a claim see `format`. 18 | /// @param format The format of `payload`, currently supported values: 19 | /// - `1` - `payload` is exactly 20 bytes and contains an SHA-1 Radicle Identity root hash 20 | /// - `2` - `payload` is exactly 32 bytes and contains an SHA-256 Radicle Identity root hash 21 | /// To revoke a claim without creating a new one, pass payload `0`, 22 | /// which is guaranteed to not match any existing identity. 23 | /// @param payload The claim payload 24 | function claim(uint256 format, bytes calldata payload) public { 25 | emit Claimed(msg.sender, format, payload); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/DummyUpgradable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | import "./Proxy.sol"; 5 | import "./ProxyAdminStorage.sol"; 6 | 7 | /// A dummy upgradable contract. 8 | contract DummyUpgradable { 9 | function upgrade(Proxy proxy) public { 10 | require(msg.sender == proxy.admin(), "only the proxy admin can change implementations"); 11 | require(proxy._acceptImplementation() == 0, "change must be authorized"); 12 | } 13 | } 14 | 15 | /// V1. 16 | contract DummyUpgradableV1 is ProxyAdminStorage, DummyUpgradable { 17 | uint256 private constant VERSION = 1; 18 | 19 | function version() public pure returns (uint256) { 20 | return VERSION; 21 | } 22 | } 23 | 24 | /// V2. 25 | contract DummyUpgradableV2 is ProxyAdminStorage, DummyUpgradable { 26 | uint256 private constant VERSION = 2; 27 | 28 | function version() public pure returns (uint256) { 29 | return VERSION; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/ErrorReporter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // Copyright 2020 Compound Labs, Inc. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 2. Redistributions in binary form must reproduce the above copyright notice, 10 | // this list of conditions and the following disclaimer in the documentation 11 | // and/or other materials provided with the distribution. 12 | // 3. Neither the name of the copyright holder nor the names of its 13 | // contributors may be used to endorse or promote products derived from this 14 | // software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | // POSSIBILITY OF SUCH DAMAGE. 27 | 28 | pragma solidity ^0.7.5; 29 | 30 | contract ErrorReporter { 31 | enum Error {NO_ERROR, UNAUTHORIZED} 32 | 33 | enum FailureInfo { 34 | ACCEPT_ADMIN_PENDING_ADMIN_CHECK, 35 | ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK, 36 | SET_PENDING_ADMIN_OWNER_CHECK, 37 | SET_PENDING_IMPLEMENTATION_OWNER_CHECK 38 | } 39 | 40 | /** 41 | * @dev `error` corresponds to enum Error; `info` corresponds to enum FailureInfo, and `detail` is an arbitrary 42 | * contract-specific code that enables us to report opaque error codes from upgradeable contracts. 43 | **/ 44 | event Failure(uint256 error, uint256 info, uint256 detail); 45 | 46 | /** 47 | * @dev use this when reporting a known error from the money market or a non-upgradeable collaborator 48 | */ 49 | function fail(Error err, FailureInfo info) internal returns (uint256) { 50 | emit Failure(uint256(err), uint256(info), 0); 51 | 52 | return uint256(err); 53 | } 54 | 55 | /** 56 | * @dev use this when reporting an opaque error from an upgradeable collaborator contract 57 | */ 58 | function failOpaque( 59 | Error err, 60 | FailureInfo info, 61 | uint256 opaqueError 62 | ) internal returns (uint256) { 63 | emit Failure(uint256(err), uint256(info), opaqueError); 64 | 65 | return uint256(err); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/Governance/Governor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // Copyright 2020 Compound Labs, Inc. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 2. Redistributions in binary form must reproduce the above copyright notice, 10 | // this list of conditions and the following disclaimer in the documentation 11 | // and/or other materials provided with the distribution. 12 | // 3. Neither the name of the copyright holder nor the names of its 13 | // contributors may be used to endorse or promote products derived from this 14 | // software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | // POSSIBILITY OF SUCH DAMAGE. 27 | 28 | pragma solidity ^0.7.5; 29 | pragma experimental ABIEncoderV2; 30 | 31 | contract Governor { 32 | /// @notice The name of this contract 33 | string public constant NAME = "Radicle Governor"; 34 | 35 | /// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed 36 | function quorumVotes() public pure returns (uint256) { 37 | return 4000000e18; 38 | } // 4,000,000 = 4% of Token 39 | 40 | /// @notice The number of votes required in order for a voter to become a proposer 41 | function proposalThreshold() public pure returns (uint256) { 42 | return 1000000e18; 43 | } // 1,000,000 = 1% of Token 44 | 45 | /// @notice The maximum number of actions that can be included in a proposal 46 | function proposalMaxOperations() public pure returns (uint256) { 47 | return 10; 48 | } // 10 actions 49 | 50 | /// @notice The delay before voting on a proposal may take place, once proposed 51 | function votingDelay() public pure returns (uint256) { 52 | return 1; 53 | } // 1 block 54 | 55 | /// @notice The duration of voting on a proposal, in blocks 56 | function votingPeriod() public pure returns (uint256) { 57 | return 17280; 58 | } // ~3 days in blocks (assuming 15s blocks) 59 | 60 | /// @notice The address of the Radicle Protocol Timelock 61 | TimelockInterface public immutable timelock; 62 | 63 | /// @notice The address of the Radicle governance token 64 | TokenInterface public immutable token; 65 | 66 | /// @notice The address of the Governor Guardian 67 | address public guardian; 68 | 69 | /// @notice The total number of proposals 70 | uint256 public proposalCount; 71 | 72 | /// @notice Change proposal 73 | struct Proposal { 74 | // Creator of the proposal 75 | address proposer; 76 | // The timestamp that the proposal will be available for execution, set once the vote succeeds 77 | uint256 eta; 78 | // the ordered list of target addresses for calls to be made 79 | address[] targets; 80 | // The ordered list of values (i.e. msg.value) to be passed to the calls to be made 81 | uint256[] values; 82 | // The ordered list of function signatures to be called 83 | string[] signatures; 84 | // The ordered list of calldata to be passed to each call 85 | bytes[] calldatas; 86 | // The block at which voting begins: holders must delegate their votes prior to this block 87 | uint256 startBlock; 88 | // The block at which voting ends: votes must be cast prior to this block 89 | uint256 endBlock; 90 | // Current number of votes in favor of this proposal 91 | uint256 forVotes; 92 | // Current number of votes in opposition to this proposal 93 | uint256 againstVotes; 94 | // Flag marking whether the proposal has been canceled 95 | bool canceled; 96 | // Flag marking whether the proposal has been executed 97 | bool executed; 98 | // Receipts of ballots for the entire set of voters 99 | mapping(address => Receipt) receipts; 100 | } 101 | 102 | /// @notice Ballot receipt record for a voter 103 | struct Receipt { 104 | // Whether or not a vote has been cast 105 | bool hasVoted; 106 | // Whether or not the voter supports the proposal 107 | bool support; 108 | // The number of votes the voter had, which were cast 109 | uint96 votes; 110 | } 111 | 112 | /// @notice Possible states that a proposal may be in 113 | enum ProposalState {Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed} 114 | 115 | /// @notice The official record of all proposals ever proposed 116 | mapping(uint256 => Proposal) public proposals; 117 | 118 | /// @notice The latest proposal for each proposer 119 | mapping(address => uint256) public latestProposalIds; 120 | 121 | /// @notice The EIP-712 typehash for the contract's domain 122 | bytes32 public constant DOMAIN_TYPEHASH = 123 | keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); 124 | 125 | /// @notice The EIP-712 typehash for the ballot struct used by the contract 126 | bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,bool support)"); 127 | 128 | /// @notice An event emitted when a new proposal is created 129 | event ProposalCreated( 130 | uint256 id, 131 | address proposer, 132 | address[] targets, 133 | uint256[] values, 134 | string[] signatures, 135 | bytes[] calldatas, 136 | uint256 startBlock, 137 | uint256 endBlock, 138 | string description 139 | ); 140 | 141 | /// @notice An event emitted when a vote has been cast on a proposal 142 | event VoteCast(address voter, uint256 proposalId, bool support, uint256 votes); 143 | 144 | /// @notice An event emitted when a proposal has been canceled 145 | event ProposalCanceled(uint256 id); 146 | 147 | /// @notice An event emitted when a proposal has been queued in the Timelock 148 | event ProposalQueued(uint256 id, uint256 eta); 149 | 150 | /// @notice An event emitted when a proposal has been executed in the Timelock 151 | event ProposalExecuted(uint256 id); 152 | 153 | constructor( 154 | address timelock_, 155 | address token_, 156 | address guardian_ 157 | ) { 158 | timelock = TimelockInterface(timelock_); 159 | token = TokenInterface(token_); 160 | guardian = guardian_; 161 | } 162 | 163 | function propose( 164 | address[] memory targets, 165 | uint256[] memory values, 166 | string[] memory signatures, 167 | bytes[] memory calldatas, 168 | string memory description 169 | ) public returns (uint256) { 170 | require( 171 | token.getPriorVotes(msg.sender, sub256(block.number, 1)) >= proposalThreshold(), 172 | "Governor::propose: proposer votes below proposal threshold" 173 | ); 174 | require( 175 | targets.length == values.length && 176 | targets.length == signatures.length && 177 | targets.length == calldatas.length, 178 | "Governor::propose: proposal function information arity mismatch" 179 | ); 180 | require(targets.length != 0, "Governor::propose: must provide actions"); 181 | require(targets.length <= proposalMaxOperations(), "Governor::propose: too many actions"); 182 | 183 | uint256 latestProposalId = latestProposalIds[msg.sender]; 184 | if (latestProposalId != 0) { 185 | ProposalState proposersLatestProposalState = state(latestProposalId); 186 | require( 187 | proposersLatestProposalState != ProposalState.Active, 188 | "Governor::propose: one live proposal per proposer, found an already active proposal" 189 | ); 190 | require( 191 | proposersLatestProposalState != ProposalState.Pending, 192 | "Governor::propose: one live proposal per proposer, found an already pending proposal" 193 | ); 194 | } 195 | 196 | uint256 startBlock = add256(block.number, votingDelay()); 197 | uint256 endBlock = add256(startBlock, votingPeriod()); 198 | 199 | proposalCount++; 200 | Proposal storage newProposal = proposals[proposalCount]; 201 | 202 | uint256 proposalId = proposalCount; 203 | 204 | newProposal.proposer = msg.sender; 205 | newProposal.eta = 0; 206 | newProposal.targets = targets; 207 | newProposal.values = values; 208 | newProposal.signatures = signatures; 209 | newProposal.calldatas = calldatas; 210 | newProposal.startBlock = startBlock; 211 | newProposal.endBlock = endBlock; 212 | newProposal.forVotes = 0; 213 | newProposal.againstVotes = 0; 214 | newProposal.canceled = false; 215 | newProposal.executed = false; 216 | 217 | latestProposalIds[newProposal.proposer] = proposalId; 218 | 219 | emit ProposalCreated( 220 | proposalId, 221 | msg.sender, 222 | targets, 223 | values, 224 | signatures, 225 | calldatas, 226 | startBlock, 227 | endBlock, 228 | description 229 | ); 230 | return proposalId; 231 | } 232 | 233 | function queue(uint256 proposalId) public { 234 | require( 235 | state(proposalId) == ProposalState.Succeeded, 236 | "Governor::queue: proposal can only be queued if it is succeeded" 237 | ); 238 | Proposal storage proposal = proposals[proposalId]; 239 | uint256 eta = add256(block.timestamp, timelock.delay()); 240 | for (uint256 i = 0; i < proposal.targets.length; i++) { 241 | _queueOrRevert( 242 | proposal.targets[i], 243 | proposal.values[i], 244 | proposal.signatures[i], 245 | proposal.calldatas[i], 246 | eta 247 | ); 248 | } 249 | proposal.eta = eta; 250 | emit ProposalQueued(proposalId, eta); 251 | } 252 | 253 | function _queueOrRevert( 254 | address target, 255 | uint256 value, 256 | string memory signature, 257 | bytes memory data, 258 | uint256 eta 259 | ) internal { 260 | require( 261 | !timelock.queuedTransactions( 262 | keccak256(abi.encode(target, value, signature, data, eta)) 263 | ), 264 | "Governor::_queueOrRevert: proposal action already queued at eta" 265 | ); 266 | timelock.queueTransaction(target, value, signature, data, eta); 267 | } 268 | 269 | function execute(uint256 proposalId) public payable { 270 | require( 271 | state(proposalId) == ProposalState.Queued, 272 | "Governor::execute: proposal can only be executed if it is queued" 273 | ); 274 | Proposal storage proposal = proposals[proposalId]; 275 | proposal.executed = true; 276 | for (uint256 i = 0; i < proposal.targets.length; i++) { 277 | timelock.executeTransaction{value: proposal.values[i]}( 278 | proposal.targets[i], 279 | proposal.values[i], 280 | proposal.signatures[i], 281 | proposal.calldatas[i], 282 | proposal.eta 283 | ); 284 | } 285 | emit ProposalExecuted(proposalId); 286 | } 287 | 288 | function cancel(uint256 proposalId) public { 289 | ProposalState _state = state(proposalId); 290 | require( 291 | _state != ProposalState.Executed, 292 | "Governor::cancel: cannot cancel executed proposal" 293 | ); 294 | 295 | Proposal storage proposal = proposals[proposalId]; 296 | require( 297 | msg.sender == guardian || 298 | // Allows anyone to cancel a proposal if the voting power of the 299 | // proposer dropped below the threshold after the proposal was 300 | // submitted. 301 | token.getPriorVotes(proposal.proposer, sub256(block.number, 1)) < 302 | proposalThreshold(), 303 | "Governor::cancel: cannot cancel unless proposer is below threhsold" 304 | ); 305 | 306 | proposal.canceled = true; 307 | for (uint256 i = 0; i < proposal.targets.length; i++) { 308 | timelock.cancelTransaction( 309 | proposal.targets[i], 310 | proposal.values[i], 311 | proposal.signatures[i], 312 | proposal.calldatas[i], 313 | proposal.eta 314 | ); 315 | } 316 | 317 | emit ProposalCanceled(proposalId); 318 | } 319 | 320 | function getActions(uint256 proposalId) 321 | public 322 | view 323 | returns ( 324 | address[] memory targets, 325 | uint256[] memory values, 326 | string[] memory signatures, 327 | bytes[] memory calldatas 328 | ) 329 | { 330 | Proposal storage p = proposals[proposalId]; 331 | return (p.targets, p.values, p.signatures, p.calldatas); 332 | } 333 | 334 | function getReceipt(uint256 proposalId, address voter) public view returns (Receipt memory) { 335 | return proposals[proposalId].receipts[voter]; 336 | } 337 | 338 | function state(uint256 proposalId) public view returns (ProposalState) { 339 | require( 340 | proposalCount >= proposalId && proposalId > 0, 341 | "Governor::state: invalid proposal id" 342 | ); 343 | Proposal storage proposal = proposals[proposalId]; 344 | if (proposal.canceled) { 345 | return ProposalState.Canceled; 346 | } else if (block.number <= proposal.startBlock) { 347 | return ProposalState.Pending; 348 | } else if (block.number <= proposal.endBlock) { 349 | return ProposalState.Active; 350 | } else if ( 351 | proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes() 352 | ) { 353 | return ProposalState.Defeated; 354 | } else if (proposal.eta == 0) { 355 | return ProposalState.Succeeded; 356 | } else if (proposal.executed) { 357 | return ProposalState.Executed; 358 | } else if (block.timestamp >= add256(proposal.eta, timelock.gracePeriod())) { 359 | return ProposalState.Expired; 360 | } else { 361 | return ProposalState.Queued; 362 | } 363 | } 364 | 365 | function castVote(uint256 proposalId, bool support) public { 366 | return _castVote(msg.sender, proposalId, support); 367 | } 368 | 369 | function castVoteBySig( 370 | uint256 proposalId, 371 | bool support, 372 | uint8 v, 373 | bytes32 r, 374 | bytes32 s 375 | ) public { 376 | bytes32 domainSeparator = 377 | keccak256( 378 | abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(NAME)), getChainId(), address(this)) 379 | ); 380 | bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)); 381 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); 382 | address signatory = ecrecover(digest, v, r, s); 383 | require(signatory != address(0), "Governor::castVoteBySig: invalid signature"); 384 | return _castVote(signatory, proposalId, support); 385 | } 386 | 387 | function _castVote( 388 | address voter, 389 | uint256 proposalId, 390 | bool support 391 | ) internal { 392 | require(state(proposalId) == ProposalState.Active, "Governor::_castVote: voting is closed"); 393 | Proposal storage proposal = proposals[proposalId]; 394 | Receipt storage receipt = proposal.receipts[voter]; 395 | require(receipt.hasVoted == false, "Governor::_castVote: voter already voted"); 396 | uint96 votes = token.getPriorVotes(voter, proposal.startBlock); 397 | 398 | if (support) { 399 | proposal.forVotes = add256(proposal.forVotes, votes); 400 | } else { 401 | proposal.againstVotes = add256(proposal.againstVotes, votes); 402 | } 403 | 404 | receipt.hasVoted = true; 405 | receipt.support = support; 406 | receipt.votes = votes; 407 | 408 | emit VoteCast(voter, proposalId, support, votes); 409 | } 410 | 411 | function __acceptAdmin() public { 412 | require(msg.sender == guardian, "Governor::__acceptAdmin: sender must be gov guardian"); 413 | timelock.acceptAdmin(); 414 | } 415 | 416 | function __abdicate() public { 417 | require(msg.sender == guardian, "Governor::__abdicate: sender must be gov guardian"); 418 | guardian = address(0); 419 | } 420 | 421 | function __queueSetTimelockPendingAdmin(address newPendingAdmin, uint256 eta) public { 422 | require( 423 | msg.sender == guardian, 424 | "Governor::__queueSetTimelockPendingAdmin: sender must be gov guardian" 425 | ); 426 | timelock.queueTransaction( 427 | address(timelock), 428 | 0, 429 | "setPendingAdmin(address)", 430 | abi.encode(newPendingAdmin), 431 | eta 432 | ); 433 | } 434 | 435 | function __executeSetTimelockPendingAdmin(address newPendingAdmin, uint256 eta) public { 436 | require( 437 | msg.sender == guardian, 438 | "Governor::__executeSetTimelockPendingAdmin: sender must be gov guardian" 439 | ); 440 | timelock.executeTransaction( 441 | address(timelock), 442 | 0, 443 | "setPendingAdmin(address)", 444 | abi.encode(newPendingAdmin), 445 | eta 446 | ); 447 | } 448 | 449 | function add256(uint256 a, uint256 b) internal pure returns (uint256) { 450 | uint256 c = a + b; 451 | require(c >= a, "addition overflow"); 452 | return c; 453 | } 454 | 455 | function sub256(uint256 a, uint256 b) internal pure returns (uint256) { 456 | require(b <= a, "subtraction underflow"); 457 | return a - b; 458 | } 459 | 460 | function getChainId() internal pure returns (uint256) { 461 | uint256 chainId; 462 | // solhint-disable no-inline-assembly 463 | assembly { 464 | chainId := chainid() 465 | } 466 | return chainId; 467 | } 468 | } 469 | 470 | interface TimelockInterface { 471 | function delay() external view returns (uint256); 472 | 473 | function gracePeriod() external view returns (uint256); 474 | 475 | function acceptAdmin() external; 476 | 477 | function queuedTransactions(bytes32 hash) external view returns (bool); 478 | 479 | function queueTransaction( 480 | address target, 481 | uint256 value, 482 | string calldata signature, 483 | bytes calldata data, 484 | uint256 eta 485 | ) external returns (bytes32); 486 | 487 | function cancelTransaction( 488 | address target, 489 | uint256 value, 490 | string calldata signature, 491 | bytes calldata data, 492 | uint256 eta 493 | ) external; 494 | 495 | function executeTransaction( 496 | address target, 497 | uint256 value, 498 | string calldata signature, 499 | bytes calldata data, 500 | uint256 eta 501 | ) external payable returns (bytes memory); 502 | } 503 | 504 | interface TokenInterface { 505 | function getPriorVotes(address account, uint256 blockNumber) external view returns (uint96); 506 | } 507 | -------------------------------------------------------------------------------- /contracts/Governance/RadicleToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // Copyright 2020 Compound Labs, Inc. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 2. Redistributions in binary form must reproduce the above copyright notice, 10 | // this list of conditions and the following disclaimer in the documentation 11 | // and/or other materials provided with the distribution. 12 | // 3. Neither the name of the copyright holder nor the names of its 13 | // contributors may be used to endorse or promote products derived from this 14 | // software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | // POSSIBILITY OF SUCH DAMAGE. 27 | 28 | pragma solidity ^0.7.5; 29 | pragma experimental ABIEncoderV2; 30 | 31 | contract RadicleToken { 32 | /// @notice EIP-20 token name for this token 33 | string public constant NAME = "Radicle"; 34 | 35 | /// @notice EIP-20 token symbol for this token 36 | string public constant SYMBOL = "RAD"; 37 | 38 | /// @notice EIP-20 token decimals for this token 39 | uint8 public constant DECIMALS = 18; 40 | 41 | /// @notice Total number of tokens in circulation 42 | uint256 public totalSupply = 100000000e18; // 100 million tokens 43 | 44 | // Allowance amounts on behalf of others 45 | mapping(address => mapping(address => uint96)) internal allowances; 46 | 47 | // Official record of token balances for each account 48 | mapping(address => uint96) internal balances; 49 | 50 | /// @notice A record of each accounts delegate 51 | mapping(address => address) public delegates; 52 | 53 | /// @notice A checkpoint for marking number of votes from a given block 54 | struct Checkpoint { 55 | uint32 fromBlock; 56 | uint96 votes; 57 | } 58 | 59 | /// @notice A record of votes checkpoints for each account, by index 60 | mapping(address => mapping(uint32 => Checkpoint)) public checkpoints; 61 | 62 | /// @notice The number of checkpoints for each account 63 | mapping(address => uint32) public numCheckpoints; 64 | 65 | /// @notice The EIP-712 typehash for the contract's domain 66 | bytes32 public constant DOMAIN_TYPEHASH = 67 | keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); 68 | 69 | /// @notice The EIP-712 typehash for the delegation struct used by the contract 70 | bytes32 public constant DELEGATION_TYPEHASH = 71 | keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); 72 | 73 | /// @notice The EIP-712 typehash for EIP-2612 permit 74 | bytes32 public constant PERMIT_TYPEHASH = 75 | keccak256( 76 | "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" 77 | ); 78 | /// @notice A record of states for signing / validating signatures 79 | mapping(address => uint256) public nonces; 80 | 81 | /// @notice An event thats emitted when an account changes its delegate 82 | event DelegateChanged( 83 | address indexed delegator, 84 | address indexed fromDelegate, 85 | address indexed toDelegate 86 | ); 87 | 88 | /// @notice An event thats emitted when a delegate account's vote balance changes 89 | event DelegateVotesChanged( 90 | address indexed delegate, 91 | uint256 previousBalance, 92 | uint256 newBalance 93 | ); 94 | 95 | /// @notice The standard EIP-20 transfer event 96 | event Transfer(address indexed from, address indexed to, uint256 amount); 97 | 98 | /// @notice The standard EIP-20 approval event 99 | event Approval(address indexed owner, address indexed spender, uint256 amount); 100 | 101 | /** 102 | * @notice Construct a new token 103 | * @param account The initial account to grant all the tokens 104 | */ 105 | constructor(address account) { 106 | balances[account] = uint96(totalSupply); 107 | emit Transfer(address(0), account, totalSupply); 108 | } 109 | 110 | /* @notice Token name */ 111 | function name() public pure returns (string memory) { 112 | return NAME; 113 | } 114 | 115 | /* @notice Token symbol */ 116 | function symbol() public pure returns (string memory) { 117 | return SYMBOL; 118 | } 119 | 120 | /* @notice Token decimals */ 121 | function decimals() public pure returns (uint8) { 122 | return DECIMALS; 123 | } 124 | 125 | /* @notice domainSeparator */ 126 | // solhint-disable func-name-mixedcase 127 | function DOMAIN_SEPARATOR() public view returns (bytes32) { 128 | return 129 | keccak256( 130 | abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(NAME)), getChainId(), address(this)) 131 | ); 132 | } 133 | 134 | /** 135 | * @notice Get the number of tokens `spender` is approved to spend on behalf of `account` 136 | * @param account The address of the account holding the funds 137 | * @param spender The address of the account spending the funds 138 | * @return The number of tokens approved 139 | */ 140 | function allowance(address account, address spender) external view returns (uint256) { 141 | return allowances[account][spender]; 142 | } 143 | 144 | /** 145 | * @notice Approve `spender` to transfer up to `amount` from `src` 146 | * @dev This will overwrite the approval amount for `spender` 147 | * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) 148 | * @param spender The address of the account which may transfer tokens 149 | * @param rawAmount The number of tokens that are approved (2^256-1 means infinite) 150 | * @return Whether or not the approval succeeded 151 | */ 152 | function approve(address spender, uint256 rawAmount) external returns (bool) { 153 | _approve(msg.sender, spender, rawAmount); 154 | return true; 155 | } 156 | 157 | function _approve( 158 | address owner, 159 | address spender, 160 | uint256 rawAmount 161 | ) internal { 162 | uint96 amount; 163 | if (rawAmount == uint256(-1)) { 164 | amount = uint96(-1); 165 | } else { 166 | amount = safe96(rawAmount, "RadicleToken::approve: amount exceeds 96 bits"); 167 | } 168 | 169 | allowances[owner][spender] = amount; 170 | 171 | emit Approval(owner, spender, amount); 172 | } 173 | 174 | /** 175 | * @notice Get the number of tokens held by the `account` 176 | * @param account The address of the account to get the balance of 177 | * @return The number of tokens held 178 | */ 179 | function balanceOf(address account) external view returns (uint256) { 180 | return balances[account]; 181 | } 182 | 183 | /** 184 | * @notice Transfer `amount` tokens from `msg.sender` to `dst` 185 | * @param dst The address of the destination account 186 | * @param rawAmount The number of tokens to transfer 187 | * @return Whether or not the transfer succeeded 188 | */ 189 | function transfer(address dst, uint256 rawAmount) external returns (bool) { 190 | uint96 amount = safe96(rawAmount, "RadicleToken::transfer: amount exceeds 96 bits"); 191 | _transferTokens(msg.sender, dst, amount); 192 | return true; 193 | } 194 | 195 | /** 196 | * @notice Transfer `amount` tokens from `src` to `dst` 197 | * @param src The address of the source account 198 | * @param dst The address of the destination account 199 | * @param rawAmount The number of tokens to transfer 200 | * @return Whether or not the transfer succeeded 201 | */ 202 | function transferFrom( 203 | address src, 204 | address dst, 205 | uint256 rawAmount 206 | ) external returns (bool) { 207 | address spender = msg.sender; 208 | uint96 spenderAllowance = allowances[src][spender]; 209 | uint96 amount = safe96(rawAmount, "RadicleToken::approve: amount exceeds 96 bits"); 210 | 211 | if (spender != src && spenderAllowance != uint96(-1)) { 212 | uint96 newAllowance = 213 | sub96( 214 | spenderAllowance, 215 | amount, 216 | "RadicleToken::transferFrom: transfer amount exceeds spender allowance" 217 | ); 218 | allowances[src][spender] = newAllowance; 219 | 220 | emit Approval(src, spender, newAllowance); 221 | } 222 | 223 | _transferTokens(src, dst, amount); 224 | return true; 225 | } 226 | 227 | /** 228 | * @notice Burn `rawAmount` tokens from `account` 229 | * @param account The address of the account to burn 230 | * @param rawAmount The number of tokens to burn 231 | */ 232 | function burnFrom(address account, uint256 rawAmount) public { 233 | require(account != address(0), "RadicleToken::burnFrom: cannot burn from the zero address"); 234 | uint96 amount = safe96(rawAmount, "RadicleToken::burnFrom: amount exceeds 96 bits"); 235 | 236 | address spender = msg.sender; 237 | uint96 spenderAllowance = allowances[account][spender]; 238 | if (spender != account && spenderAllowance != uint96(-1)) { 239 | uint96 newAllowance = 240 | sub96( 241 | spenderAllowance, 242 | amount, 243 | "RadicleToken::burnFrom: burn amount exceeds allowance" 244 | ); 245 | allowances[account][spender] = newAllowance; 246 | emit Approval(account, spender, newAllowance); 247 | } 248 | 249 | balances[account] = sub96( 250 | balances[account], 251 | amount, 252 | "RadicleToken::burnFrom: burn amount exceeds balance" 253 | ); 254 | emit Transfer(account, address(0), amount); 255 | 256 | _moveDelegates(delegates[account], address(0), amount); 257 | 258 | totalSupply -= rawAmount; 259 | } 260 | 261 | /** 262 | * @notice Delegate votes from `msg.sender` to `delegatee` 263 | * @param delegatee The address to delegate votes to 264 | */ 265 | function delegate(address delegatee) public { 266 | return _delegate(msg.sender, delegatee); 267 | } 268 | 269 | /** 270 | * @notice Delegates votes from signatory to `delegatee` 271 | * @param delegatee The address to delegate votes to 272 | * @param nonce The contract state required to match the signature 273 | * @param expiry The time at which to expire the signature 274 | * @param v The recovery byte of the signature 275 | * @param r Half of the ECDSA signature pair 276 | * @param s Half of the ECDSA signature pair 277 | */ 278 | function delegateBySig( 279 | address delegatee, 280 | uint256 nonce, 281 | uint256 expiry, 282 | uint8 v, 283 | bytes32 r, 284 | bytes32 s 285 | ) public { 286 | bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry)); 287 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash)); 288 | address signatory = ecrecover(digest, v, r, s); 289 | require(signatory != address(0), "RadicleToken::delegateBySig: invalid signature"); 290 | require(nonce == nonces[signatory]++, "RadicleToken::delegateBySig: invalid nonce"); 291 | require(block.timestamp <= expiry, "RadicleToken::delegateBySig: signature expired"); 292 | _delegate(signatory, delegatee); 293 | } 294 | 295 | /** 296 | * @notice Approves spender to spend on behalf of owner. 297 | * @param owner The signer of the permit 298 | * @param spender The address to approve 299 | * @param deadline The time at which the signature expires 300 | * @param v The recovery byte of the signature 301 | * @param r Half of the ECDSA signature pair 302 | * @param s Half of the ECDSA signature pair 303 | */ 304 | function permit( 305 | address owner, 306 | address spender, 307 | uint256 value, 308 | uint256 deadline, 309 | uint8 v, 310 | bytes32 r, 311 | bytes32 s 312 | ) public { 313 | bytes32 structHash = 314 | keccak256( 315 | abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline) 316 | ); 317 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash)); 318 | require(owner == ecrecover(digest, v, r, s), "RadicleToken::permit: invalid signature"); 319 | require(owner != address(0), "RadicleToken::permit: invalid signature"); 320 | require(block.timestamp <= deadline, "RadicleToken::permit: signature expired"); 321 | _approve(owner, spender, value); 322 | } 323 | 324 | /** 325 | * @notice Gets the current votes balance for `account` 326 | * @param account The address to get votes balance 327 | * @return The number of current votes for `account` 328 | */ 329 | function getCurrentVotes(address account) external view returns (uint96) { 330 | uint32 nCheckpoints = numCheckpoints[account]; 331 | return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; 332 | } 333 | 334 | /** 335 | * @notice Determine the prior number of votes for an account as of a block number 336 | * @dev Block number must be a finalized block or else this function will revert to prevent misinformation. 337 | * @param account The address of the account to check 338 | * @param blockNumber The block number to get the vote balance at 339 | * @return The number of votes the account had as of the given block 340 | */ 341 | function getPriorVotes(address account, uint256 blockNumber) public view returns (uint96) { 342 | require(blockNumber < block.number, "RadicleToken::getPriorVotes: not yet determined"); 343 | 344 | uint32 nCheckpoints = numCheckpoints[account]; 345 | if (nCheckpoints == 0) { 346 | return 0; 347 | } 348 | 349 | // First check most recent balance 350 | if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) { 351 | return checkpoints[account][nCheckpoints - 1].votes; 352 | } 353 | 354 | // Next check implicit zero balance 355 | if (checkpoints[account][0].fromBlock > blockNumber) { 356 | return 0; 357 | } 358 | 359 | uint32 lower = 0; 360 | uint32 upper = nCheckpoints - 1; 361 | while (upper > lower) { 362 | uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow 363 | Checkpoint memory cp = checkpoints[account][center]; 364 | if (cp.fromBlock == blockNumber) { 365 | return cp.votes; 366 | } else if (cp.fromBlock < blockNumber) { 367 | lower = center; 368 | } else { 369 | upper = center - 1; 370 | } 371 | } 372 | return checkpoints[account][lower].votes; 373 | } 374 | 375 | function _delegate(address delegator, address delegatee) internal { 376 | address currentDelegate = delegates[delegator]; 377 | uint96 delegatorBalance = balances[delegator]; 378 | delegates[delegator] = delegatee; 379 | 380 | emit DelegateChanged(delegator, currentDelegate, delegatee); 381 | 382 | _moveDelegates(currentDelegate, delegatee, delegatorBalance); 383 | } 384 | 385 | function _transferTokens( 386 | address src, 387 | address dst, 388 | uint96 amount 389 | ) internal { 390 | require( 391 | src != address(0), 392 | "RadicleToken::_transferTokens: cannot transfer from the zero address" 393 | ); 394 | require( 395 | dst != address(0), 396 | "RadicleToken::_transferTokens: cannot transfer to the zero address" 397 | ); 398 | 399 | balances[src] = sub96( 400 | balances[src], 401 | amount, 402 | "RadicleToken::_transferTokens: transfer amount exceeds balance" 403 | ); 404 | balances[dst] = add96( 405 | balances[dst], 406 | amount, 407 | "RadicleToken::_transferTokens: transfer amount overflows" 408 | ); 409 | emit Transfer(src, dst, amount); 410 | 411 | _moveDelegates(delegates[src], delegates[dst], amount); 412 | } 413 | 414 | function _moveDelegates( 415 | address srcRep, 416 | address dstRep, 417 | uint96 amount 418 | ) internal { 419 | if (srcRep != dstRep && amount > 0) { 420 | if (srcRep != address(0)) { 421 | uint32 srcRepNum = numCheckpoints[srcRep]; 422 | uint96 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0; 423 | uint96 srcRepNew = 424 | sub96(srcRepOld, amount, "RadicleToken::_moveVotes: vote amount underflows"); 425 | _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); 426 | } 427 | 428 | if (dstRep != address(0)) { 429 | uint32 dstRepNum = numCheckpoints[dstRep]; 430 | uint96 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0; 431 | uint96 dstRepNew = 432 | add96(dstRepOld, amount, "RadicleToken::_moveVotes: vote amount overflows"); 433 | _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew); 434 | } 435 | } 436 | } 437 | 438 | function _writeCheckpoint( 439 | address delegatee, 440 | uint32 nCheckpoints, 441 | uint96 oldVotes, 442 | uint96 newVotes 443 | ) internal { 444 | uint32 blockNumber = 445 | safe32(block.number, "RadicleToken::_writeCheckpoint: block number exceeds 32 bits"); 446 | 447 | if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber) { 448 | checkpoints[delegatee][nCheckpoints - 1].votes = newVotes; 449 | } else { 450 | checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes); 451 | numCheckpoints[delegatee] = nCheckpoints + 1; 452 | } 453 | 454 | emit DelegateVotesChanged(delegatee, oldVotes, newVotes); 455 | } 456 | 457 | function safe32(uint256 n, string memory errorMessage) internal pure returns (uint32) { 458 | require(n < 2**32, errorMessage); 459 | return uint32(n); 460 | } 461 | 462 | function safe96(uint256 n, string memory errorMessage) internal pure returns (uint96) { 463 | require(n < 2**96, errorMessage); 464 | return uint96(n); 465 | } 466 | 467 | function add96( 468 | uint96 a, 469 | uint96 b, 470 | string memory errorMessage 471 | ) internal pure returns (uint96) { 472 | uint96 c = a + b; 473 | require(c >= a, errorMessage); 474 | return c; 475 | } 476 | 477 | function sub96( 478 | uint96 a, 479 | uint96 b, 480 | string memory errorMessage 481 | ) internal pure returns (uint96) { 482 | require(b <= a, errorMessage); 483 | return a - b; 484 | } 485 | 486 | function getChainId() internal pure returns (uint256) { 487 | uint256 chainId; 488 | // solhint-disable no-inline-assembly 489 | assembly { 490 | chainId := chainid() 491 | } 492 | return chainId; 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /contracts/Governance/Timelock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // Copyright 2020 Compound Labs, Inc. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 2. Redistributions in binary form must reproduce the above copyright notice, 10 | // this list of conditions and the following disclaimer in the documentation 11 | // and/or other materials provided with the distribution. 12 | // 3. Neither the name of the copyright holder nor the names of its 13 | // contributors may be used to endorse or promote products derived from this 14 | // software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | // POSSIBILITY OF SUCH DAMAGE. 27 | 28 | pragma solidity ^0.7.5; 29 | 30 | import "../libraries/SafeMath.sol"; 31 | 32 | contract Timelock { 33 | using SafeMath for uint256; 34 | 35 | event NewAdmin(address indexed newAdmin); 36 | event NewPendingAdmin(address indexed newPendingAdmin); 37 | event NewDelay(uint256 indexed newDelay); 38 | event CancelTransaction( 39 | bytes32 indexed txHash, 40 | address indexed target, 41 | uint256 value, 42 | string signature, 43 | bytes data, 44 | uint256 eta 45 | ); 46 | event ExecuteTransaction( 47 | bytes32 indexed txHash, 48 | address indexed target, 49 | uint256 value, 50 | string signature, 51 | bytes data, 52 | uint256 eta 53 | ); 54 | event QueueTransaction( 55 | bytes32 indexed txHash, 56 | address indexed target, 57 | uint256 value, 58 | string signature, 59 | bytes data, 60 | uint256 eta 61 | ); 62 | 63 | uint256 public constant GRACE_PERIOD = 14 days; 64 | uint256 public constant MINIMUM_DELAY = 2 days; 65 | uint256 public constant MAXIMUM_DELAY = 30 days; 66 | 67 | address public admin; 68 | address public pendingAdmin; 69 | uint256 public delay; 70 | 71 | mapping(bytes32 => bool) public queuedTransactions; 72 | 73 | constructor(address admin_, uint256 delay_) { 74 | require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); 75 | require( 76 | delay_ <= MAXIMUM_DELAY, 77 | "Timelock::setDelay: Delay must not exceed maximum delay." 78 | ); 79 | 80 | admin = admin_; 81 | delay = delay_; 82 | } 83 | 84 | // solhint-disable no-empty-blocks 85 | receive() external payable {} 86 | 87 | function gracePeriod() public pure returns (uint256) { 88 | return GRACE_PERIOD; 89 | } 90 | 91 | function setDelay(uint256 delay_) public { 92 | require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); 93 | require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); 94 | require( 95 | delay_ <= MAXIMUM_DELAY, 96 | "Timelock::setDelay: Delay must not exceed maximum delay." 97 | ); 98 | delay = delay_; 99 | 100 | emit NewDelay(delay); 101 | } 102 | 103 | function acceptAdmin() public { 104 | require( 105 | msg.sender == pendingAdmin, 106 | "Timelock::acceptAdmin: Call must come from pendingAdmin." 107 | ); 108 | admin = msg.sender; 109 | pendingAdmin = address(0); 110 | 111 | emit NewAdmin(admin); 112 | } 113 | 114 | function setPendingAdmin(address pendingAdmin_) public { 115 | require( 116 | msg.sender == address(this), 117 | "Timelock::setPendingAdmin: Call must come from Timelock." 118 | ); 119 | pendingAdmin = pendingAdmin_; 120 | 121 | emit NewPendingAdmin(pendingAdmin); 122 | } 123 | 124 | function queueTransaction( 125 | address target, 126 | uint256 value, 127 | string calldata signature, 128 | bytes calldata data, 129 | uint256 eta 130 | ) public returns (bytes32) { 131 | require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); 132 | require( 133 | eta >= getBlockTimestamp().add(delay), 134 | "Timelock::queueTransaction: Estimated execution block must satisfy delay." 135 | ); 136 | 137 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 138 | queuedTransactions[txHash] = true; 139 | 140 | emit QueueTransaction(txHash, target, value, signature, data, eta); 141 | return txHash; 142 | } 143 | 144 | function cancelTransaction( 145 | address target, 146 | uint256 value, 147 | string calldata signature, 148 | bytes calldata data, 149 | uint256 eta 150 | ) public { 151 | require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); 152 | 153 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 154 | queuedTransactions[txHash] = false; 155 | 156 | emit CancelTransaction(txHash, target, value, signature, data, eta); 157 | } 158 | 159 | function executeTransaction( 160 | address target, 161 | uint256 value, 162 | string calldata signature, 163 | bytes calldata data, 164 | uint256 eta 165 | ) public payable returns (bytes memory) { 166 | require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); 167 | 168 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 169 | require( 170 | queuedTransactions[txHash], 171 | "Timelock::executeTransaction: Transaction hasn't been queued." 172 | ); 173 | require( 174 | getBlockTimestamp() >= eta, 175 | "Timelock::executeTransaction: Transaction hasn't surpassed time lock." 176 | ); 177 | require( 178 | getBlockTimestamp() <= eta.add(GRACE_PERIOD), 179 | "Timelock::executeTransaction: Transaction is stale." 180 | ); 181 | 182 | queuedTransactions[txHash] = false; 183 | 184 | bytes memory callData; 185 | 186 | if (bytes(signature).length == 0) { 187 | callData = data; 188 | } else { 189 | callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); 190 | } 191 | 192 | // solhint-disable avoid-low-level-calls 193 | (bool success, bytes memory returnData) = target.call{value: value}(callData); 194 | require(success, "Timelock::executeTransaction: Transaction execution reverted."); 195 | 196 | emit ExecuteTransaction(txHash, target, value, signature, data, eta); 197 | 198 | return returnData; 199 | } 200 | 201 | function getBlockTimestamp() internal view returns (uint256) { 202 | // solium-disable-next-line security/no-block-members 203 | return block.timestamp; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /contracts/Governance/VestingToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | /// Token vesting contract. 5 | /// Adapted from Melonport AG 6 | 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | 9 | contract VestingToken { 10 | using SafeMath for uint256; 11 | 12 | ERC20 public immutable token; // Radicle ERC20 contract 13 | 14 | address public immutable owner; // deployer; can interrupt vesting 15 | uint256 public immutable totalVestingAmount; // quantity of vested token in total 16 | uint256 public immutable vestingStartTime; // timestamp when vesting is set 17 | uint256 public immutable vestingPeriod; // total vesting period in seconds 18 | uint256 public immutable cliffPeriod; // cliff period 19 | address public immutable beneficiary; // address of the beneficiary 20 | 21 | bool public interrupted; // whether vesting is still possible 22 | uint256 public withdrawn; // quantity of token withdrawn so far 23 | 24 | /// Vesting was terminated. 25 | event VestingTerminated(uint256 remainingVested, uint256 remainingUnvested); 26 | /// Vesting tokens were withdrawn 27 | event VestedWithdrawn(uint256 amount); 28 | 29 | modifier notInterrupted() { 30 | require(!interrupted, "The contract has been interrupted"); 31 | _; 32 | } 33 | 34 | modifier onlyOwner() { 35 | require(msg.sender == owner, "Only owner can do this"); 36 | _; 37 | } 38 | 39 | modifier onlyBeneficiary() { 40 | require(msg.sender == beneficiary, "Only beneficiary can do this"); 41 | _; 42 | } 43 | 44 | /// @notice Create a vesting allocation of tokens. 45 | /// 46 | /// @param _token Address of token being vested 47 | /// @param _beneficiary Address of beneficiary 48 | /// @param _amount Amount of tokens 49 | /// @param _vestingPeriod Vesting period in seconds from vestingStartTime 50 | /// @param _vestingStartTime Vesting start time in seconds since Epoch 51 | constructor( 52 | address _token, 53 | address _owner, 54 | address _beneficiary, 55 | uint256 _amount, 56 | uint256 _vestingStartTime, 57 | uint256 _vestingPeriod, 58 | uint256 _cliffPeriod 59 | ) { 60 | require(_beneficiary != address(0), "Beneficiary cannot be the zero address"); 61 | require(_vestingStartTime < block.timestamp, "Vesting start time must be in the past"); 62 | require(_vestingPeriod > 0, "Vesting period must be positive"); 63 | require(_amount > 0, "VestingToken::constructor: amount must be positive"); 64 | 65 | ERC20 erc20 = ERC20(_token); 66 | require( 67 | erc20.transferFrom(msg.sender, address(this), _amount), 68 | "VestingToken::constructor: token deposit failed" 69 | ); 70 | 71 | token = erc20; 72 | owner = _owner; 73 | totalVestingAmount = _amount; 74 | beneficiary = _beneficiary; 75 | vestingStartTime = _vestingStartTime; 76 | vestingPeriod = _vestingPeriod; 77 | cliffPeriod = _cliffPeriod; 78 | } 79 | 80 | /// @notice Returns the token amount that is currently withdrawable 81 | /// @return withdrawable Quantity of withdrawable Radicle asset 82 | function withdrawableBalance() public view returns (uint256 withdrawable) { 83 | if (interrupted) return 0; 84 | 85 | uint256 timePassed = block.timestamp.sub(vestingStartTime); 86 | 87 | if (timePassed < cliffPeriod) { 88 | withdrawable = 0; 89 | } else if (timePassed < vestingPeriod) { 90 | uint256 vested = totalVestingAmount.mul(timePassed) / vestingPeriod; 91 | withdrawable = vested.sub(withdrawn); 92 | } else { 93 | withdrawable = totalVestingAmount.sub(withdrawn); 94 | } 95 | } 96 | 97 | /// @notice Withdraw vested tokens 98 | function withdrawVested() external onlyBeneficiary notInterrupted { 99 | uint256 withdrawable = withdrawableBalance(); 100 | 101 | withdrawn = withdrawn.add(withdrawable); 102 | 103 | require( 104 | token.transfer(beneficiary, withdrawable), 105 | "VestingToken::withdrawVested: transfer to beneficiary failed" 106 | ); 107 | emit VestedWithdrawn(withdrawable); 108 | } 109 | 110 | /// @notice Force withdrawal of vested tokens to beneficiary 111 | /// @notice Send remainder back to owner 112 | /// @notice Prevent further vesting 113 | function terminateVesting() external onlyOwner notInterrupted { 114 | uint256 remainingVested = withdrawableBalance(); 115 | uint256 totalToBeVested = withdrawn.add(remainingVested); 116 | uint256 remainingUnvested = totalVestingAmount.sub(totalToBeVested); 117 | 118 | interrupted = true; 119 | withdrawn = totalToBeVested; 120 | 121 | require( 122 | token.transfer(beneficiary, remainingVested), 123 | "VestingToken::terminateVesting: transfer to beneficiary failed" 124 | ); 125 | require( 126 | token.transfer(owner, remainingUnvested), 127 | "VestingToken::terminateVesting: transfer to owner failed" 128 | ); 129 | emit VestingTerminated(remainingVested, remainingUnvested); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /contracts/PoolTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "./libraries/ProxyDeltas.sol"; 6 | import "./libraries/ReceiverWeights.sol"; 7 | import "hardhat/console.sol"; 8 | 9 | contract ReceiverWeightsTest { 10 | bool internal constant PRINT_GAS_USAGE = false; 11 | 12 | using ReceiverWeightsImpl for ReceiverWeights; 13 | 14 | /// @dev The tested data structure 15 | ReceiverWeights private receiverWeights; 16 | /// @dev The values returned from the iteration after the last `setWeights` call 17 | ReceiverWeightIterated[] private receiverWeightsIterated; 18 | /// @notice The change of sum of the stored receiver weights due to the last `setWeights` call 19 | int256 public weightReceiverSumDelta; 20 | /// @notice The change of sum of the stored proxy weights due to the last `setWeights` call 21 | int256 public weightProxySumDelta; 22 | 23 | struct ReceiverWeightIterated { 24 | address receiver; 25 | uint32 weightReceiver; 26 | uint32 weightProxy; 27 | } 28 | 29 | function setWeights(ReceiverWeightIterated[] calldata weights) external { 30 | require( 31 | receiverWeights.isZeroed() == (receiverWeightsIterated.length == 0), 32 | "Invalid result of isZeroed" 33 | ); 34 | weightReceiverSumDelta = 0; 35 | weightProxySumDelta = 0; 36 | uint256 totalGasUsed = 0; 37 | for (uint256 i = 0; i < weights.length; i++) { 38 | address setReceiver = weights[i].receiver; 39 | uint32 newWeightReceiver = weights[i].weightReceiver; 40 | uint32 newWeightProxy = weights[i].weightProxy; 41 | uint256 gasUsed = gasleft(); 42 | uint32 oldWeightReceiver = 43 | receiverWeights.setReceiverWeight(setReceiver, newWeightReceiver); 44 | gasUsed -= gasleft(); 45 | uint32 oldWeightProxy = receiverWeights.setProxyWeight(setReceiver, newWeightProxy); 46 | totalGasUsed += gasUsed; 47 | weightReceiverSumDelta -= oldWeightReceiver; 48 | weightReceiverSumDelta += newWeightReceiver; 49 | weightProxySumDelta -= oldWeightProxy; 50 | weightProxySumDelta += newWeightProxy; 51 | if (PRINT_GAS_USAGE) 52 | console.log( 53 | "Setting for receiver %s weight %d with gas used %d", 54 | setReceiver, 55 | newWeightReceiver, 56 | gasUsed 57 | ); 58 | } 59 | delete receiverWeightsIterated; 60 | address receiver = ReceiverWeightsImpl.ADDR_ROOT; 61 | address hint = ReceiverWeightsImpl.ADDR_ROOT; 62 | uint256 iterationGasUsed = 0; 63 | while (true) { 64 | // Each step of the non-pruning iteration should yield the same items 65 | ( 66 | address receiverIter, 67 | address hintIter, 68 | uint32 weightReceiverIter, 69 | uint32 weightProxyIter 70 | ) = receiverWeights.nextWeight(receiver, hint); 71 | uint32 weightReceiver; 72 | uint32 weightProxy; 73 | uint256 gasLeftBefore = gasleft(); 74 | (receiver, hint, weightReceiver, weightProxy) = receiverWeights.nextWeightPruning( 75 | receiver, 76 | hint 77 | ); 78 | iterationGasUsed += gasLeftBefore - gasleft(); 79 | require(receiverIter == receiver, "Non-pruning iterator yielded a different receiver"); 80 | require(hintIter == hint, "Non-pruning iterator yielded a different next receiver"); 81 | require( 82 | weightReceiverIter == weightReceiver, 83 | "Non-pruning iterator yielded a different receiver weight" 84 | ); 85 | require( 86 | weightProxyIter == weightProxy, 87 | "Non-pruning iterator yielded a different proxy weight" 88 | ); 89 | if (receiver == ReceiverWeightsImpl.ADDR_ROOT) break; 90 | receiverWeightsIterated.push( 91 | ReceiverWeightIterated(receiver, weightReceiver, weightProxy) 92 | ); 93 | } 94 | if (PRINT_GAS_USAGE) { 95 | console.log("Iterated over weight list with gas used %d", iterationGasUsed); 96 | console.log("Total gas used %d", totalGasUsed + iterationGasUsed); 97 | } 98 | } 99 | 100 | /// @dev Making `receiverWeightsIterated` public would generate 101 | /// a getter accepting an index parameter and returning a single item 102 | function getReceiverWeightsIterated() external view returns (ReceiverWeightIterated[] memory) { 103 | return receiverWeightsIterated; 104 | } 105 | } 106 | 107 | contract ProxyDeltasTest { 108 | bool internal constant PRINT_GAS_USAGE = false; 109 | 110 | using ProxyDeltasImpl for ProxyDeltas; 111 | 112 | /// @dev The tested data structure 113 | ProxyDeltas private proxyDeltas; 114 | /// @dev The values returned from the iteration after the last `addToDeltas` call 115 | ProxyDeltaIterated[] private proxyDeltasIterated; 116 | 117 | struct ProxyDeltaIterated { 118 | uint64 cycle; 119 | int128 thisCycleDelta; 120 | int128 nextCycleDelta; 121 | } 122 | 123 | function addToDeltas(uint64 finishedCycle, ProxyDeltaIterated[] calldata deltas) external { 124 | uint256 totalGasUsed = 0; 125 | for (uint256 i = 0; i < deltas.length; i++) { 126 | ProxyDeltaIterated calldata delta = deltas[i]; 127 | uint256 addToDeltaGasUsed = gasleft(); 128 | proxyDeltas.addToDelta(delta.cycle, delta.thisCycleDelta, delta.nextCycleDelta); 129 | addToDeltaGasUsed -= gasleft(); 130 | totalGasUsed += addToDeltaGasUsed; 131 | if (PRINT_GAS_USAGE) { 132 | if (delta.thisCycleDelta >= 0) 133 | console.log( 134 | "Adding to cycle %s delta %d with gas used %d", 135 | delta.cycle, 136 | uint128(delta.thisCycleDelta), 137 | addToDeltaGasUsed 138 | ); 139 | else 140 | console.log( 141 | "Adding to cycle %s delta -%d with gas used %d", 142 | delta.cycle, 143 | uint128(-delta.thisCycleDelta), 144 | addToDeltaGasUsed 145 | ); 146 | } 147 | } 148 | delete proxyDeltasIterated; 149 | uint64 cycle = ProxyDeltasImpl.CYCLE_ROOT; 150 | uint64 hint = ProxyDeltasImpl.CYCLE_ROOT; 151 | uint256 gasUsed = 0; 152 | while (true) { 153 | int128 thisCycleDelta; 154 | int128 nextCycleDelta; 155 | uint256 gasLeftBefore = gasleft(); 156 | (cycle, hint, thisCycleDelta, nextCycleDelta) = proxyDeltas.nextDeltaPruning( 157 | cycle, 158 | hint, 159 | finishedCycle 160 | ); 161 | gasUsed += gasLeftBefore - gasleft(); 162 | if (cycle == ProxyDeltasImpl.CYCLE_ROOT) break; 163 | proxyDeltasIterated.push(ProxyDeltaIterated(cycle, thisCycleDelta, nextCycleDelta)); 164 | } 165 | if (PRINT_GAS_USAGE) { 166 | console.log("Iterated over proxy deltas with gas used %d", gasUsed); 167 | console.log("Total gas used %d", totalGasUsed + gasUsed); 168 | } 169 | } 170 | 171 | /// @dev Making `proxyDeltasIterated` public would generate 172 | /// a getter accepting an index parameter and returning a single item 173 | function getProxyDeltasIterated() external view returns (ProxyDeltaIterated[] memory) { 174 | return proxyDeltasIterated; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /contracts/Proxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // Copyright (c) 2020 Compound Labs, Inc. 3 | 4 | pragma solidity ^0.7.5; 5 | 6 | import "./ErrorReporter.sol"; 7 | import "./ProxyAdminStorage.sol"; 8 | 9 | /** 10 | * @dev This abstract contract provides a fallback function that delegates all calls to another 11 | * contract using the EVM instruction `delegatecall`. We refer to the second contract as the 12 | * _implementation_ behind the proxy. 13 | * 14 | * Additionally, delegation to the implementation can be triggered manually through the {_fallback} 15 | * function, or to a different contract through the {_delegate} function. 16 | * 17 | * The success and return data of the delegated call will be returned back to the caller of the 18 | * proxy. 19 | */ 20 | contract Proxy is ProxyAdminStorage, ErrorReporter { 21 | /** 22 | * @notice Emitted when pendingImplementation is changed 23 | */ 24 | event NewPendingImplementation( 25 | address oldPendingImplementation, 26 | address newPendingImplementation 27 | ); 28 | 29 | /** 30 | * @notice Emitted when pendingImplementation is accepted, which means comptroller 31 | * implementation is updated 32 | */ 33 | event NewImplementation(address oldImplementation, address newImplementation); 34 | 35 | /** 36 | * @notice Emitted when pendingAdmin is changed 37 | */ 38 | event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); 39 | 40 | /** 41 | * @notice Emitted when pendingAdmin is accepted, which means admin is updated 42 | */ 43 | event NewAdmin(address oldAdmin, address newAdmin); 44 | 45 | constructor(address _admin) { 46 | // Set admin to caller 47 | admin = _admin; 48 | } 49 | 50 | /*** Admin Functions ***/ 51 | function _setPendingImplementation(address newPendingImplementation) public returns (uint256) { 52 | if (msg.sender != admin) { 53 | return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_IMPLEMENTATION_OWNER_CHECK); 54 | } 55 | 56 | address oldPendingImplementation = pendingImplementation; 57 | 58 | pendingImplementation = newPendingImplementation; 59 | 60 | emit NewPendingImplementation(oldPendingImplementation, pendingImplementation); 61 | 62 | return uint256(Error.NO_ERROR); 63 | } 64 | 65 | /** 66 | * @notice Accepts new implementation. `msg.sender` must be pendingImplementation 67 | * @dev Admin function for new implementation to accept its role as implementation 68 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 69 | */ 70 | function _acceptImplementation() public returns (uint256) { 71 | // Check caller is pendingImplementation and pendingImplementation ≠ address(0) 72 | if (msg.sender != pendingImplementation || pendingImplementation == address(0)) { 73 | return 74 | fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK); 75 | } 76 | 77 | // Save current values for inclusion in log 78 | address oldImplementation = implementation; 79 | address oldPendingImplementation = pendingImplementation; 80 | 81 | implementation = pendingImplementation; 82 | 83 | pendingImplementation = address(0); 84 | 85 | emit NewImplementation(oldImplementation, implementation); 86 | emit NewPendingImplementation(oldPendingImplementation, pendingImplementation); 87 | 88 | return uint256(Error.NO_ERROR); 89 | } 90 | 91 | /** 92 | * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to 93 | * finalize the transfer. 94 | * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to 95 | * finalize the transfer. 96 | * @param newPendingAdmin New pending admin. 97 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 98 | */ 99 | function _setPendingAdmin(address newPendingAdmin) public returns (uint256) { 100 | // Check caller = admin 101 | if (msg.sender != admin) { 102 | return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_ADMIN_OWNER_CHECK); 103 | } 104 | 105 | // Save current value, if any, for inclusion in log 106 | address oldPendingAdmin = pendingAdmin; 107 | 108 | // Store pendingAdmin with value newPendingAdmin 109 | pendingAdmin = newPendingAdmin; 110 | 111 | // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) 112 | emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); 113 | 114 | return uint256(Error.NO_ERROR); 115 | } 116 | 117 | /** 118 | * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin 119 | * @dev Admin function for pending admin to accept role and update admin 120 | * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) 121 | */ 122 | function _acceptAdmin() public returns (uint256) { 123 | // Check caller is pendingAdmin and pendingAdmin ≠ address(0) 124 | if (msg.sender != pendingAdmin || msg.sender == address(0)) { 125 | return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_ADMIN_PENDING_ADMIN_CHECK); 126 | } 127 | 128 | // Save current values for inclusion in log 129 | address oldAdmin = admin; 130 | address oldPendingAdmin = pendingAdmin; 131 | 132 | // Store admin with value pendingAdmin 133 | admin = pendingAdmin; 134 | 135 | // Clear the pending value 136 | pendingAdmin = address(0); 137 | 138 | emit NewAdmin(oldAdmin, admin); 139 | emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); 140 | 141 | return uint256(Error.NO_ERROR); 142 | } 143 | 144 | /** 145 | * @dev Delegates the current call to `implementation`. 146 | * 147 | * This function does not return to its internall call site, it will return directly to the external caller. 148 | */ 149 | function _delegate(address impl) internal { 150 | // Prevents potential attacks when the target contract methods clash 151 | // with the proxy contract. The admin should never need to call into 152 | // the target contract. 153 | require(msg.sender != admin, "Proxy: admin cannot fallback to proxy target"); 154 | 155 | // solhint-disable-next-line avoid-low-level-calls 156 | (bool success, ) = impl.delegatecall(msg.data); 157 | 158 | // solhint-disable-next-line no-inline-assembly 159 | assembly { 160 | returndatacopy(0, 0, returndatasize()) 161 | 162 | switch success 163 | case 0 { 164 | revert(0, returndatasize()) 165 | } 166 | default { 167 | return(0, returndatasize()) 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * @dev Fallback function that delegates calls to the address in `implementation`. 174 | * Will run if no other function in the contract matches the call data. 175 | */ 176 | fallback() external payable { 177 | _delegate(implementation); 178 | } 179 | 180 | /** 181 | * @dev Fallback function that delegates calls to the address in `implementation`. 182 | * Will run if call data is empty. 183 | */ 184 | receive() external payable { 185 | _delegate(implementation); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /contracts/ProxyAdminStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | contract ProxyAdminStorage { 5 | /// Administrator for this contract 6 | address public admin; 7 | 8 | /// Pending administrator for this contract 9 | address public pendingAdmin; 10 | 11 | /// Active implementation behind this proxy 12 | address public implementation; 13 | 14 | /// Pending implementation behind this proxy 15 | address public pendingImplementation; 16 | } 17 | -------------------------------------------------------------------------------- /contracts/Registrar.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // solhint-disable no-empty-blocks 3 | pragma solidity ^0.7.5; 4 | 5 | import "@ensdomains/ens/contracts/ENS.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 7 | 8 | // commitments are kept in a seperate contract to allow the state to be reused 9 | // between different versions of the registrar 10 | contract Commitments { 11 | address public owner; 12 | modifier auth { 13 | require(msg.sender == owner, "Commitments: unauthorized"); 14 | _; 15 | } 16 | event SetOwner(address usr); 17 | 18 | /// Mapping from the commitment to the block number in which the commitment was made 19 | mapping(bytes32 => uint256) public commited; 20 | 21 | constructor() { 22 | owner = msg.sender; 23 | } 24 | 25 | function setOwner(address usr) external auth { 26 | owner = usr; 27 | emit SetOwner(usr); 28 | } 29 | 30 | function commit(bytes32 commitment) external auth { 31 | commited[commitment] = block.number; 32 | } 33 | } 34 | 35 | contract Registrar { 36 | // --- DATA --- 37 | 38 | /// The ENS registry. 39 | ENS public immutable ens; 40 | 41 | /// The Radicle ERC20 token. 42 | RadicleTokenI public immutable rad; 43 | 44 | /// @notice EIP-712 name for this contract 45 | string public constant NAME = "Registrar"; 46 | 47 | /// The commitment storage contract 48 | Commitments public immutable commitments = new Commitments(); 49 | 50 | /// The namehash of the `eth` TLD in the ENS registry, eg. namehash("eth"). 51 | bytes32 public constant ETH_NODE = keccak256(abi.encodePacked(bytes32(0), keccak256("eth"))); 52 | 53 | /// The namehash of the node in the `eth` TLD, eg. namehash("radicle.eth"). 54 | bytes32 public immutable radNode; 55 | 56 | /// The token ID for the node in the `eth` TLD, eg. sha256("radicle"). 57 | uint256 public immutable tokenId; 58 | 59 | /// The minimum number of blocks that must have passed between a commitment and name registration 60 | uint256 public minCommitmentAge; 61 | 62 | /// Registration fee in *Radicle* (uRads). 63 | uint256 public registrationFeeRad = 10e18; 64 | 65 | /// @notice The EIP-712 typehash for the contract's domain 66 | bytes32 public constant DOMAIN_TYPEHASH = 67 | keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); 68 | 69 | /// @notice The EIP-712 typehash for the delegation struct used by the contract 70 | bytes32 public constant COMMIT_TYPEHASH = 71 | keccak256("Commit(bytes32 commitment,uint256 nonce,uint256 expiry,uint256 submissionFee)"); 72 | 73 | /// @notice A record of states for signing / validating signatures 74 | mapping(address => uint256) public nonces; 75 | 76 | // --- LOGS --- 77 | 78 | /// @notice A name was registered. 79 | event NameRegistered(string indexed name, bytes32 indexed label, address indexed owner); 80 | 81 | /// @notice A commitment was made 82 | event CommitmentMade(bytes32 commitment, uint256 blockNumber); 83 | 84 | /// @notice The contract admin was changed 85 | event AdminChanged(address newAdmin); 86 | 87 | /// @notice The registration fee was changed 88 | event RegistrationRadFeeChanged(uint256 amt); 89 | 90 | /// @notice The ownership of the domain was changed 91 | event DomainOwnershipChanged(address newOwner); 92 | 93 | /// @notice The resolver changed 94 | event ResolverChanged(address resolver); 95 | 96 | /// @notice The ttl changed 97 | event TTLChanged(uint64 amt); 98 | 99 | /// @notice The minimum age for a commitment was changed 100 | event MinCommitmentAgeChanged(uint256 amt); 101 | 102 | // --- AUTH --- 103 | 104 | /// The contract admin who can set fees. 105 | address public admin; 106 | 107 | /// Protects admin-only functions. 108 | modifier adminOnly { 109 | require(msg.sender == admin, "Registrar: only the admin can perform this action"); 110 | _; 111 | } 112 | 113 | // --- INIT --- 114 | 115 | constructor( 116 | ENS _ens, 117 | RadicleTokenI _rad, 118 | address _admin, 119 | uint256 _minCommitmentAge, 120 | bytes32 _radNode, 121 | uint256 _tokenId 122 | ) { 123 | ens = _ens; 124 | rad = _rad; 125 | admin = _admin; 126 | minCommitmentAge = _minCommitmentAge; 127 | radNode = _radNode; 128 | tokenId = _tokenId; 129 | } 130 | 131 | // --- USER FACING METHODS --- 132 | 133 | /// Commit to a future name registration 134 | function commit(bytes32 commitment) public { 135 | _commit(msg.sender, commitment); 136 | } 137 | 138 | /// Commit to a future name and submit permit in the same transaction 139 | function commitWithPermit( 140 | bytes32 commitment, 141 | address owner, 142 | uint256 value, 143 | uint256 deadline, 144 | uint8 v, 145 | bytes32 r, 146 | bytes32 s 147 | ) external { 148 | rad.permit(owner, address(this), value, deadline, v, r, s); 149 | _commit(msg.sender, commitment); 150 | } 151 | 152 | /// Commit to a future name with a 712-signed message 153 | function commitBySig( 154 | bytes32 commitment, 155 | uint256 nonce, 156 | uint256 expiry, 157 | uint256 submissionFee, 158 | uint8 v, 159 | bytes32 r, 160 | bytes32 s 161 | ) public { 162 | bytes32 domainSeparator = 163 | keccak256( 164 | abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(NAME)), getChainId(), address(this)) 165 | ); 166 | bytes32 structHash = 167 | keccak256(abi.encode(COMMIT_TYPEHASH, commitment, nonce, expiry, submissionFee)); 168 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); 169 | address signatory = ecrecover(digest, v, r, s); 170 | require(signatory != address(0), "Registrar::commitBySig: invalid signature"); 171 | require(nonce == nonces[signatory]++, "Registrar::commitBySig: invalid nonce"); 172 | require(block.timestamp <= expiry, "Registrar::commitBySig: signature expired"); 173 | rad.transferFrom(signatory, msg.sender, submissionFee); 174 | _commit(signatory, commitment); 175 | } 176 | 177 | /// Commit to a future name with a 712-signed message and submit permit in the same transaction 178 | function commitBySigWithPermit( 179 | bytes32 commitment, 180 | uint256 nonce, 181 | uint256 expiry, 182 | uint256 submissionFee, 183 | uint8 v, 184 | bytes32 r, 185 | bytes32 s, 186 | address owner, 187 | uint256 value, 188 | uint256 deadline, 189 | uint8 permitV, 190 | bytes32 permitR, 191 | bytes32 permitS 192 | ) public { 193 | rad.permit(owner, address(this), value, deadline, permitV, permitR, permitS); 194 | commitBySig(commitment, nonce, expiry, submissionFee, v, r, s); 195 | } 196 | 197 | function _commit(address payer, bytes32 commitment) internal { 198 | require(commitments.commited(commitment) == 0, "Registrar::commit: already commited"); 199 | 200 | rad.burnFrom(payer, registrationFeeRad); 201 | commitments.commit(commitment); 202 | 203 | emit CommitmentMade(commitment, block.number); 204 | } 205 | 206 | /// Register a subdomain 207 | function register( 208 | string calldata name, 209 | address owner, 210 | uint256 salt 211 | ) external { 212 | bytes32 label = keccak256(bytes(name)); 213 | bytes32 commitment = keccak256(abi.encodePacked(name, owner, salt)); 214 | uint256 commited = commitments.commited(commitment); 215 | 216 | require(valid(name), "Registrar::register: invalid name"); 217 | require(available(name), "Registrar::register: name has already been registered"); 218 | require(commited != 0, "Registrar::register: must commit before registration"); 219 | require( 220 | commited + minCommitmentAge < block.number, 221 | "Registrar::register: commitment too new" 222 | ); 223 | 224 | ens.setSubnodeRecord(radNode, label, owner, ens.resolver(radNode), ens.ttl(radNode)); 225 | 226 | emit NameRegistered(name, label, owner); 227 | } 228 | 229 | /// Check whether a name is valid. 230 | function valid(string memory name) public pure returns (bool) { 231 | uint256 len = bytes(name).length; 232 | return len >= 2 && len <= 128; 233 | } 234 | 235 | /// Check whether a name is available for registration. 236 | function available(string memory name) public view returns (bool) { 237 | bytes32 label = keccak256(bytes(name)); 238 | bytes32 node = namehash(radNode, label); 239 | return ens.owner(node) == address(0); 240 | } 241 | 242 | /// Get the "namehash" of a label. 243 | function namehash(bytes32 parent, bytes32 label) public pure returns (bytes32) { 244 | return keccak256(abi.encodePacked(parent, label)); 245 | } 246 | 247 | // --- ADMIN METHODS --- 248 | 249 | /// Set the owner of the domain. 250 | function setDomainOwner(address newOwner) public adminOnly { 251 | IERC721 ethRegistrar = IERC721(ens.owner(ETH_NODE)); 252 | 253 | ens.setOwner(radNode, newOwner); 254 | ethRegistrar.transferFrom(address(this), newOwner, tokenId); 255 | commitments.setOwner(newOwner); 256 | 257 | emit DomainOwnershipChanged(newOwner); 258 | } 259 | 260 | /// Set a new resolver for radicle.eth. 261 | function setDomainResolver(address resolver) public adminOnly { 262 | ens.setResolver(radNode, resolver); 263 | emit ResolverChanged(resolver); 264 | } 265 | 266 | /// Set a new ttl for radicle.eth 267 | function setDomainTTL(uint64 ttl) public adminOnly { 268 | ens.setTTL(radNode, ttl); 269 | emit TTLChanged(ttl); 270 | } 271 | 272 | /// Set the minimum commitment age 273 | function setMinCommitmentAge(uint256 amt) public adminOnly { 274 | minCommitmentAge = amt; 275 | emit MinCommitmentAgeChanged(amt); 276 | } 277 | 278 | /// Set a new registration fee 279 | function setRadRegistrationFee(uint256 amt) public adminOnly { 280 | registrationFeeRad = amt; 281 | emit RegistrationRadFeeChanged(amt); 282 | } 283 | 284 | /// Set a new admin 285 | function setAdmin(address newAdmin) public adminOnly { 286 | admin = newAdmin; 287 | emit AdminChanged(newAdmin); 288 | } 289 | 290 | function getChainId() internal pure returns (uint256) { 291 | uint256 chainId; 292 | // solhint-disable no-inline-assembly 293 | assembly { 294 | chainId := chainid() 295 | } 296 | return chainId; 297 | } 298 | } 299 | 300 | interface RadicleTokenI { 301 | function transferFrom( 302 | address src, 303 | address dst, 304 | uint256 rawAmount 305 | ) external returns (bool); 306 | 307 | function burnFrom(address account, uint256 rawAmount) external; 308 | 309 | function permit( 310 | address owner, 311 | address spender, 312 | uint256 value, 313 | uint256 deadline, 314 | uint8 v, 315 | bytes32 r, 316 | bytes32 s 317 | ) external; 318 | } 319 | -------------------------------------------------------------------------------- /contracts/TestDai.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity ^0.7.5; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract Dai is ERC20 { 8 | bytes32 private immutable domainSeparator; 9 | bytes32 private immutable typehash; 10 | mapping(address => uint256) public nonces; 11 | 12 | constructor() ERC20("DAI Stablecoin", "DAI") { 13 | // TODO replace with `block.chainid` after upgrade to Solidity 0.8 14 | uint256 chainId; 15 | // solhint-disable no-inline-assembly 16 | assembly { 17 | chainId := chainid() 18 | } 19 | domainSeparator = keccak256( 20 | abi.encode( 21 | keccak256( 22 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 23 | ), 24 | keccak256(bytes(name())), 25 | keccak256(bytes("1")), 26 | chainId, 27 | address(this) 28 | ) 29 | ); 30 | typehash = keccak256( 31 | "Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)" 32 | ); 33 | _mint(msg.sender, 10**9 * 10**18); // 1 billion DAI, 18 decimals 34 | } 35 | 36 | function permit( 37 | address holder, 38 | address spender, 39 | uint256 nonce, 40 | uint256 expiry, 41 | bool allowed, 42 | uint8 v, 43 | bytes32 r, 44 | bytes32 s 45 | ) external { 46 | bytes32 message = keccak256(abi.encode(typehash, holder, spender, nonce, expiry, allowed)); 47 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, message)); 48 | address signer = ecrecover(digest, v, r, s); 49 | require(holder == signer, "Invalid signature"); 50 | require(nonce == nonces[holder]++, "Invalid nonce"); 51 | require(expiry == 0 || expiry > block.timestamp, "Signature expired"); 52 | uint256 amount = allowed ? type(uint256).max : 0; 53 | _approve(holder, spender, amount); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/deploy/phase0.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | import {ENS} from "@ensdomains/ens/contracts/ENS.sol"; 5 | import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol"; 6 | import {Governor} from "../Governance/Governor.sol"; 7 | import {RadicleToken} from "../Governance/RadicleToken.sol"; 8 | import {Timelock} from "../Governance/Timelock.sol"; 9 | import {Registrar, RadicleTokenI} from "../Registrar.sol"; 10 | 11 | contract Phase0 { 12 | RadicleToken public immutable token; 13 | Timelock public immutable timelock; 14 | Governor public immutable governor; 15 | Registrar public immutable registrar; 16 | 17 | address public immutable monadicAddr; 18 | address public immutable foundationAddr; 19 | uint256 public immutable timelockDelay; 20 | address public immutable governorGuardian; 21 | ENS public immutable ens; 22 | bytes32 public immutable namehash; 23 | string public label; 24 | 25 | uint256 public constant MONADIC_ALLOCATION = 32221392e18; 26 | uint256 public constant FOUNDATION_ALLOCATION = 13925009e18; 27 | uint256 public constant TREASURY_ALLOCATION = 53853599e18; 28 | 29 | // solhint-disable reentrancy 30 | constructor( 31 | address _monadicAddr, 32 | address _foundationAddr, 33 | uint256 _timelockDelay, 34 | address _governorGuardian, 35 | ENS _ens, 36 | bytes32 _namehash, 37 | string memory _label 38 | ) { 39 | require( 40 | uint160(address(this)) >> 154 != 0, 41 | "Factory contract address starts with 0 byte, " 42 | "please make any transaction and rerun deployment" 43 | ); 44 | uint8 governorNonce = 3; 45 | bytes memory govAddrPayload = abi.encodePacked(hex"d694", address(this), governorNonce); 46 | address govAddr = address(uint256(keccak256(govAddrPayload))); 47 | 48 | RadicleToken _token = new RadicleToken(address(this)); 49 | Timelock _timelock = new Timelock(govAddr, _timelockDelay); 50 | Governor _governor = new Governor(address(_timelock), address(_token), _governorGuardian); 51 | require(address(_governor) == govAddr, "Governor deployed under an unexpected address"); 52 | 53 | _token.transfer(_monadicAddr, MONADIC_ALLOCATION); 54 | _token.transfer(_foundationAddr, FOUNDATION_ALLOCATION); 55 | _token.transfer(address(_timelock), TREASURY_ALLOCATION); 56 | require(_token.balanceOf(address(this)) == 0, "All tokens are allocated"); 57 | require( 58 | MONADIC_ALLOCATION + FOUNDATION_ALLOCATION + TREASURY_ALLOCATION == 59 | _token.totalSupply(), 60 | "All tokens are allocated" 61 | ); 62 | 63 | Registrar _registrar = 64 | new Registrar( 65 | _ens, 66 | RadicleTokenI(address(_token)), 67 | address(_timelock), 68 | 10, 69 | _namehash, 70 | uint256(keccak256(bytes(_label))) 71 | ); 72 | 73 | token = _token; 74 | timelock = _timelock; 75 | governor = _governor; 76 | registrar = _registrar; 77 | 78 | monadicAddr = _monadicAddr; 79 | foundationAddr = _foundationAddr; 80 | timelockDelay = _timelockDelay; 81 | governorGuardian = _governorGuardian; 82 | ens = _ens; 83 | namehash = _namehash; 84 | label = _label; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /contracts/libraries/ProxyDeltas.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | /// @notice A list of cycles and their deltas of amounts received by a proxy. 5 | /// For each cycle there are stored two deltas, one for the cycle itself 6 | /// and one for the cycle right after it. 7 | /// It reduces storage access for some usage scenarios. 8 | /// The cycle is described by its own entry and an entry for the previous cycle. 9 | /// Iterable and with random access. 10 | struct ProxyDeltas { 11 | mapping(uint64 => ProxyDeltasImpl.ProxyDeltaStored) data; 12 | } 13 | 14 | /// @notice Helper methods for proxy deltas list. 15 | /// The list works optimally if after applying a series of changes it's iterated over. 16 | /// The list uses 2 words of storage per stored cycle. 17 | library ProxyDeltasImpl { 18 | using ProxyDeltasImpl for ProxyDeltas; 19 | 20 | struct ProxyDeltaStored { 21 | uint64 next; 22 | int128 thisCycleDelta; 23 | bool isAttached; 24 | // Unused. Hints the compiler that it has full control over the content 25 | // of the whole storage slot and allows it to optimize more aggressively. 26 | uint56 slotFiller1; 27 | // --- SLOT BOUNDARY 28 | int128 nextCycleDelta; 29 | // Unused. Hints the compiler that it has full control over the content 30 | // of the whole storage slot and allows it to optimize more aggressively. 31 | uint128 slotFiller2; 32 | } 33 | 34 | uint64 internal constant CYCLE_ROOT = 0; 35 | 36 | /// @notice Return the next non-zero, non-obsolete delta and its cycle. 37 | /// The order is undefined, it may or may not be chronological. 38 | /// Prunes all the fully zeroed or obsolete items found between the current and the next cycle. 39 | /// Iterating over the whole list prunes all the zeroed and obsolete items. 40 | /// @param prevCycle The previously returned `cycle` or CYCLE_ROOT to start iterating 41 | /// @param prevCycleHint The previously returned `cycleHint` or CYCLE_ROOT to start iterating 42 | /// @param finishedCycle The last finished cycle. 43 | /// Entries describing cycles before `finishedCycle` are considered obsolete. 44 | /// @return cycle The next iterated cycle or CYCLE_ROOT if the end of the list was reached. 45 | /// @return cycleHint A value passed as `prevCycleHint` on the next call 46 | /// @return thisCycleDelta The receiver delta applied for the `next` cycle. 47 | /// May be zero if `nextCycleDelta` is non-zero 48 | /// @return nextCycleDelta The receiver delta applied for the cycle after the `next` cycle. 49 | /// May be zero if `thisCycleDelta` is non-zero 50 | function nextDeltaPruning( 51 | ProxyDeltas storage self, 52 | uint64 prevCycle, 53 | uint64 prevCycleHint, 54 | uint64 finishedCycle 55 | ) 56 | internal 57 | returns ( 58 | uint64 cycle, 59 | uint64 cycleHint, 60 | int128 thisCycleDelta, 61 | int128 nextCycleDelta 62 | ) 63 | { 64 | if (prevCycle == CYCLE_ROOT) prevCycleHint = self.data[CYCLE_ROOT].next; 65 | cycle = prevCycleHint; 66 | while (cycle != CYCLE_ROOT) { 67 | thisCycleDelta = self.data[cycle].thisCycleDelta; 68 | nextCycleDelta = self.data[cycle].nextCycleDelta; 69 | cycleHint = self.data[cycle].next; 70 | if ((thisCycleDelta != 0 || nextCycleDelta != 0) && cycle >= finishedCycle) break; 71 | delete self.data[cycle]; 72 | cycle = cycleHint; 73 | } 74 | if (cycle != prevCycleHint) self.data[prevCycle].next = cycle; 75 | } 76 | 77 | /// @notice Add value to the delta for a specific cycle. 78 | /// @param cycle The cycle for which deltas are modified. 79 | /// @param thisCycleDeltaAdded The value added to the delta for `cycle` 80 | /// @param nextCycleDeltaAdded The value added to the delta for the cycle after `cycle` 81 | function addToDelta( 82 | ProxyDeltas storage self, 83 | uint64 cycle, 84 | int128 thisCycleDeltaAdded, 85 | int128 nextCycleDeltaAdded 86 | ) internal { 87 | self.attachToList(cycle); 88 | self.data[cycle].thisCycleDelta += thisCycleDeltaAdded; 89 | self.data[cycle].nextCycleDelta += nextCycleDeltaAdded; 90 | } 91 | 92 | /// @notice Ensures that the delta for a specific cycle is attached to the list 93 | /// @param cycle The cycle for which delta should be attached 94 | function attachToList(ProxyDeltas storage self, uint64 cycle) internal { 95 | require(cycle != CYCLE_ROOT && cycle != type(uint64).max, "Invalid cycle number"); 96 | if (!self.data[cycle].isAttached) { 97 | uint64 rootNext = self.data[CYCLE_ROOT].next; 98 | self.data[CYCLE_ROOT].next = cycle; 99 | self.data[cycle].next = rootNext; 100 | self.data[cycle].isAttached = true; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /contracts/libraries/ReceiverWeights.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | /// @notice A list of receivers to their weights, iterable and with random access 5 | struct ReceiverWeights { 6 | mapping(address => ReceiverWeightsImpl.ReceiverWeightStored) data; 7 | } 8 | 9 | /// @notice Helper methods for receiver weights list. 10 | /// The list works optimally if after applying a series of changes it's iterated over. 11 | /// The list uses 1 word of storage per receiver with a non-zero weight. 12 | library ReceiverWeightsImpl { 13 | using ReceiverWeightsImpl for ReceiverWeights; 14 | 15 | struct ReceiverWeightStored { 16 | address next; 17 | uint32 weightReceiver; 18 | uint32 weightProxy; 19 | bool isAttached; 20 | // Unused. Hints the compiler that it has full control over the content 21 | // of the whole storage slot and allows it to optimize more aggressively. 22 | uint24 slotFiller; 23 | } 24 | 25 | address internal constant ADDR_ROOT = address(0); 26 | 27 | /// @notice Return the next non-zero receiver or proxy weight and its address. 28 | /// Removes all the items that have zero receiver and proxy weights found 29 | /// between the current and the next item from the list. 30 | /// Iterating over the whole list prunes all the zeroed items. 31 | /// @param prevReceiver The previously returned `receiver` or ADDR_ROOT to start iterating 32 | /// @param prevReceiverHint The previously returned `receiverHint` 33 | /// or ADDR_ROOT to start iterating 34 | /// @return receiver The receiver address, ADDR_ROOT if the end of the list was reached 35 | /// @return receiverHint A value passed as `prevReceiverHint` on the next call 36 | /// @return weightReceiver The receiver weight, may be zero if `weightProxy` is non-zero 37 | /// @return weightProxy The proxy weight, may be zero if `weightReceiver` is non-zero 38 | function nextWeightPruning( 39 | ReceiverWeights storage self, 40 | address prevReceiver, 41 | address prevReceiverHint 42 | ) 43 | internal 44 | returns ( 45 | address receiver, 46 | address receiverHint, 47 | uint32 weightReceiver, 48 | uint32 weightProxy 49 | ) 50 | { 51 | if (prevReceiver == ADDR_ROOT) prevReceiverHint = self.data[ADDR_ROOT].next; 52 | receiver = prevReceiverHint; 53 | while (receiver != ADDR_ROOT) { 54 | weightReceiver = self.data[receiver].weightReceiver; 55 | weightProxy = self.data[receiver].weightProxy; 56 | receiverHint = self.data[receiver].next; 57 | if (weightReceiver != 0 || weightProxy != 0) break; 58 | delete self.data[receiver]; 59 | receiver = receiverHint; 60 | } 61 | if (receiver != prevReceiverHint) self.data[prevReceiver].next = receiver; 62 | } 63 | 64 | /// @notice Return the next non-zero receiver or proxy weight and its address 65 | /// @param prevReceiver The previously returned `receiver` or ADDR_ROOT to start iterating 66 | /// @param prevReceiverHint The previously returned `receiverHint` 67 | /// or ADDR_ROOT to start iterating 68 | /// @return receiver The receiver address, ADDR_ROOT if the end of the list was reached 69 | /// @return receiverHint A value passed as `prevReceiverHint` on the next call 70 | /// @return weightReceiver The receiver weight, may be zero if `weightProxy` is non-zero 71 | /// @return weightProxy The proxy weight, may be zero if `weightReceiver` is non-zero 72 | function nextWeight( 73 | ReceiverWeights storage self, 74 | address prevReceiver, 75 | address prevReceiverHint 76 | ) 77 | internal 78 | view 79 | returns ( 80 | address receiver, 81 | address receiverHint, 82 | uint32 weightReceiver, 83 | uint32 weightProxy 84 | ) 85 | { 86 | receiver = (prevReceiver == ADDR_ROOT) ? self.data[ADDR_ROOT].next : prevReceiverHint; 87 | while (receiver != ADDR_ROOT) { 88 | weightReceiver = self.data[receiver].weightReceiver; 89 | weightProxy = self.data[receiver].weightProxy; 90 | receiverHint = self.data[receiver].next; 91 | if (weightReceiver != 0 || weightProxy != 0) break; 92 | receiver = receiverHint; 93 | } 94 | } 95 | 96 | /// @notice Checks if the list is fully zeroed and takes no storage space. 97 | /// It means that either it was never used or that 98 | /// it's been pruned after removal of all the elements. 99 | /// @return True if the list is zeroed 100 | function isZeroed(ReceiverWeights storage self) internal view returns (bool) { 101 | return self.data[ADDR_ROOT].next == ADDR_ROOT; 102 | } 103 | 104 | /// @notice Set weight for a specific receiver 105 | /// @param receiver The receiver to set weight 106 | /// @param weight The weight to set 107 | /// @return previousWeight The previously set weight, may be zero 108 | function setReceiverWeight( 109 | ReceiverWeights storage self, 110 | address receiver, 111 | uint32 weight 112 | ) internal returns (uint32 previousWeight) { 113 | self.attachToList(receiver); 114 | previousWeight = self.data[receiver].weightReceiver; 115 | self.data[receiver].weightReceiver = weight; 116 | } 117 | 118 | /// @notice Set weight for a specific proxy 119 | /// @param proxy The proxy to set weight 120 | /// @param weight The weight to set 121 | /// @return previousWeight The previously set weight, may be zero 122 | function setProxyWeight( 123 | ReceiverWeights storage self, 124 | address proxy, 125 | uint32 weight 126 | ) internal returns (uint32 previousWeight) { 127 | self.attachToList(proxy); 128 | previousWeight = self.data[proxy].weightProxy; 129 | self.data[proxy].weightProxy = weight; 130 | } 131 | 132 | /// @notice Ensures that the weight for a specific receiver is attached to the list 133 | /// @param receiver The receiver whose weight should be attached 134 | function attachToList(ReceiverWeights storage self, address receiver) internal { 135 | require(receiver != ADDR_ROOT, "Invalid receiver address"); 136 | if (!self.data[receiver].isAttached) { 137 | address rootNext = self.data[ADDR_ROOT].next; 138 | self.data[ADDR_ROOT].next = receiver; 139 | self.data[receiver].next = rootNext; 140 | self.data[receiver].isAttached = true; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /contracts/libraries/SafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.7.5; 3 | 4 | // a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math) 5 | 6 | library SafeMath { 7 | function add(uint256 x, uint256 y) internal pure returns (uint256 z) { 8 | require((z = x + y) >= x, "ds-math-add-overflow"); 9 | } 10 | 11 | function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { 12 | require((z = x - y) <= x, "ds-math-sub-underflow"); 13 | } 14 | 15 | function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { 16 | require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/how_the_pool_works.md: -------------------------------------------------------------------------------- 1 | This document is an introduction to understanding how the funding pool contract works. 2 | It doesn't describe all the implementation details or the API, 3 | it focuses on the basic principles of the core mechanics. 4 | 5 | # Overview 6 | 7 | The funding pool is a smart contract which creates real-time streams of donations. 8 | One can start, alter or end the process of sending their funds at any time with immediate effect. 9 | The flow of funds is automatically maintained and steady over time. 10 | 11 | There are 3 roles present in the contract. 12 | Any Ethereum address can simultaneously take on any of these roles. 13 | 14 | - **The sender**: has assets and chooses who do they want to send them to, 15 | how much, and at what rate 16 | - **The receiver:** receives funds from senders 17 | - **The proxy**: receives funds from senders, 18 | but immediately passes them to receivers of their choice 19 | 20 | ## The cycles 21 | 22 | The whole blockchain history is divided into cycles of equal duration 23 | so that every block is assigned to a cycle based on its timestamp. 24 | Cycles are numbered starting with zero. 25 | In the examples below, we assume that the cycle length is 5 seconds. 26 | 27 | ![](how_the_pool_works_1.png) 28 | 29 | # The sender 30 | 31 | The sender has a balance, a funding rate, and a set of receivers. 32 | 33 | The balance is automatically reduced by the funding rate every second 34 | and the same amount is credited to the sender's receivers. 35 | When the sender's balance reaches an amount lower than the per-second funding rate, 36 | the funding is stopped. 37 | This process doesn't actually require updates every second, 38 | its effects are calculated on the fly whenever they are needed. 39 | Thus the contract state is updated only when the funding parameters are altered by the users. 40 | 41 | The sender balance is manually increased by topping up, 42 | which requires sending the assets from the user wallet to the contract. 43 | The opposite operation is withdrawal, 44 | which results in sending the assets from the contract back to the user wallet. 45 | 46 | This funding rate stays constant over time unless explicitly updated. 47 | 48 | The sender maintains a list of receivers, each of them with a weight. 49 | The weights regulate how the funded amount is split between the receivers. 50 | For example, a receiver with weight 2 is going to get a share twice as big 51 | as a receiver with weight 1, but only half as big as another receiver with weight 4. 52 | 53 | ## The deltas 54 | 55 | Every second funds from the sender’s pool account are credited to the sender’s receivers 56 | according to the funding rate. 57 | The receiver can collect funds sent in a given second only when the cycle containing it is over. 58 | 59 | ![](how_the_pool_works_2.png) 60 | 61 | Here, we see the timeline of a receiver who is receiving funds from two senders. 62 | Each of the senders has sent different amounts over different periods of time. 63 | At the end of each cycle, the collectable amount was increased by the total sent amount. 64 | 65 | The receiver needs to know, how much was sent to them on each cycle. 66 | For every receiver we store the amount of funds received in the last collected cycle. 67 | In addition we store changes to this value for the following cycles. 68 | This allows us to calculate the funds that the receiver receives in each not yet collected cycle. 69 | 70 | ![](how_the_pool_works_3.png) 71 | 72 | In this example, we start with having the raw **collectable** value of 23 for every second until 73 | the end of time. 74 | Next, we reduce that to storing values **added** to the collectable amount on each cycle. 75 | Now we need to describe only cycles when receiving anything. 76 | The senders usually are sending constant per-cycle amounts over long periods of time, so 77 | the added values tend to create long series of constant numbers, in this case, 5s. 78 | We exploit that and finally turn them into **deltas** relative to the previous cycles. 79 | Now we need to store data only for cycles where the funding rate changes, it's very cheap. 80 | This is what the contract actually stores: a mapping from cycle numbers to deltas. 81 | 82 | ## Starting sending 83 | 84 | In order to start sending, the sender needs to have a non-zero funding rate, 85 | a balance of at least the said funding rate and a non-empty list of receivers. 86 | As soon as the sender is updated to match these criteria, the flow of assets starts. 87 | First, the funding period is calculated. 88 | Its start is the current block timestamp and its end is the moment on which the balance will run out. 89 | Next, for each receiver, the weighted share of the funding rate is calculated. 90 | The receiver's deltas are updated to reflect that during the whole sending period every second 91 | it's going to receive the calculated amount. 92 | 93 | Let's take a look at an example of an application of a delta. 94 | The sender will be sending 1 per second or 5 per cycle. 95 | 96 | ![](how_the_pool_works_4.png) 97 | 98 | The deltas are applied relative to the existing values. 99 | It doesn't matter if anybody else is funding the receiver, it won't affect this sender. 100 | 101 | Another important point is that the delta changes are usually split between two cycles. 102 | This reflects that the first cycle is only partially affected by the change in funding. 103 | Only the second one is fully affected and it must apply the rest of the delta. 104 | 105 | In this case, the total change of the per-cycle delta is +5 to start sending. 106 | The current cycle isn't fully affected though, only 2 out of 5 seconds are sending. 107 | It's effectively going to transfer only the amount of 2, which is reflected in the +2 delta change. 108 | On the other hand, the next cycle and the ones after it are going to transfer the full 5. 109 | This is expressed with the +3 delta change, which turns 2 per cycle into the full 5 per cycle. 110 | 111 | A similar logic is applied to express the end of the funding period. 112 | The cycle in which funding runs out has 1 transferring second resulting in delta being -4. 113 | The following cycle doesn't transfer any funds and has delta -1 to complete zeroing of the rate. 114 | 115 | ## Stopping sending 116 | 117 | When funding is stopped, the deltas need to be reverted. 118 | To do that basically the same process is applied, just with negative deltas. 119 | Because the already sent funds are out of the sender's control, the past deltas must stay untouched 120 | and only the effects on the receiver's future must be erased. 121 | 122 | In this case, the reverting is split into 2 cycles too, one with -4 and the other with -1. 123 | 124 | Let's assume that a few seconds have passed, but the sender wants to stop sending. 125 | This can happen because the sender doesn't want to fund the receiver anymore 126 | or because they want to change some of its configuration. 127 | In the latter case sending is stopped only to be immediately resumed, but with different parameters. 128 | Either way, the effects of the sender on the receiver's deltas need to be reverted 129 | from the current timestamp to the end of the existing funding period. 130 | 131 | ![](how_the_pool_works_5.png) 132 | 133 | The old funding end deltas are reverted because they don't reflect the real funding end anymore. 134 | On the other hand, a new end is applied to the current timestamp, 135 | just as if it was always supposed to be the end of the funding period. 136 | Now the receiver's future isn't affected by the sender anymore. 137 | The past stays untouched because the already sent funds are out of the sender's control. 138 | 139 | # The receiver 140 | 141 | There are no setup steps for one to become a receiver. 142 | Any address can receive donations at any time, from any sender. 143 | The only function of this role is the collection of funds sent by others. 144 | 145 | ## Collecting 146 | 147 | The receiver can at any time collect the funds sent to it. 148 | The contract calculates the total amount and then transfers it out to the receiver's wallet. 149 | The collected amount is always everything available at a given moment, there's no way to limit it. 150 | 151 | As shown in the previous sections, the collectable amount is described with deltas, one per cycle. 152 | The receiver stores the number of the first cycle, for which the funds haven't been collected yet. 153 | This assures that funds can be collected only once. 154 | The receiver also stores the amount, which was collected for the last collected cycle. 155 | This value is set to 0 if this is the first collection of the receiver. 156 | It's the initial value to which the deltas are added. 157 | 158 | To calculate, how much the receiver can collect, 159 | the contract iterates over all the completed cycles since the first uncollected one. 160 | For each of them, it adds the corresponding delta to the value collected from the previous cycle. 161 | This reconstructs the amount sent from all the senders during each cycle. 162 | These amounts are then added up and the result is the total collected amount. 163 | Finally, the next uncollected cycle number and the last cycle amount are updated. 164 | 165 | ![](how_the_pool_works_6.png) 166 | 167 | In this example funds received from 4 cycles are being collected. 168 | The yellow fields are the stored state before the collection, green after it. 169 | The blue field is the collected value, which is going to be transferred to the sender's wallet. 170 | 171 | # The proxy 172 | 173 | Multiple senders can send funds to a proxy and the owner 174 | controls how these funds are further distributed to receivers. 175 | 176 | The proxy is configured only with a list of receivers with an associated weight. 177 | The sum of the receivers' weights must always be a constant value, 178 | which is defined in the contract and it's the same for all the proxies. 179 | A proxy, which has never been configured has no receivers and it's impossible to send funds via it. 180 | After the first configuration, it's impossible to disable the proxy, it's forever active. 181 | It can be reconfigured, but it must preserve the constant receivers' weights sum. 182 | 183 | Just like a receiver, the proxy has a mapping between the cycles and the deltas of received amounts. 184 | 185 | ## Sending via a proxy 186 | 187 | When a sender starts sending funds to a proxy, it does so in two steps. 188 | First it applies changes to the proxy's deltas similarly to how it would do with a receiver. 189 | Next, it iterates over all the proxy's receivers and applies changes to 190 | their deltas as if they were directly funded by the sender. 191 | The funding rate applied to the receivers is split according to their weights in the proxy. 192 | 193 | For the example sake let's assume that the delta's proxy weights sum must be equal to 5. 194 | The proxy has 2 receivers: A with weight 3 and B with weight 2. 195 | The sender wants to start sending via the proxy 2 per second or 10 per cycle. 196 | It's 2 per cycle per proxy weight. 197 | 198 | ![](how_the_pool_works_7.png) 199 | 200 | The proxy's deltas store amount per 1 proxy weight, which is 2. 201 | The receivers get their shares, A with weight 3 gets 6 and B with weight 2 gets 4 per cycle. 202 | 203 | When a sender stops sending, the process is reversed like with regular receivers. 204 | All the deltas are once again applied, but with negative values. 205 | 206 | ## Updating a proxy 207 | 208 | When the list of proxy receivers is updated, all funding must be moved to a new set of receivers. 209 | That's when the proxy's deltas come useful. 210 | For each cycle and each receiver, the proxy can tell the total delta it has applied. 211 | It can then use this information to erase its future contributions from its receivers. 212 | 213 | ![](how_the_pool_works_8.png) 214 | 215 | In this example, the receiver's weight is 3. 216 | To erase the future contribution, the proxy's deltas are multiplied by 217 | the receiver's weights and subtracted from the corresponding receiver's deltas. 218 | 219 | After removing its contributions from one set of receivers the proxy must reapply them on a new set. 220 | This is done in the same way as removal, but this time the deltas are added and not subtracted. 221 | 222 | ### The current cycle problem 223 | 224 | Unlike the senders, the proxies store data with a per-cycle precision. 225 | When changing the set of receivers, a delta describing the current cycle may need to be applied. 226 | When it happens, it's unclear what part of the per-cycle delta should be moved, 227 | because some funds were sent before the current timestamp and some will be sent after it. 228 | 229 | ![](how_the_pool_works_9.png) 230 | 231 | The solution is to ignore the problem and move the whole current cycle delta. 232 | Some funds already sent in the current cycle may disappear from one receiver and appear in another. 233 | Such behavior, however, is not of significant importance since 234 | the receivers have no access to funds coming from an unfinished cycle. 235 | The senders aren't strongly affected either, they already sent these funds and they trust the proxy. 236 | -------------------------------------------------------------------------------- /docs/how_the_pool_works.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works.odg -------------------------------------------------------------------------------- /docs/how_the_pool_works_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_1.png -------------------------------------------------------------------------------- /docs/how_the_pool_works_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_2.png -------------------------------------------------------------------------------- /docs/how_the_pool_works_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_3.png -------------------------------------------------------------------------------- /docs/how_the_pool_works_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_4.png -------------------------------------------------------------------------------- /docs/how_the_pool_works_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_5.png -------------------------------------------------------------------------------- /docs/how_the_pool_works_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_6.png -------------------------------------------------------------------------------- /docs/how_the_pool_works_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_7.png -------------------------------------------------------------------------------- /docs/how_the_pool_works_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_8.png -------------------------------------------------------------------------------- /docs/how_the_pool_works_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-contracts/3d333e591bba079fdd76de9905e286ee273e95e8/docs/how_the_pool_works_9.png -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { task } from "hardhat/config"; 3 | import { 4 | TASK_COMPILE, 5 | TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT, 6 | } from "hardhat/builtin-tasks/task-names"; 7 | import { runTypeChain, glob } from "typechain"; 8 | import "@nomiclabs/hardhat-ethers"; 9 | 10 | // You have to export an object to set up your config 11 | // This object can have the following optional entries: 12 | // defaultNetwork, networks, solc, and paths. 13 | // Go to https://hardhat.org/config/ to learn more 14 | export default { 15 | // This is a sample solc configuration that specifies which version of solc to use 16 | solidity: { 17 | version: "0.7.6", 18 | settings: { 19 | optimizer: { 20 | enabled: true, 21 | runs: 200, 22 | }, 23 | }, 24 | }, 25 | }; 26 | 27 | // Additional contracts to generate TypeScript bindings for. 28 | // Only contracts never used in .sol files should be listed here to avoid conflicts. 29 | const contracts = [ 30 | "node_modules/@ensdomains/ens/build/contracts/ENSRegistry.json", 31 | "ethregistrar/build/contracts/BaseRegistrar.json", 32 | "ethregistrar/build/contracts/BaseRegistrarImplementation.json", 33 | ]; 34 | 35 | task(TASK_COMPILE).setAction(async (_, runtime, runSuper) => { 36 | await runSuper(); 37 | const artifacts = await runtime.artifacts.getArtifactPaths(); 38 | artifacts.push(...contracts.map((contract) => path.resolve(contract))); 39 | const artifactsGlob = "{" + artifacts.join(",") + "}"; 40 | await typeChain(artifactsGlob, "."); 41 | console.log(`Successfully generated Typechain artifacts!`); 42 | }); 43 | 44 | task(TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT).setAction(async (_, __, runSuper) => { 45 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 46 | const input = await runSuper(); 47 | // eslint-disable-next-line 48 | input.settings.outputSelection["*"]["*"].push("storageLayout"); 49 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 50 | return input; 51 | }); 52 | 53 | async function typeChain(filesGlob: string, modulePath: string): Promise { 54 | const outDir = "./contract-bindings"; 55 | const cwd = process.cwd(); 56 | const allFiles = glob(cwd, [filesGlob]); 57 | await runTypeChain({ 58 | cwd, 59 | filesToProcess: allFiles, 60 | allFiles, 61 | outDir: path.join(outDir, "ethers", modulePath), 62 | target: "ethers-v5", 63 | }); 64 | await runTypeChain({ 65 | cwd, 66 | filesToProcess: allFiles, 67 | allFiles, 68 | outDir: path.join(outDir, "web3", modulePath), 69 | target: "web3-v1", 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radicle-contracts", 3 | "version": "0.1.0", 4 | "license": "GPL-3.0-only", 5 | "main": "build/src/index.js", 6 | "types": "build/src/index.d.ts", 7 | "dependencies": { 8 | "@ethersproject/abi": "^5.1.2", 9 | "@ethersproject/bytes": "^5.1.0", 10 | "@ethersproject/providers": "^5.1.2", 11 | "ethers": "^5.1.4" 12 | }, 13 | "devDependencies": { 14 | "@ensdomains/ens": "^0.5.0", 15 | "@nomiclabs/hardhat-ethers": "^2.0.1", 16 | "@openzeppelin/contracts": "^3.4.1-solc-0.7", 17 | "@typechain/ethers-v5": "^7.0.0", 18 | "@typechain/web3-v1": "^3.0.0", 19 | "@types/chai": "^4.2.15", 20 | "@types/mocha": "^8.2.1", 21 | "@types/readline-sync": "^1.4.3", 22 | "@typescript-eslint/eslint-plugin": "^4.16.1", 23 | "@typescript-eslint/parser": "^4.16.1", 24 | "chai": "^4.3.3", 25 | "eslint": "^7.21.0", 26 | "fast-glob": "^3.2.7", 27 | "hardhat": "^2.1.1", 28 | "prettier": "^2.2.1", 29 | "prettier-plugin-solidity": "^1.0.0-beta.5", 30 | "readline-sync": "^1.4.10", 31 | "solhint": "^3.3.3", 32 | "ts-generator": "^0.1.1", 33 | "ts-node": "^9.1.1", 34 | "typechain": "^5.0.0", 35 | "typescript": "^4.2.3", 36 | "web3": "^1.3.5" 37 | }, 38 | "scripts": { 39 | "build": "hardhat compile && tsc && ts-node scripts/copy-contract-declaration-files.ts", 40 | "deploy:claims": "ts-node -e 'require(\"./src/deploy-to-network.ts\").claims();'", 41 | "deploy:claimsV2": "ts-node -e 'require(\"./src/deploy-to-network.ts\").claimsV2();'", 42 | "deploy:ethFundingPool": "ts-node -e 'require(\"./src/deploy-to-network.ts\").ethFundingPool();'", 43 | "deploy:erc20FundingPool": "ts-node -e 'require(\"./src/deploy-to-network.ts\").erc20FundingPool();'", 44 | "deploy:daiFundingPool": "ts-node -e 'require(\"./src/deploy-to-network.ts\").daiFundingPool();'", 45 | "deploy:testEns": "ts-node -e 'require(\"./src/deploy-to-network.ts\").testEns();'", 46 | "deploy:phase0": "ts-node -e 'require(\"./src/deploy-to-network.ts\").phase0();'", 47 | "deploy:vestingTokens": "ts-node -e 'require(\"./src/deploy-to-network.ts\").vestingTokens();'", 48 | "deploy:playground": "ts-node -e 'require(\"./src/deploy-to-network.ts\").playground();'", 49 | "prepare": "yarn prepack", 50 | "prepack": "yarn build", 51 | "test": "hardhat test", 52 | "lint": "yarn run lint:prettier:check && yarn run lint:solhint && yarn run lint:eslint", 53 | "lint:solhint": "solhint --max-warnings=0 $(git ls-files | grep -E '\\.sol$')", 54 | "lint:eslint": "eslint . --max-warnings=0", 55 | "lint:prettier": "prettier $(git ls-files | grep -E '\\.(sol|ts|js)$')", 56 | "lint:prettier:check": "yarn lint:prettier --check", 57 | "lint:prettier:write": "yarn lint:prettier --write" 58 | }, 59 | "files": [ 60 | "src/**", 61 | "build/**", 62 | "contract-bindings/**" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /scripts/copy-contract-declaration-files.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { promises as fs } from "fs"; 3 | import fastGlob from "fast-glob"; 4 | 5 | // Copies all `.d.ts` files from `contract-bindings` to the build 6 | // directory so that dependents pick them up properly. 7 | // 8 | // See https://github.com/ethereum-ts/TypeChain/issues/430 9 | 10 | async function main(): Promise { 11 | const projectRoot = path.resolve(__dirname, ".."); 12 | const declarationFiles = await fastGlob(["contract-bindings/ethers/**/*.d.ts"], { 13 | cwd: projectRoot, 14 | }); 15 | for (const file of declarationFiles) { 16 | await fs.copyFile(path.resolve(projectRoot, file), path.resolve(projectRoot, "build", file)); 17 | } 18 | } 19 | 20 | main().catch((err) => { 21 | throw err; 22 | }); 23 | -------------------------------------------------------------------------------- /src/deploy-to-network.ts: -------------------------------------------------------------------------------- 1 | import { 2 | deployClaims, 3 | deployClaimsV2, 4 | deployDaiPool, 5 | deployErc20Pool, 6 | deployEthPool, 7 | deployTestEns, 8 | deployVestingToken, 9 | deployPhase0, 10 | } from "./deploy"; 11 | import { BigNumber, Contract, Wallet, Signer, providers, utils } from "ethers"; 12 | import SigningKey = utils.SigningKey; 13 | import { keyInSelect, keyInYNStrict, question } from "readline-sync"; 14 | import { ERC20__factory } from "../contract-bindings/ethers"; 15 | 16 | const INFURA_ID = "de5e2a8780c04964950e73b696d1bfb1"; 17 | 18 | export async function testEns(): Promise { 19 | console.log("The deployer will become an owner of the '', 'eth' and '.eth' domains,"); 20 | console.log("the owner of the root ENS and the owner and controller of the 'eth' registrar"); 21 | const signer = await connectPrivateKeySigner(); 22 | const label = askFor("an 'eth' subdomain to register"); 23 | await deploy("ENS", () => deployTestEns(signer, label)); 24 | } 25 | 26 | export async function phase0(): Promise { 27 | const signer = await connectPrivateKeySigner(); 28 | const governorGuardian = askForAddress("of the governor guardian"); 29 | const monadicAddr = askForAddress("of Monadic"); 30 | const foundationAddr = askForAddress("of the Foundation"); 31 | const ensAddr = askForAddress("of the ENS"); 32 | const ethLabel = askFor("an 'eth' subdomain on which the registrar should operate"); 33 | const timelockDelay = 60 * 60 * 24 * 2; 34 | 35 | const phase0 = await deploy("phase0", () => 36 | deployPhase0( 37 | signer, 38 | monadicAddr, 39 | foundationAddr, 40 | timelockDelay, 41 | governorGuardian, 42 | ensAddr, 43 | ethLabel 44 | ) 45 | ); 46 | 47 | printDeployed("Radicle Token", await phase0.token()); 48 | printDeployed("Timelock", await phase0.timelock()); 49 | printDeployed("Governor", await phase0.governor()); 50 | printDeployed("Registrar", await phase0.registrar()); 51 | console.log(`Remember to give the '${ethLabel}.eth' domain to the registrar`); 52 | } 53 | 54 | export async function vestingTokens(): Promise { 55 | console.log("The deployer will be the one providing tokens for vesting"); 56 | const signer = await connectPrivateKeySigner(); 57 | const tokenAddr = askForAddress("of the Radicle token contract"); 58 | const token = ERC20__factory.connect(tokenAddr, signer); 59 | const decimals = await token.decimals(); 60 | const symbol = await token.symbol(); 61 | const owner = askForAddress("of the vesting contracts admin"); 62 | const vestingPeriod = askForDaysInSeconds("the vesting period"); 63 | const cliffPeriod = askForDaysInSeconds("the cliff period"); 64 | do { 65 | const beneficiary = askForAddress("of beneficiary"); 66 | const amount = askForAmount("to vest", decimals, symbol); 67 | const vestingStartTime = askForTimestamp("of the vesting start"); 68 | await deploy("vesting tokens", () => 69 | deployVestingToken( 70 | signer, 71 | tokenAddr, 72 | owner, 73 | beneficiary, 74 | amount, 75 | vestingStartTime, 76 | vestingPeriod, 77 | cliffPeriod 78 | ) 79 | ); 80 | console.log(beneficiary, "has", amount.toString(), "tokens vesting"); 81 | } while (askYesNo("Create another vesting?")); 82 | } 83 | 84 | export async function ethFundingPool(): Promise { 85 | const signer = await connectPrivateKeySigner(); 86 | const cycleSecs = askForNumber("the length of the funding cycle in seconds"); 87 | await deploy("funding pool", () => deployEthPool(signer, cycleSecs)); 88 | } 89 | 90 | export async function erc20FundingPool(): Promise { 91 | const signer = await connectPrivateKeySigner(); 92 | const tokenAddr = askForAddress("of the ERC-20 token to used in the funding pool"); 93 | const cycleSecs = askForNumber("the length of the funding cycle in seconds"); 94 | await deploy("funding pool", () => deployErc20Pool(signer, cycleSecs, tokenAddr)); 95 | } 96 | 97 | export async function daiFundingPool(): Promise { 98 | const signer = await connectPrivateKeySigner(); 99 | const tokenAddr = askForAddress("of the DAI token to used in the funding pool"); 100 | const cycleSecs = askForNumber("the length of the funding cycle in seconds"); 101 | await deploy("funding pool", () => deployDaiPool(signer, cycleSecs, tokenAddr)); 102 | } 103 | 104 | export async function claims(): Promise { 105 | const signer = await connectPrivateKeySigner(); 106 | await deploy("claims", () => deployClaims(signer)); 107 | } 108 | 109 | export async function claimsV2(): Promise { 110 | const signer = await connectPrivateKeySigner(); 111 | await deploy("claimsV2", () => deployClaimsV2(signer)); 112 | } 113 | 114 | async function connectPrivateKeySigner(): Promise { 115 | const signingKey = askForSigningKey("to sign all the transactions"); 116 | const network = askForNetwork("to connect to"); 117 | const provider = new providers.InfuraProvider(network, INFURA_ID); 118 | const wallet = new Wallet(signingKey, provider); 119 | const networkName = (await wallet.provider.getNetwork()).name; 120 | console.log("Connected to", networkName, "using account", wallet.address); 121 | 122 | const defaultGasPrice = await provider.getGasPrice(); 123 | const gasPrice = askForGasPrice("to use in all transactions", defaultGasPrice); 124 | provider.getGasPrice = function (): Promise { 125 | return Promise.resolve(gasPrice); 126 | }; 127 | // eslint-disable-next-line @typescript-eslint/unbound-method 128 | const superSendTransaction = provider.sendTransaction; 129 | provider.sendTransaction = async (txBytes): Promise => { 130 | const tx = utils.parseTransaction(await txBytes); 131 | console.log("Sending transaction", tx.hash); 132 | return superSendTransaction.call(provider, txBytes); 133 | }; 134 | 135 | return wallet; 136 | } 137 | 138 | function askForSigningKey(keyUsage: string): SigningKey { 139 | for (;;) { 140 | let key = askFor("the private key " + keyUsage, undefined, true); 141 | if (!key.startsWith("0x")) { 142 | key = "0x" + key; 143 | } 144 | try { 145 | return new SigningKey(key); 146 | } catch (e) { 147 | printInvalidInput("private key"); 148 | } 149 | } 150 | } 151 | 152 | function askForNetwork(networkUsage: string): string { 153 | const networks = ["mainnet", "goerli"]; 154 | const query = "Enter the network " + networkUsage; 155 | const network = keyInSelect(networks, query, { cancel: false }); 156 | return networks[network]; 157 | } 158 | 159 | function askForGasPrice(gasUsage: string, defaultPrice: BigNumber): BigNumber { 160 | const giga = 10 ** 9; 161 | const question = "gas price " + gasUsage + " in GWei"; 162 | const defaultPriceGwei = (defaultPrice.toNumber() / giga).toString(); 163 | for (;;) { 164 | const priceStr = askFor(question, defaultPriceGwei); 165 | const price = parseFloat(priceStr); 166 | if (Number.isFinite(price) && price >= 0) { 167 | const priceWei = (price * giga).toFixed(); 168 | return BigNumber.from(priceWei); 169 | } 170 | printInvalidInput("amount"); 171 | } 172 | } 173 | 174 | function askForAddress(addressUsage: string): string { 175 | for (;;) { 176 | const address = askFor("the address " + addressUsage); 177 | if (utils.isAddress(address)) { 178 | return address; 179 | } 180 | printInvalidInput("address"); 181 | } 182 | } 183 | 184 | function askForAmount(amountUsage: string, decimals: number, symbol: string): BigNumber { 185 | const amount = askForBigNumber("amount " + amountUsage + " in " + symbol); 186 | return BigNumber.from(10).pow(decimals).mul(amount); 187 | } 188 | 189 | function askForBigNumber(numberUsage: string): BigNumber { 190 | for (;;) { 191 | const bigNumber = askFor(numberUsage); 192 | try { 193 | return BigNumber.from(bigNumber); 194 | } catch (e) { 195 | printInvalidInput("number"); 196 | } 197 | } 198 | } 199 | 200 | function askForNumber(numberUsage: string): number { 201 | for (;;) { 202 | const numStr = askFor(numberUsage); 203 | const num = parseInt(numStr); 204 | if (Number.isInteger(num)) { 205 | return num; 206 | } 207 | printInvalidInput("number"); 208 | } 209 | } 210 | 211 | function askForTimestamp(dateUsage: string): number { 212 | for (;;) { 213 | const dateStr = askFor( 214 | "the date " + 215 | dateUsage + 216 | " in the ISO-8601 format, e.g. 2020-01-21, the timezone is UTC if unspecified" 217 | ); 218 | try { 219 | const date = new Date(dateStr); 220 | return date.valueOf() / 1000; 221 | } catch (e) { 222 | printInvalidInput("date"); 223 | } 224 | } 225 | } 226 | 227 | function askForDaysInSeconds(daysUsage: string): number { 228 | const days = askForNumber(daysUsage + " in whole days"); 229 | return days * 24 * 60 * 60; 230 | } 231 | 232 | function askYesNo(query: string): boolean { 233 | return keyInYNStrict(query); 234 | } 235 | 236 | function askFor(query: string, defaultInput?: string, hideInput = false): string { 237 | const questionDefault = defaultInput === undefined ? "" : " (default: " + defaultInput + ")"; 238 | const options = { 239 | hideEchoBack: hideInput, 240 | limit: /./, 241 | limitMessage: "", 242 | defaultInput, 243 | }; 244 | return question("Enter " + query + questionDefault + ":\n", options); 245 | } 246 | 247 | function printInvalidInput(inputType: string): void { 248 | console.log("This is not a valid", inputType); 249 | } 250 | 251 | async function deploy(name: string, fn: () => Promise): Promise { 252 | for (;;) { 253 | try { 254 | console.log("Deploying", name, "contract"); 255 | const contract = await fn(); 256 | printDeployed(name, contract.address); 257 | return contract; 258 | } catch (e) { 259 | console.log(e); 260 | if (askYesNo("Retry?") == false) { 261 | throw "Deployment failed"; 262 | } 263 | } 264 | } 265 | } 266 | 267 | function printDeployed(name: string, address: string): void { 268 | console.log("Deployed", name, "contract", "under address", address); 269 | } 270 | -------------------------------------------------------------------------------- /src/deploy.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { 3 | constants, 4 | providers, 5 | utils, 6 | BigNumberish, 7 | BaseContract, 8 | ContractReceipt, 9 | Signer, 10 | } from "ethers"; 11 | import { Claims } from "../contract-bindings/ethers/Claims"; 12 | import { ClaimsV2 } from "../contract-bindings/ethers/ClaimsV2"; 13 | import { Dai } from "../contract-bindings/ethers/Dai"; 14 | import { DaiPool } from "../contract-bindings/ethers/DaiPool"; 15 | import { ENS } from "../contract-bindings/ethers/ENS"; 16 | import { EthPool } from "../contract-bindings/ethers/EthPool"; 17 | import { Governor } from "../contract-bindings/ethers/Governor"; 18 | import { Phase0 } from "../contract-bindings/ethers/Phase0"; 19 | import { RadicleToken } from "../contract-bindings/ethers/RadicleToken"; 20 | import { Registrar } from "../contract-bindings/ethers/Registrar"; 21 | import { Timelock } from "../contract-bindings/ethers/Timelock"; 22 | import { VestingToken } from "../contract-bindings/ethers/VestingToken"; 23 | import { 24 | BaseRegistrarImplementation__factory, 25 | Claims__factory, 26 | ClaimsV2__factory, 27 | Dai__factory, 28 | DaiPool__factory, 29 | ENSRegistry__factory, 30 | Erc20Pool__factory, 31 | Erc20Pool, 32 | EthPool__factory, 33 | Governor__factory, 34 | IERC20__factory, 35 | IERC721__factory, 36 | Phase0__factory, 37 | RadicleToken__factory, 38 | Registrar__factory, 39 | Timelock__factory, 40 | VestingToken__factory, 41 | } from "../contract-bindings/ethers"; 42 | import { labelHash } from "./ens"; 43 | 44 | export async function nextDeployedContractAddr( 45 | signer: Signer, 46 | afterTransactions: number 47 | ): Promise { 48 | return utils.getContractAddress({ 49 | from: await signer.getAddress(), 50 | nonce: (await signer.getTransactionCount()) + afterTransactions, 51 | }); 52 | } 53 | 54 | export interface DeployedContracts { 55 | gov: Governor; 56 | rad: RadicleToken; 57 | dai: Dai; 58 | registrar: Registrar; 59 | ens: ENS; 60 | ethPool: EthPool; 61 | erc20Pool: Erc20Pool; 62 | daiPool: DaiPool; 63 | claims: Claims; 64 | } 65 | 66 | export async function deployAll(signer: Signer): Promise { 67 | const signerAddr = await signer.getAddress(); 68 | const rad = await deployRadicleToken(signer, signerAddr); 69 | const dai = await deployTestDai(signer); 70 | const timelock = await deployTimelock(signer, signerAddr, 2 * 60 * 60 * 24); 71 | const gov = await deployGovernance(signer, timelock.address, rad.address, signerAddr); 72 | const label = "radicle"; 73 | const minCommitmentAge = 50; 74 | const ens = await deployTestEns(signer, label); 75 | const registrar = await deployRegistrar( 76 | signer, 77 | ens.address, 78 | rad.address, 79 | signerAddr, 80 | label, 81 | minCommitmentAge 82 | ); 83 | await transferEthDomain(ens, label, registrar.address); 84 | const ethPool = await deployEthPool(signer, 10); 85 | const erc20Pool = await deployErc20Pool(signer, 10, rad.address); 86 | const daiPool = await deployDaiPool(signer, 10, dai.address); 87 | const claims = await deployClaims(signer); 88 | 89 | return { gov, rad, dai, registrar, ens, ethPool, erc20Pool, daiPool, claims }; 90 | } 91 | 92 | export async function deployRadicleToken(signer: Signer, account: string): Promise { 93 | return deployOk(new RadicleToken__factory(signer).deploy(account)); 94 | } 95 | 96 | export async function deployVestingToken( 97 | signer: Signer, 98 | tokenAddr: string, 99 | owner: string, 100 | beneficiary: string, 101 | amount: BigNumberish, 102 | vestingStartTime: BigNumberish, 103 | vestingPeriod: BigNumberish, 104 | cliffPeriod: BigNumberish 105 | ): Promise { 106 | const token = IERC20__factory.connect(tokenAddr, signer); 107 | const vestingAddr = await nextDeployedContractAddr(signer, 1); 108 | await submitOk(token.approve(vestingAddr, amount)); 109 | return deployOk( 110 | new VestingToken__factory(signer).deploy( 111 | tokenAddr, 112 | owner, 113 | beneficiary, 114 | amount, 115 | vestingStartTime, 116 | vestingPeriod, 117 | cliffPeriod 118 | ) 119 | ); 120 | } 121 | 122 | export async function deployRegistrar( 123 | signer: Signer, 124 | ensAddr: string, 125 | token: string, 126 | admin: string, 127 | label: string, 128 | minCommitmentAge: BigNumberish 129 | ): Promise { 130 | return await deployOk( 131 | new Registrar__factory(signer).deploy( 132 | ensAddr, 133 | token, 134 | admin, 135 | minCommitmentAge, 136 | utils.namehash(label + ".eth"), 137 | labelHash(label) 138 | ) 139 | ); 140 | } 141 | 142 | // The ENS signer must be the owner of the domain. 143 | // The new owner becomes the registrant, owner and resolver of the domain. 144 | export async function transferEthDomain(ens: ENS, label: string, newOwner: string): Promise { 145 | const signerAddr = await ens.signer.getAddress(); 146 | const ethNode = utils.namehash("eth"); 147 | const ethRegistrarAddr = await ens.owner(ethNode); 148 | assert.notStrictEqual(ethRegistrarAddr, constants.AddressZero, "No eth registrar found on ENS"); 149 | const labelNode = utils.namehash(label + ".eth"); 150 | await submitOk(ens.setRecord(labelNode, newOwner, newOwner, 0)); 151 | const tokenId = labelHash(label); 152 | const ethRegistrar = IERC721__factory.connect(ethRegistrarAddr, ens.signer); 153 | await submitOk(ethRegistrar.transferFrom(signerAddr, newOwner, tokenId)); 154 | } 155 | 156 | export async function deployGovernance( 157 | signer: Signer, 158 | timelock: string, 159 | token: string, 160 | guardian: string 161 | ): Promise { 162 | return deployOk(new Governor__factory(signer).deploy(timelock, token, guardian)); 163 | } 164 | 165 | export async function deployTimelock( 166 | signer: Signer, 167 | admin: string, 168 | delay: BigNumberish 169 | ): Promise { 170 | return deployOk(new Timelock__factory(signer).deploy(admin, delay)); 171 | } 172 | 173 | export async function deployEthPool(signer: Signer, cycleSecs: number): Promise { 174 | return deployOk(new EthPool__factory(signer).deploy(cycleSecs)); 175 | } 176 | 177 | export async function deployErc20Pool( 178 | signer: Signer, 179 | cycleSecs: number, 180 | erc20TokenAddress: string 181 | ): Promise { 182 | return deployOk(new Erc20Pool__factory(signer).deploy(cycleSecs, erc20TokenAddress)); 183 | } 184 | 185 | export async function deployDaiPool( 186 | signer: Signer, 187 | cycleSecs: number, 188 | daiAddress: string 189 | ): Promise { 190 | return deployOk(new DaiPool__factory(signer).deploy(cycleSecs, daiAddress)); 191 | } 192 | 193 | // The signer becomes an owner of the '', 'eth' and '